site

(djk)
2011-01-23: Exported into a new repo to get rid of cruft.

Exported into a new repo to get rid of cruft.

diff --git a/.hgignore b/.hgignore
new file mode 100644
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,11 @@
+syntax: glob
+*.orig
+*.rej
+*~
+*.class
+*.pyc
+build/*
+buildtests/*
+
+syntax: regexp
+.*\#.*\#$
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 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.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, 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 or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+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 give any other recipients of the Program a copy of this License
+along with the Program.
+
+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 Program or any portion
+of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+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 Program, 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 Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) 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; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, 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 executable.  However, as a
+special exception, the source code 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.
+
+If distribution of executable or 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 counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program 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.
+
+  5. 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 Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program 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 to
+this License.
+
+  7. 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 Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program 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 Program.
+
+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.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program 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.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 Program
+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 Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, 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
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), 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 Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  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 program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; 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.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/alien/origins/WikiParser_1_0_004.zip b/alien/origins/WikiParser_1_0_004.zip
new file mode 100644
index 0000000000000000000000000000000000000000..97eaf0dc2d57abaf435232c2299c85e23b1d17c4
GIT binary patch
literal 37038
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
Ol0V(>CpQxQn*Ie&_CqHC

diff --git a/alien/origins/inetlib-1.1.2.tar.gz b/alien/origins/inetlib-1.1.2.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..59f292abd8038925a3a81476f180e5198e083743
GIT binary patch
literal 255851
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∓cseS;R+
zsoOB#p~1XcC$d8y*DMRYa3ECVYwY(xs!#QWzKwdKh%8Mda|tezYEPZ6pn+|%y|P0f
zhXM9b7CDb89F&!_+g5#$1%YTwNd?PxKh4#it#;OT&j^jdYBDD%20g{AbBo@HuFIEs
z9ksh!RNCp7Ls^uNwqlbLCkvXKUJ+7_ZSscEdonm{nDz$4w_cAt)toFr!5EAc*Y+mH
z<&+fN<js++tnZB!0R0jj&c1M5&2A3Kwp4Na$8sIIu99@@T&?glp_}h~&YH3{y-`(o
zHWiqRf01J^{)Nw)`->b4aue^^Xxzmw*Kpjh$Imn(sF1mHuhMAJPnMK{0@-8gJMpW@
z2nJ1f<Y+Z~ETA@i!dVi8Lt6*M+5q0(z_PTlN5stMZ~DACR_eVkFq+}DzL(C*jn^{j
ztL@AuUNQ~L6DQeD)F|zUmdT<y+qJk6H870L^`aq7vI`F<ir<URkx9j?e^Y@*A`k~R
zX36G#$o!j(45^hrUu3Q|tPRZSDT=>kAi64MYv`$1+4atcEM#thdb?47(#zhMnZw11
z7U}qU5SgycN^q1QwR%T7zsEg~@-owFpmNDt!n_H|nT@a5f7FFDZ}#38o~oFmP}1Rw
zt~1Mw9H>w@xs1+D)Jg7Mb{eCAWB;8*m?6Q38Z@?S%6NO5(Olb8k-sOM!WLQb&>qU+
zsrm}@cRH|on&S~-3xZyJ%EKYK!szu@fZ7U4^5;dtkzd6<VZrpgHY&X~Yq|5}%YXcZ
zdsFupAQs{>lDB&rT*fK-{HjQ-tacKa0TVvlL7uCtynqNp=IdjH&?N?i!raLNb90Iy
zLhmD;?@)v#M+jmK#f|D!)PIBN_U7j^sw|ml&~{4rlA{ASn@z<*lqvM^hcC~cENnMU
z*<x%z28$|?4HGplRGP6c*3&voJ$z-M;6~1p_LIL0TS+l8H~XJZc^aC~O#d~()YZSz
zK^S!CDo3Tdfhtw0{Vt<b^(s}d5jR@1$FE4Rjok#7TzT`(pC!pW#lPmb&5V9X;_(eF
zM4c<5+9PFJ#1psts`GP4#IeJUMjxQ<UW~!g2^b2H1Y}+Ws=tVVUF~C{kO69BNC1^i
zI!ibS!~POsyYx&mBH*yRm6z{vQBHe58E=2}oA~jup;d$4_m0i$jTFAFMjxBAmA=2}
zwm<3)k^0y3N+7v$`3TeGc=HL{915Oz8ljy6M$i7jx$$*yVv2Gp9@@%wCo(;p>*C2m
zX&sVG#ZA|qU)W~hPUp1f;Jd6KIY;vFwprm9;<AU8uGX#h8M_oXWa);BoHbe(r<J|d
z_|o#bp=Gw8Y9n<?+~B=9E`$PdzJwFT`NF@0zyF<6xTceM3~NZ{DnsIkWCVYD?M%#2
z`&yVGlf-;^NO^&JOL}CWQkqjHL_^!<jZ2VStbsTNb_|n@x3!OaRn+T`M7@lRzqZoH
z4K!^SL>FI+SU?2sWWvF^4w%Bo;qr>EW0v8|yd7cyo9w<i+HD{6UJxbaGZ(^F{AF2C
z$)&HpQbkA+F}V!Wx7+`w55?TjuV7{6$rD2<r9zxB#CbPN0$2J<LsjI%i0a@i_QkZD
z1L@eR@~mh~fz8VOQJd8;t#e(cJY4ZtCL=e^dCb5OHL+hQIn4G+HN0?5laQXg`MkMP
zF?8%-hbP^mlkUI%&%(Uv7`uQpc1{t&d}X~{(sBxWy@+4jr^fyZ6?0Eu*z~ouMxTu#
zQg93i%yPPI5`1n-gR^C2k?u&j>2QpKKAilcK^%>wo_7|6n@|4_MjOM_2a8|*ADK7k
z^{T$Pk%HJL!aO@71v@2T64}s~C+Ua`+ogVu;@mK;hm$5DgA@ROhDzb^`h9q<lB1iV
zaovzPT##Sat=#FF8b7slK|=a^YRX`LS3*znR!bVH<f3%d6~d?jsPnRm@)lHA-qrn>
zE(Gb*$OzZ#Q837R36BCMPrvY5)e(tgUgv0H3{9s-W>U6i_CwZ&RokXoHa=P{YQ#jD
zMPsZ?gF4YT5Yezl;Sd=^3^jR)7yc^t8i%qql?S{boj@l(<rG)VI>lskHDU@yc}uJ8
zu^{(fBcb9dbCo1}XTH?^vX<9dB<pfhB8r{=(!&inm$~1#c~qQi>>JPR#>0fev29$Y
z87Ld~(j2rF1ug6uSuH2$?Ay5n3|5?8IItHC`&_Suv||h8#yRJ^3bO9&C)p?(1`h}v
z9HLsr+6Z1}8Zlyt<Em~iOpjM0M(03UaBXUHe7l5+r|aUyK&6Kl|0;0s;sWz?T7FF(
zADyrd;sJ;1^Q<~mCp4Dd5x1Nj>Rkv5!x4|>bO2A?zOs}e0%@S7QCC=AX`_-<1qJf<
z&o*uHZ(uQb)`cZ+Tm#VObRVe~2SZ|u@5+Tu)-AfK^v)Npp)+4}sRw&@Ag75+(G*z$
zGL4t7Gpc$OKW%EqQ*SsSZFb+nUBP1JnFy{M4k@=E94W)GNK<FTy~6oKLG3Bxre{wX
zVe99Y?&NRY1gZV1wZ6>q<DH+Ig4q@=#cx7+GH1;Yy*C?fa|%!jRe3>7Bhp%#5tJBZ
z>FMmcQpZthjRATT2Iuj+XlR<y@3&uoj9S-j7g3BHDS2B8szeGA3zT%tpk0TGWSK-;
zUM{JN<AfoxOUu$ex3*vbL*KTbKcU>>%6l7jHIjGf6Nk*P0If0&iaED=3@eL^?-)a;
zF4A3aSEOYioLpAj1#e!(`3$T;_MCHdpofY{-)&<}PCs*lt{F9F?wFX;{BEgWSB#Nw
za6R7O3fr9&`cdxNoDsPN%OmN?h4Wf-oyI^lT6~2i7pS9{l>&a`cUDoG{28PjdQHiK
zSTRR+Vqg+OKzZU*I9R$oD4rTny$zoQ%h9;EJm@#lq=8{kZ`!uKxUc{1fL*Yf&(H$2
zj6+W)k<AlbmADZ*D+dQgu*}VnZ~<+yww;@}qaLIbZ=SW1gFbnLV+dmGUhSL30OpD^
z%Y^4q+L*sDf?LuT-b@QR7s#Iltdb~U`og>3%EB66auqUb9x)LPTV>sm;6XjJuVJur
z`07>jU^m~1udEB(cbt5FiGx+Jt8frsUPk?x(hoGUqNq$PWJV3LB{tucY#C<@7GQ7Y
z`Xt!}yoD+9bNQqBqs$O(YG!v7RBncS%d|xs$%@D{8K;#`<(AUHt0gNg3fo^8nX(Co
zZOaRCJRLZ87k>|a#2_%regor?xHiIpEyU;<U`<ZpeU#9ciVG<^n)<9j1vZA`XfE}y
zF~3A_A?u+%qc2FFD~R$0X_$T=xG#-ld&}9$QEns4%5S6qL&fY<sCU~;vh!i){UJ0
zFi+IKO?V4aG1F(|_dk?=s_*9L#hC*RR)$`}gapDA>8>ZyF%Zhd09<3nMB2CU`7lAu
zLJ|sxz*07pB_0<tGm@AYPcQ-7TmPk-uT}EU%1xiMwPLc?`LE<XD~2q~s5-c$8mM|;
zdd|N0%skRglj$`=k+Q^RhS_y9`YC*qM%+#o!`EL?Cdm10GKQS8Pd#neez97KFuM<^
zFaIHXgO>LLwes;fexUL^g1I9e?aT_jkc!lbRtHr?6UQUej>6$P1DCYBue5c$p*fS!
znCO=0;XU3Xt0P6<9~+MhtC#L59Hz+H;`EZlOfxT;x-ZmSl}xKA)8NhfB5c^9koB6@
z(49>N%%&9R!DTcKdvFkiJygaoHs*3~IZFoJAoQS!8ENo(USw`9jDcZXUHi1~J_%Xp
zSY=NYizbLo>&?(P?@_N~mUxscDC)OYTFtuJ|3+%9{hc?>{e24MjRYJ%W&ik%RDba=
z9nhM8yFno`(3=M@y|CfAf(+jcxbVv}KqRKH>{sagNAY`vK_n%KG$e?It5wFh`<H!M
z%S<L_j31X?4MGfKM_z&=a8jT4M(Ks*wK8%@I;7S^V&yqFZGmkO#U9A4dhXsJ3OQ+t
ziV-bEIezuKeo-LQ(O2%l=rR+XN<~K+T>>fS$#@sg(KjL!+{)(|QNSCE$rm;0Y>`Ke
z4nY_gXG;y#4x{w7Q_f0!?}T!)-+Qh!ZBlc2lOQ|CukJLy&ghbZDsUjIQGCukDrZ(v
zD%PBE3tHqX&uBX2CBtyay(pMY-5u38n<-KSn8VJLQhRTf%VH~Q1Z@-Oq26qmF~Vqe
z-X!1RPEUkA#EB#01`N`S(Ie_WmN<{nC_0-Ahy=b%K&I_a(E1IEWa+!?0BvqLKkT26
z4s997e9^Y$m@n3OUY3{3>&6bTuX-uFF51J0n+8S#EAKARA=n%sS)ss(d29@dTVr(J
zm)ejF*Au*+`{MWCWx0!K<+KoIA&X94uXb49En||d)l7&rT@$B>2!Z1bk{go^iW_8L
z-VW6!;{`kFm!l+2<5P>R6v_;W%kwT8;dYmC&+c03f;iC#wBwxgXViY5eksjzv?YN~
z-qdLIfWGq6^CG?lO6VS1cXRokaRnf3x46K&SEhgv(MDyuTuPK1&pR%%nceSA43X86
zxHiuPb`zSgShug*biyj6E@BHUC%z~$oygiw_>3nU>xqo{gprtG{;Pkw8DNcV^rf|r
zzOcE7Rgq;On}X7NWh@NaJ?Rl8GL)QnX^t{=@-qeh@(UnL-}c%+y=py`t`MWhbb&p^
z9>BpTR8t=9qOkt+sk%JGk@Z6lLK~SDD#ftWsO8X13>I|M-ZA_yWu`n%s3(GBvw>qH
z6CFTj1)s+gj^;rxp!xPoG}InOCU$#B8%yK5n&q#KeMJ@AE&6L`GWYe_EipsCovJxl
zvS|8m+T`B!#sayKgZ$ee{&Sq^W^k3$pk;adkzfArqEXU+lZ7jV4-)Q@WDF7Z`y6IR
z%tu|;MD>oYjW?6pa?~@6hUK>FnNXzR@~O5&2I{+4Zh75GjIAYScTUd15Y<L}R;S~b
z=rpOkQ=UwHi6LbZ5@XOkVQ{VHF(syzJ-I|=en59F&L+>eCViDIRnF;jPR7GSbac+(
z`HbI$<9uQHPE7H>DC)>yykurbkyP4{v((IT^bMI}dX@%$Q!qX@dR9xuKFz59l-d5J
znpQg{pz%VQWa`7ovheXrgHj8@I(cDAjw?;807X8g^3a!xRLY=53fA|LNO92*bR0`z
zpGO$Q@{;G0cS+tXICi&GL74CHk*%M&+agax&qBK~5r!UUetvPT^ga2j!`+sl4v=*y
z{UT6WM_Z7iE1*S{_b-+TLn`M?MMl13qcFXIy@&7cRt!HEZrMMWyt6p?yU{CL2d+9d
zXPsWz?h-desz>#5&f5+iqE_JXzEC(~JA^RAWQv;$92fABeB{tz;mCo769arbH-D0b
z=TZkpG~}1Vqi*}~;MYc@5&VM7w$wa+iNEINjz-BljP=GzGtuixu~JLq*G4SX<cc(q
zs85f3*g8$w(h}On#OHzfBP?Zp7hg=olGy6yrKNs!Ix%XA=m|(YPm@$C?8l=J-Sp;`
zhIOn0{Xu+}e{zZ>W33J`7&M?Ko2p$K`)+CJEDF)yIdwsthH2b$dTfS~LB<|eO@d)#
zRy}5%VW9A@|1DA_a)AX=oYv6zldaMLNiQ!k)y;Y2wnMORv7|2;W?8VYoZkM@kIur$
zV7v`|jz)AHiV>3KGIxFz@cuiAqjBKxHyd}#E(=pn-DT3%a4LFZV#0y!x}4#0c)s0j
zzEl+_X_SWvB8+o*9zX?AU7nuuk6V0~&2}?4M<bUZZ@o5%N>xJKoBk6toKFN5OPXCc
z)m@mZ%Z^H_uKXPek5s9*q}Y76IAPo{;1#m`Q$D41FM$ecX>fsheR1)IcpgfANs)0I
z>#!?IqhjT3fSJf>SnLSy3W)rzH8DtQsLSSU5E-)?f5O8sRDDOi_|T{*>S^)UntH?X
z4W~ae4R-Yk%qav$q`4MBB`N0#pSSoPGtRSLqoQ?U#~J%)0ZHgw7?53EKLY-aT8jAk
zKuj4yo0~h1h|mFfB2ncTe34cK|2iK|8p-JVIhojbZ&mORC49qEv>Z7W{bVdXeii(3
z{A$T~z1QAv5uH9RgSJ@im9&;Kt$t<F9vBi?cP29T7Q+kEH0i~}Ym+2F#|<_;R~uhn
z31of!Z}My#T`nxbw5x7*DAXng!%u^nG*twua<H25-VLn15tktejLAoKDN&YxGf%N
z!(^5o6LmvVSBW>=-p)~P<1uwXuv(4a5bu57$7xjOK;zujIE@Bp*ra61CTMk!9h#Rb
z$Gdy>wG&if=kV1LG_@q`2eCxw@Z@->MKd|KQ-*k`9VH`c(ft<@d(K9Z+GGypidYb1
zu?9r>mXd(IUYBU}BB;9SX3NRbtfE*KBVk6PC0KZwou1iW#Sds9pJTZjl~4!372Qg<
zc0gAY5zX{pI6zlRf!iTqQX^RqZo!k7=nLmKmXXlNiW=tEo_@wIshDFt#0vz<V08<
zBXoc>?1!WNf*EdUPHi!^%hwA@YvP$k!YmQkmqb>gcG7mrV`V|RcuoVBNlDPiARbS~
zvg(e+7c}99*Jl_m7$&8s{f6gkv=N2sSZa_gC<Hfbac4qsd5FoG{e{>HB1_Gg&$C<L
zw6LR<7#$RwtWhzggVLQc+lLOPnF3zR2<&Py>Wdh_+6-+@BmhF9W|$YzxfPBg$1Iv4
ziR<fB%nmu(ki`O;#tewA$*8&@)lt656i|lRHWN3qS2YL4&`rl9nUX{?@^hwWiY+Of
zkUMlnZeX~LO(a&oUuSgVk)_($MxmE>ikk_$1ZVRLo8L29?aaj8K!p~bSi*qc&e_2#
z7^T<oRqkrb5Hmd^VTc%gO3B&8#G*VDjVNq3u&D2V1fb!45S*mZobacS@`_3m6SKPz
znJP#pAyk#wHYIYd)tnlZ7Bfw%WW3r3oo;h~-#C*1Q^h-j(F@o;O3jE<O|lf$N=RFp
zz_8fY#7!j_n-gwJJTU@-uw3n3Em&j$bCt2OSQzUtuvA3z2I>l@44&O4{xKfY8Z)i0
z(aG|nv%7>4ptGj~9Ya$xDhLrUBsm|V$h)W}9FLQkXPX%0{r184);#ayQ{zmrXBoPE
z9`Vd10UN||$N|emYn;qUZrfzAjaf6Bwa+|eaQFxjJj#e?3Nev!PH=<V#+)M3%weYJ
z+!hY3F^F~2#M)RTSU0OTOw=OcD^)^5Cgt$WKKT0y*>j_78IUqzz^%Q`C|*5PM1uxX
zQWO>OC<v9TC{9V(bXR=e6M4@MBI6=$=z`!AbL`%+3$UNZ@udJIs!3>s0<ri>6d5`M
zOs7c3Nmx3dAmf811nT=nJV#)*)jDxN7@J?#q_r@)Ic8>Ht0aSWl4b7?pm=S4(SRrG
zcYc0FoZS~+7`&XYDI*41#JQ%zcw56vlB|{ixHJNK_aQiLy=?td4k&O4Jqn-CbpWF9
zH#D>n5!xt763(VA;uQ0^Pa=rHLGxA16idx1QdzkSvSq~hhEJ@GU^v^X0Y>tjvMkdU
zm037KB_6Q3OQ>yMQUWf9jv@;++#Kc`VLXR5Y-yNXL=k!A3j$zC(O7FT>_=zBRC4@|
z-XN3#vUDfu27+U>oH0p>J^;C-6rvi@GD3l%E#_0j_^M!=l4B<b^p&pk7zUhq)D0%v
z7PXUEWfdnTRmTQA*bhC-)we`dEX)c#f2@fTdIW<7_n{c<G)gvo+}dw7JJ40x{8iX{
zi~2TMz*H<@J>mfk{G_u;kTE&m6e@<&Ua*C!lRY?4&X`NbzXZQp$e#5h6?BD}Pnbn%
znaEy#CeCMMrDC#~qY9Gba|uTt?3^4Qw+_0$JUwpx(8iqIzdUWeZ2kJH*xGW|<j75R
zT<{BN3KWklG?7H?R(=KP01sR`)0BQdAKN;Kt@PemSmcAAy*iMqgT}4}1j`vvtOlo-
zL2V@(j|U=Ov`&kG@=v&cK<A7qCJ&vNp1NpGs2WBTahv2reR^NdIVYls^=fV-O6sk;
zAT8=Maz5rY;hxp!h-})D984I?aNN|QOcAX1CUgHVcZ9X*6f-C3W*tZX<u9e|+lI&s
zyf7ec9APLVolYIbkkkKAaBB^8ms2BA(~=#^9CIG8|BX)3+&_+nvp_tJk&##y#CL^#
zz$xHPsOwMN6+Tl515FX$m|I>{)Cu|dD5LX=6ST~nXJQAn-gCM_FIO4LO!l8kwU?*I
z3w(OKxtr?ozSawNlbb@XwX^qj5E|o~OLhns75xfnQbGr=YqxL-XOhDgd-JHL$wtv&
zzvyoBB@#~L4tc7WF`$)^(ba|&k^*E4sas5AIHc%w!+6jl)Fs|c3zp>+nh)T-=%?bq
z5Mp2uyd>Qh-rx-49#Rj00As`}z&b)WPuM0to0S0jB9N8BRwG}Cjnx4H2Vr|p+2`Rj
zN1J}6Q8Wh`=lJ*0NcFTY17gqpT_>&z)3?F{kQ#+g88w@B2kavfuB2VDxXNS*mn6Kh
zp~_V_f+0+Rxk4Z1OX~0tdD)=Z{;95jzK@s*!EuONQH%!)7)aA7CV~uF-JysW@iK`!
z-oIft@Z_f}4R|iQOU7vL)q%>%pBUg0?FH*rtuaSlU7?((CDAnAuEEGigNf_4fcdIn
z51xkXo-NmnK;qI`K!f1LWGre4oJxdTsB5vOqUu)z5f#JMzG%H{A0V64Jv`h;v<-F+
z5BAzGpE`Y<~@Fc`+}ld50g1NbUsBGX1Fa7D@#Bv-wPUw+upy8xUN>;aQ|H6Im~-~
zm~3sK0X+XgGsrEnW%zg5-g4_udOrb59Q+_}N1wtYBnMCFDtw7=su1NafM@H=fD%vF
z;8APXe>(TK>hCZ22b3QKvD2m1#%g12nfLCp(DU3?8!Iaq*LIUXA8%Nn@VB|XzV^4(
z)%A_dwe`)-$53u{?aBK3--18XM2f+v&p*iD-@goqYfCT4dPw-hkxjTz_N6kcH4BNT
zNEi{@9{hW$2eSMEk?Wo0>UtEPUyOs*Z@>Mv9<0F}1bd?>3OdQz_&xF&dnn0^STyZn
zuR-m_<1sJ?r`JIjj)X3p3(<A(Y)qg3>Rq1?CusvZ@*E1!2{%52**-(~ser#l3Q1J4
z0PV(*Jn<=BH^t=tvrKFg+Vmj%B1S<6C|<UyJajJ)PK3qiAUK+w0>2XM1D`TX$;Rah
zp8zezi#fqO<c)Ak#@UjB?ivX#HCCHSNp=ySpu|mnSF)&osCq=oKi~p*P{AXOS(U(-
zlV_6wGDw(r^2c`fJCKEf=E2XwkIm!b=0W%8EoJ0{ng>>GU2>*S7zBt|W4r_c0Iyod
zJKsT#=8N`zyZbZtf3Mv=07f#{J3J1W!BO+L+uk|ZZypCnC&x#J9kiqn`Td}N!DK2f
zmJKOV9`nzzerDPN5L%)bb(10I5}LY5IGuw;>H@~b6mFqP7SI<M7(ls5>P)QVgW4{N
z)`QJeC=?Fg4p5f~;(r7n_Tn=Dzc)ydQ9XD8j42j>)eKhFR##V+R@YZn>%mE<N&BKn
zp-Wh8EktQV2ho21Q0n5Y2S6CeOhcmE;*fbwoD;2Yw=bzeml>0J8qQ6dXuNyHQX)iL
z2yE-HH<<JzwM_)7lcksH3k`y{j=Bq&h=m<nH!#SLs%wV72Q2wg&)AUR==kvEar2dN
z9sGA7ED504A>P<icmtGi`&y?+&hjiW%86trZ3z@w!Zx?RTAdTQ9}`Yo)#DG2$pB=H
zoPnXIU;SNX8Dt|`ky?0lUOL7tB{J=LpeGfi5~6s(wk0ey;~lj!?@EJz%kMoTlrTB;
z|3+`2p@DUnMNf?Mu#@w_HAPQU#kl)lNsn2P(K0aH_~yS#*k6>Bf+ZMMp^=>ahrI`k
z<TGZ#0qB#cMz0c;-AlU&n`Y7~1%0EZW`k$+D`_Wefrt5XYc7^5C)?y<veB_+)tji0
z&PJFn)0a{v@faqEOzQp=ZRpd8sooIhofpsy`uHw+D{(DkFjwIimEf?rc<ruivPp?d
zwzlZPk0KyTtI1ms*0txtoNy5USzY2MZ$@!N90h|Ul~-VNxrRsB1qJ;As5AzKLB<F
zYT*t84<Eqz)9{p(f26lHx)csl0V8=!I7Rlv935kxoWfIKK71f_LflLwvNjtRfAW^E
zG1p^s`8Ru*^n~Mua#j8vU6p_TeSv>1zi#xtT5crE80Q+e1(98VMvwWJwStP%e?KK^
z^6y`k8%w_~a3oQ@jWE)Bs3nj>I?<=05QjwxoEO&YNW#<*(T9Sd36oMsvt2WHnn<A~
zb5;M{R0WEhVyt+Ox(T{@za$J{>tQ6v;RyA@1HJ@m%;6Bp1zk*4l9?+S=kITRKVSHF
zqb~{5Xc9?!1jx^+Aiff?g6DzPeMMY8!SueJQ)c_=0FzCx@<vi#aifr|fre})qoTEv
zI^=DP5fV<L@q5HFZki0BfIn=^;lIj@Ky+w}m0LhDSA(@+brV<lm_spYs%&GNaG=0&
z;#Tl0o*!c;U%t~%)#fLJwfi7cUcw)tlI>=f|JX)s`5r{00{M@`B`N<sUReYAZ*^mH
z^KZdsCI5Yz^#`$Q`ID{xI?DfR8&9hB|D@}WFP1;m`me98Jg(ON)35&-uujov`GMB|
z$;Rs1<`bC0)s2nEYt{OHHtWAfA3Mo#2)r<=sQwea3ul{}$IiOz^<Q0my!zPC|33cP
z%G%22>RP4$eTcv1ujhiV1MdRk3$50z?JS~Fcb#sK^VFVL1^?ewda79KsZiM^(5<eG
z&Kk0g@%qNPT>x_@%3Xjnn|4>Wp|p3&2!XizUZ1wtKm1wsy#V2s3SWS5JB_axL}`_;
z82IhAz5v8k`wHYLrS?Vqzm?wCQ2Yj{_yvdvbYIxdSrM`MvX<+(1o8(?Ks~yc3#<V
zOwhI^P7%&!_*R-+U7nJq#`|RSmNXZA)>vrxr1Y7vQ;5KR*dYi-N&02>LoazJcF_Ba
zWDs$EY+P6h@Bsa~?YfZpCE)-7cw@w^8Z&_g1*G8yNLT3bl%_&6PByJpS+?vMviGJ)
zATtqxV6H~+y@w`Q2uWLI)d7>v!%-iKr8;j3kI$qFWV@*#(}Xi{k!3w$G^CUzrY4@0
z+>^mrX9poW7*?4h0g-JG0bqldI`$Sl<@;J~j>@^<k`PQe>2pAGB4?RFJ8SD%SUF0e
zf7e4ot=G^4k`1lIcfRct@$6wNvDCdI95jr{!8?)4hAFZ2QeQ2gIWeOny|?Z|Xqt@B
zCsv*rq-``=KtP~h)D^(ka}ntPwdcAE;->OFIt0nLb!pIs4Kt$1wsA{Fm(rKc>IY)_
z^4y%X^+n%Zq|puLi_o)%Ond@7E*o*ucriIUi{OYlB%@-a8{y?vrqJ~`Y69K8E-cj^
z7Q8!b$=+w)<pafzcaq-QXbc@+{+hRN(1U{jhc5J^JTgR6ycQY=2j^2hg^u@jg4MNe
zzL5mrU*QA}mQnC53@%2|+4jOEQ1)^1w4a<yKj($#I~OBZ>KBs{fJ&b&htFwFubANU
z1|jTDm;IRg4c$Aaive*H+J;H?4@O0rYycGm0+=wHqQt*wwifKR_L?XA-8V;v$K7B%
zc=GKQ!_{C<I!n#YPP-jM!(M_0zXnjid{k0fP+?wizn^s8aJB7VUV@n4%5<7&!GOX@
zq%`sDJ1TD1FDFBhn>ICDO~#~q4tS|VMH@EY`TK|+z(N)8g8lo_$Or2*Nys%^l(nI;
zG9D?wFnTX1xTkqRF7+?UK^ah%QrCKvoT>o5#M?yHM#m#(johWKi5bP3d^t3}7@<+G
zf#dM_H}NV?Ygn>wE$t#+8hw1=G0Dm*tTunS>DcRpun1%7EC^KErlB#Gh)JR9*~vJL
zpdH}-RCFoA7j!voD15@(20mFR-1$hdWa7MH){T87GW$+qSDgO9uxpE|v!iG{85xM@
z_N^m*XYZ(6a#<ue1fY3O@wAQxjCepw+hBJ@6z?{T#&SH){#mpzvLV~r&L8GJa1J6L
z{5znL%qgTYpR3ulzzOt)sZB)%F}sZ4HM|U}2l%fC2^jfO$0-N<U{4%AVUK=k8;hQW
zR9Tc0jb;sy$x%sq(9q~|1}FL}e#sVR9IdQ&gAps^S@jh6Pw9CGXr|d{8RR*9xtJj+
zs%}5Q7?YB$5G1=k7g)UK*{uf#wJ9~-%geAkhkY~TJv}!$4BUGdZl9=7<2)LhFKdf@
z>(6D7TKV{+8oU+-F`%EZ#qjSp<-rBT8YIQl7M2$lw|vikG@m0dwTKww5$F8(-}T~%
zXN!i9*l(7<m;pI3AtR!hpT3rg`Y+4kd?7QK)+9f%s9MpG^ZM(ZnSc3bM#7lhq5<pm
zXhAQadN?#6o((4HMNNT_Xrn@tN}hyX7#uA!lP1;}uOu`tmyGcfBr5sd%gU$l@|Ryh
zYB&?6>A}Xzw~v3_Qt|U@c;XHs>`6RqV6e*?R9M`iXE51It6QGe+3ANqiP?eo^<brr
zwVY`cyBdp*6W)k=2?Xha|2|XwFQ=dz&8YS*ssCNsz{_4s|GNsMS1SGQ!|8v`1+LV-
zmD;yb`&Me-O6^;zeJizZDYb8|J{BvT9PfJ{YCVwmH8Zs0U^W%D@DBP;+%Q`SkBF|*
zP-LhcM~i#c`dZN|@@O8Df4$pb*5GOdB(mDT=kKx>_Xq}?)e&Vbp-mm167MxkYYx#N
zduXWEaF!;cL0O#w$_U)P1+|7CJ|%npIoW?$Gc$`UFopl$Twhyt>_1jlo~%~<|3lk<
zWEZ%S1u9vfk_9SRpppeDS)h^y%9(&jy>y`g$4!ho$_f*<sGu*s8Q<WvNbgBTFpW8^
zKG}kTYv<we{YICO^%mDw@+x6VSFu_3vK&8!-8C6|`_@-9KK<?$?`Dk2&Apvr3B$e)
zFzRg^vWG2r;Kj&y30?!&1M!muCvmqRX9SarX|_$?j&g;QG#a&sXNjm}j@G=6aj#&?
zjQvZ|Xw=V+SxurnY<Vq%H&D=PWwD}erL-}P8lw`=d>gAqpv2IE;7=-}eRBCv5WmL7
z_;PUf_dhG^n=6_7pT|!s`R^hAo}DKB>*sTG&#s=&byc>#lr%~=v<>VU7r$ZybEt_>
z8)At>rx->P_^C+`Q!4cv$WEpST0Eq4p@u;*AQ%jDK89Ugcjw5YrWk>7kWhx_XUkVO
zu4NuzwbO61|Id@Fi%;YK^<<+|N3m!|0R9glm8cd-CWD~e{EFpzaCKk=Uw1R`By3b
zD&=3L{Hv6ImGZAr{#nXD=8HUr0Qc*(C0?j*Ovdq`v2(n?=WEw4^@?+>S_QSDkjEqB
zy}Py+aG8q&?=I1g8+mtqv$8c;<UnP<vQ3fgcdmJRk+YPkT;-!hj6is+xPAr<Ls?_z
zI}OMm?j*Q#d57*(WWIeb<5U?24TlTN<EWB9==GjH85zH;3?z}nV3w(y!TSnTpVZ2;
z?O-LV`J`9dfor;=b)aQkxPlTk^3$C!++|d}Q<N?+v##5=ZO^uC+qUhVZQHhO+qR9_
zwr%U%|5{_Ov)8$K@+LQxjAUd~1;t0sCw+O$BWgj4Plg!Wn_W$#ekviI90+X_W)dY@
zUrb))*n==nRUpMLXoS#I19!=)tc_!l=w_DxDwaO=Ap||joFtr)uoskyNb|$}iwi-h
zhY3NOayDeR1a7P4bK>d*Wk;7L{+pSkWvsJo)|6%EzF>b)BsTJ`m$Jq(_LTFsdePdv
zq0IT&06bhjR|QkBZ>nFE+*?-yn<??DN}f$7MTYh~m=`l6%-s&_)I6D&qPMs!#yM0C
z!WQ&h4Wb09Sw7+NNdy%u#L5ea^GvCUia?Zmg@*70F(t4zvG=b6&)WeA?9*6XO@^;U
z=oiM0xb37T3`0XKaY0O!HYYq{3ii4$c3%3z|8@&t!dDHbe-=muNR+--uJHTQwkjxI
z_)_;Ol)imjUHB3#0f&HV#S>1Pou49`{_rxV%V`N9Cb>^}dVz5`zU<R~kLvQ>?*C#o
z81i;AGR^`_Or9dI0F7}E{WQOT!8`vQ@VbydX{?I>4IU(jYnIjFcf>t}<hNmYRxe0c
z4~sk^Ve@mxtFnQCGgkaRK|&a3+NWNWqoMIi^$f>x&%5?fu~f_@h-ae?VJySM<yIgY
zB4||`%%F^Ky~-NinhD9}h~CJ|v|3Wc-WO+7XgR1QHwCA`f<GHhm)?AaKp;Za>+($B
z6l8>I5cvFYZ#LpkjE*-!T1joGs8XDL_bJ5ca_F1E25b~rI6}yQE58qjN=Dj9Y=Np_
z;V=dlA48Rc%7~&34IqlvHF>y71|IC7WnmJQf=z~Vam~IOzb(6eN{<EJf*$Rk1ZwK*
z%x?S%H9iOl6n~%S|Ni+>Yclu`Ui(weH6r~|&)xlB`=Rb7-&If1h*5W6v6|f|Naw4A
zOIR-SS~#IHfQKDm;X<sUi2sVecJoNZ-?(e-I&UBtHmOmvAT5q~ocvMo;||*qVFk)_
zm~qJblWRmRV&F$3(q*Eo8>P`RD|9f+T;*STfOT5LfgM|5u_8HUK5su6i9{$P;9st=
z6t>WMZ!?^BoA`6{6&(==)|kIesa)z&4yv|pavFBh548-s?lC5Y2S-kdlq+_$8+z5S
zH|%jFG%L76G!^}~pqv5qwS(MT9$aby!UmuLBk{NwzMzn0<4!BwM<`Di-gSbSV9!oQ
zNATIMZr9iAgC$z5#O#GL0~rqjV@!0-ZxgMxvB>Np!C@1)eZ7tMnx1s9-*qr6!Wcz=
zz{wtNH&J(S4CC&xBG4lou)rn}M8`}pDn;_#nZ{e@aCD}5{+P#j>Y3#=I>Z>2N8Dh{
z!{9QuqV{)nwLQHRLikpnje@niNW;uU1jVNli;y}IbfY*5>97MZO9eFbFXERcaIT4~
zf+8g>hb*hO_983x<Yb=`uN1kp=7j^u*vB_iuTi66k(-~NcXcxv&4Vrjsb*VHX7#kA
zttq2?_rmD)IW#r2JS}f*Z%*I4ztt?YnDs;l`)_sWz)f4c4$sc;QNGNeE)BSIx%2z!
zdG~PeYBpwg_+C0lk3LKT%Q*NwyY!&<BL92ciwadh_KEpZ!&4dbkil2Dlg)Dn+DdZr
z>fqdgy-hpX3`MD78={+zK<`CMLwo6fp9+j^d#nG#Wg5u4-Nqm5G|@SUQXqnMHw6SY
zA#MinW*z$#$SWHKb!lK;2Jm|`?|QTrx6W3-e?DCBgaX=R2puAi{aQOs1SUAib|ew-
zDSrHTdH*o<<1sIFca@w~jAQ``^((88Dq)V0bB6Eqcq6_?w56nJA7I1hdpG@@jAIWO
zXb&q}E>2laZBUn1_!7pd>BFP+0?d#<5;zUdH}5pDS|9B@PBTk(@Vl+yN9VjaY<2hd
z;e>32`(mIKgIXE#k~6ahVKprDtN&pBsrA!Iv<iHnOPK-A5>B*s0?M`lHMX`L5BVCa
z>@<LU1xG+JW=GdgrM-XER{rH{`TxkT?w`(wfAYV%@KPjiJ|@H3UOY*#F^5Ts(1?Hw
zU|mI0$-ajx8Xc~Hl>+II;6xZ9h|VlXr*da3T|Kg4%L`ViU2P`%sDZ66A$yZq`{K|w
z;AP@O<^<rHGuKBk-f#L(q#hV=MN(i{dXC~6Ow)B-&jxoy9$*ybft0^1-Gah!xb$-L
ziAU7v9uhRLot3jaW%3A{0^rHee>5bxm93GaZ6*c?&wvOI>9n4WDp3z8rR$3;1&$%|
zJa9UsjeCN?K9O-6nS$(D2U7N}Qp%{KJ(Nsp)Q-*jCbP*JF-kQ$sFc39cq0bLNhN|m
z8<LufQlh~;Z(6=nV!|tgbBv@VPb6n;Pm9i{U-bt*;)9=*JislKwVnJL$cOyCM-a0Y
z@HngZ4}E+hU3&2hm&8!c;q%;oU|j#`Tt-%oP4lK^rOd!qYHl<U9cdXFa5TG#w`sR)
zS9jN%I`Oa3fv1-WIoa2Q3acZ?@uPQ1v2i|ZD;{K$qB+2urE|$Xs8`f`ROGF^T(%{3
zk$p&dj%`$ve=Bq=GGDn<k1M;7PJyaACVbaMNR5Uybd>+$;uDjr<a%nwn9$r#PRYaz
z;EfUrN)}2TGdH}o+PRJ>YvN<msIF+XhBixXf*rjo?7q(hO1KG%L=n>?;vHmE9w{_0
z7jg?i120d^YV;#Wdv9K_;{BGXV6nId^wxhnD6Fb;=mGZrwNeANg1-RjAA8h)3`@ak
zw*6`73`@alS_1Ml;Lg8ZVB1hfcj9&K;II#B))&GUuFW{%=FQ@4Qp2FsA4u@>vYC6L
z@KpD;u0~wtMM+4M1t=Y?C@vcjDq(utNHK~D=`jK^e<y}JH!i=eE^LJ=Qc28&PRU${
zE<f$U9Fj)qvt}F6PuOo5W+E?5_l?a`j8AH}9?$xw!OY!Ah|i!y4;CK9Ev%!&+bUzk
z;36Uii=xc0P)!H3Pm}H5O<-#y$0>Nm3A1Tr+so<M@$w~>DtVcTe!q>OyN_zv(l-W2
z_w&1g%WeqMJke-r83y{B23B?Z-Z&vp)s;f*zKZe4q#_!$HHB*or@C=e!cLCD(c$?~
zjlI1_UX=Au31LaGX9(735K<I%Y_S&p4*bE{5t@nx&Uj#vzUxDD1^=wabh(Ky19YB!
z$7j=Qyw58TXnf{}zfT|gr})%8GA&;tSwKQ>nkWxLQITWyD@eA=7lqGkyt~jlbK`Q`
zYWT+*7p#g+Cs1*h{8%>fL(YpROo0%S^}tFvGE1Suoxk!Jsw?5JG)pHX6;07-c>#h4
z^23f4k^m2msep%OEdc)K`oWKYH%frjYyVZe-Y4Mr;Z$4<KqwE$*D|a>^e<Na)`$W~
zKNwK`(v13-z5}kPe>17Fm~PMPIT7Y14o;LNHlUt%r*-48|6{7&HMxC}@z>v6H~@j_
z66E$DU+G`gkKmx^*djy@>AjB5uG3?OmU7V(#}%Sk{eM$t0<5^TkThj}8<48jO<V%|
zoP9C-l)1!W`}k&1Efz)Qr%)kfqU}LveXF8Q1FSgiN%WX0=y;`d^qw>2_tBFZTCA5q
zZU5B&vu`m=i}YCvR(gCM!|oTgz($_+@bSt>`WtZX?Ohzt$i9@J5}QjjyjS;SV~Kja
z0Hum0vHO|aDJ}(@Vg&5{E3n2djKD^_fjKQ5V>46dz9bPC$|EML96s|qcmG<!J;MRg
zOjab1GL_J`_6_A$L_FQf9WHg~+xT{~LT`OmUCx-p0w7<&{!{4&7gG*$v1KdKUui&G
zSh+gr+2JT5FQ0^L`NZR3M1fOVTobf_E?TCcS3@m58HqR-eYh7}>Te@$n^>SS;{HXr
zC&tlQRM?LJ@zMw;XBc`$Z%AFS)_ww0Xf$bf=ceL7b|>A#))nMF>04T0L?`}k5t(OZ
zrB8#8%?_Pb5fW@n*O{A+Uv;41wg+yJ*lN)o!tBMW(xt$GTkVsw)yHGw%0UD}Il9*<
zi`SzAc(8UxFCP8gj%IQKDf}mhZZdngWf?0_b;AnqId8F%t8wwUX0kH9wwlZ_uhUsJ
zyxcvsuXYnzxRDsVGzRt4TgB+5^PAy)k~REg1Rv^$g@ov{_{?KbuThH$T0R96O2Jtj
z-|m>F4nNgW)??ZU{6&n9!z~5M%>r1M%>Dz^)@!^8Al3m?V0Enk)HepDKh-V%sju}*
zziONPD4YFO^50)A6?>r<qt*9QPkDLP)w+ce!HFXlSdbaE23&2|KbEie$8YB^B|e1N
z`tB7(NV(8M)zwg|rFSg7t*&4?TKgbl;7s3@h+wkOKD9>_SCKVz=7o8q#c1lNQAI0-
zuc5YZqgz39hc+t16+f7zD5OUW6=M`?+A!o7<8VLWe8h46wd$p&K<2tsTAC<`yW(q^
z{E_a_*7l2*D6n0Ef2OP(Ac<vD$E8$&K#1@ITGtaEqUFr=&5twlR^c0+P%hNT-m9Ts
zn(24X=7DMSxAh+%Pu#+(v1x8-0~LoYiTuGK7mPpdY<*gNIEofyUIl1qs#AaF&l-Pc
zcd_0BG^7MJZwPjRzx%raZEhz3_x;kp{+u5I-D9a|4y7O!sz9kQ1<Jsx)B)1qhSfj;
zG+vbLd+guK*KfdU$^aR5=t;Vz!gmAyViGSJIqQ;BBdm}S=zVUG(rbdok3p;mBQFS6
z;QMySoAaJQG}Rm^V78G6qt8j@;PV<}_{qw|9ZsN%0_ml@xE|>w;sLDadr4cMVSX|!
zFoh8GlwUF^=vc8^a;{YDqXpxz>lD@d0s=gRFp=%+8b&4y%^48s__#&o0JY}Hiv7Y9
zL#Q)W{<Jn^LDzaDyRXw>4-Z!9InPcpeauGQbu%CH6ZWNu@Nm#MtQsc%BSMrgeHf!Z
zqSi<@)YcHp5W!$`bZ#qZ;>y3%9+>yGZK~EVwy?;2(67#cVM8&anprs6j<u_<^D^0r
zj&Qvw<f>sTtppDDwp);FI`+6)*Qw~ZWLD%pW_++=C$p{S%L4)-%O^dpB};kZHKCw8
z@Ws=nnj+b74$#{#Yzi3l2q#pETYMDx|4cdmgr_t6p{mTsW{0s)Pv_cL9ycM!ipN$X
z8?G2?1~(dYRycUsBdg{6m)H%%(bm!NXZc(AnFze#n!znoOt~SQeEk7njR{JS4?FS-
z=EcpxF^T9Dd{6u&zY<*DL_*n*n#bjRv|!u`Nb$}XTg_#J4_L*JbC!b#uT9ib984P%
zx<u1%FdiSC0V8o5WuSD;-Hf%YoQ|Ts86hOnA%Q|(T)>?mH^%Ehy1QdSZ#6e1LP4En
zB*ReoZe<k9UM{#80p>`NEt52l-yLS<H1e*PI-QkIJQ3xZ%SheL(4)BBN>dc!ENdL4
zJ5#BskQ#8s-y1c;yK6aeZyl>YE-MpnMRh2LMlzg7j%T+|$G)*GjsGA`*}RSZ9?ZSv
znHvloJiP8|?{N9Jy1+fU*{m%;BjAN}@GAjUZ(yBo%K4wM-~?@PToS0*k^W$8*rSm~
zd>cWign76~=4>KEy2r|&jnZwA+1P{yz(;OMySV7^K>zXPv`N4<WlXLXI^575HNvZ^
zMF*QvJMT4{O^7yseco|$zeuz4-Z2^OgD=xr<GDuY`m5j#=cVJ844uYk(&(Tn;X_Oq
zE)&R^6uMCVAgmmLuC?XA@mZ9vw}#P+Mp^zBwex6F+|4m*GpyY0S>1ow^DjC+vl!JD
zVu*M~t;1rdRZSFw7s_(-cWs`C7`*y+NXU9NEH$DQZ^O*R@}56nr;LtsNmtpJ+)WMX
zKLP9H%o$5al+i2>sl|?XlX<_`btmkDNvFCq=gd~yCzrWYwjbc^k+<0ZSUwui{rDc4
z4X`out?dGMcYF!l;b>?C{6jZ%1=rLAR{tM)2=kYLV4w<3A1tkxY^80WdxumZL*+<5
z3JM2}dz586jo@zIIHrC?_jdql&t}i&Ay<?A<R7v5iRa9R-#u{4BJQXL$rDs$0WnTL
zeeES|CG-io{-LNo_AKt4jK7Z>;<XMNM*%&0WH`2pvTadLJ=YE*7l~?K1;yvxF4Ur=
zEQ3n3{yL_46Tt%l#fjE_Wm=-#<K}oK8RQ&9{3_DYV;7jpDka7QsD7?Qd|QqXzr@oC
zV&61<Z${Z$tvKj4oS{@7s9PYbd8hZQlZG}VJSFaIN;R~*H$H=5qbRD+bY@N@0~YPR
zLcAp?=%OWz<vX`L$VbE;&ecMr$2~U5?m+qSnBj_KK_+K&X1BWhlWRpI<Few&wVSyD
z3$m{>@>xGF*@nBUhfUFCv&A4s^x$35^J)%PBuy&ri^0OLf#~19#1nU_pZT%Vs4Kva
zhpThx5`Z`7cU<>K`bMSFm%3HqzYktgH_Mg3QX0Scd(61W=WSLkB1|@z7sS;N$q`XD
z3t%0c_8N6MEH~trO<kkd_%<ii$<%htx*`4T)xK`A{3_4GJKF>mC$i`$ECQs5P1iry
zPnduc1n!hKe8}xFW60$EeUvP@0r^a=*;*(W7yv8ZlSXeqcUzm*0|0Rx@U_;F1V$}Y
z@V~+T1#9@Q`MTb-Y_kuS&3Hn<c~$CItfR9``&M{s=kV9zQWs6FJ<0tqjV5b(@Fn*p
z@v(r$cdyNt0n+Dx{GgBi)#`5pL`s0?S_cY%`l0@RV=B^rQHYt`IEnDyqAOwI&m$<P
zZ<eJ<^R8)W18KmyJ_jSUM3(XGn-X)u6|F-fsUT~Hpypy3Bu=PQ9)_S6uX?p;y}tO7
z?=vm{0jdp@OC=#V3NLoQ1V6Uk%xeB78@cm5&S6(qeWovnCC_4*4OdBle%Q!k&O$*n
z-9?Il7{wPEfhxj<9HhyW!y>>bsYwd3MRQJr6jnQfzfUPlVZAQvqU=&%&uf;z0*06K
zEExCH({_ZNSsX%8rmC<DpJjp(-W|tRN)H+A4~&STe>h5r9*B8cYIK@3-$BXBhWdER
zkicU2folOt*g?>|lOM0`px=U+H7?+cZ^I0yqBahjO}O<|Qi~RkcdV`Ac#5NW0Zl@>
zo4%Y|5x{!#oh7Dg23G^t8){YLBXmFAB81*~H=@%=)~4(S%)<i`N4XR#uS}L^zr>v7
zyC3waZq9h!7NoduM5in|S;S>k^tdFlaXKh2fz~6NN6wR&$TeY`YKuU%wY@%ZSmJFQ
zkb@XLdH>0rS@MJvA)^bLsX`xq{_hmo7*uEu*B3cCUpW@_Us)8#ZPKT>z#`q6Flf?Z
zpGdBpijBhvljXbzzfAF%HkXJ8+6=&k-@{batJa*AQjzN2g0Vht4pPB2pRCE`{n-|X
zO0KxAR4h=)iZd!JdF4UIgyCosgcY0xSQMuJ%9=G=Oh}6R(1Q&`j+Kp*xgx-s`f~p4
z6<Y)fkEymAI?LjDS>&N_M(IQgDW2tpw}3YehP{>c-%6g=D8#<i07-Z0zRn_RjUug1
z-BG`*;do^v_TL^Nn1JJaB<Hj8eXQ)*y|rVhZH03;K|)m!p#3b5R6b4^91DcT0#|xN
z*7*aknb%Gz*8qb#6yE6LB4kf_Bp&%75bXyve1~@Uyce@Ut8#g{yFhpJAk(V&7DbBt
z64r+M?BSDAfgmWIY=gWvTMgh~*y>=zR>m&FV69HVWMpbcw<Ba;dGWQCL*u%jydh}i
zhHRlEVLs;~xOJ?-dU3M(i{Q8WOnCMbf>C5-)N89ng2{O3P)=d$+>{M|*j$U4-{;DE
zQj5)+xy%%Zw6z>of8&^=^ZsN?{ORTG>_1?TtPGMLKA_Yg!6{3@$3?TSyVtRfBu)#P
zkb#l-aC|b;<*Z~PTAm1jc&pQ>$B`aa<`(5Ue%4I7#%NE^)_&Ds39He$JFjtsF#R;(
zVIMvMSbiJ^Gv^Ps+7{}QkuCF`kIw5C1o_u43UN9PubW?V{%a4jEe|%F<F&cNM|uKe
zei{U;N>66;`S)^_ldKBoNzND1_3Fmdy^gCh{7>Ch#c!js%}CG&sUU~Z{|UgB?#{})
zcrG(CvfzyR%HZImYJG$?k5PW-V;T}CB13}w_6iLI3<(NwR4X8(j8mzvi3Lk9H*{)M
z(P0a1H~k{5sZ!%9{UG6Zos~V2MxQHqr7(8bohb&vyefKU9zyhUHGXZSGqEhJ77G*8
z8?2&&47D405G_<51(&ojJ;{~k1wGCP*ue~@mOb6iQ2$v^3Ew;s<Bjn&%!)P--u3J7
zNaL}2jz0Z^He-_n6A)0~cxzYd>fg3<Y}zrX?+mosv7cYJb4=PbXqR){k=0sc`6}Sz
z&fk{{-i7gU*!_bki2gcNOp|-wtWZ$hO>6w8toHLErkY+e{OgL9gRyo?g2gTSJ>@@@
zAizolH)y!n3D3!c>R;LB`TSzJd9#%=y0>U8D^w<nw?%^>-r8;+7Pqy1pu6IEobU31
zMOmONT1@qz7pY!b4RW(m$@(m%(0hGk4zJ8^L+j8ej}%I+Y%M-9A0Bj>;;DGJk*;qe
zl_@S0W<B5kIIyt+*3=P6&a%OU+Qa_6q#Ui-d4)XnH;p=3NE`}L4)5IYgXOpY!Wo>_
zl%F%l6#YNLtX)QM30A-U3hX=I@$vb#>K}xJtG~<IM^n$u%s(}<0yN9>|08|N^4p{g
z<6{+&Xx@=}S&9W&m<k=Gy=wrg>7Ngtyk*34_uMUatxwBBTHe@C++fa3op9kY9sqet
zUua*Lc~~N7VyTqogWe%JkaxU96=Qs=CdCf9&~WreJ3t9~EZ0F;E@51Z9q@~;Cb+0W
zWz7366I4pFUUIXVySPn>EbvT~Bl1Jt9Jz?rR`d)f{Xg2B;fQ1JN0{E0)gyA!y6h_x
z#QsH-#(s*W(TMUUlPw&v6`sqLHqEOZBF3Z2j@ljk`Qirz?E_Pi`R5rt_FeL_!(Q*@
z-zXW=E#|Szz3tLwwg5?@zI_ZKb|6W|2vsaFC87z<k^+kEd+sfG6LnbACFRHbq(ldD
zp#OMN@9&eSYXAq=+9<$#!()E$SSpYvbin@xrw!5s-i2bT7b3sODO*^NF%ttv(_;}D
zD;72~TXK6y*6HT)v{hJ3fnX}qI^KjbFB7*)lLf(r*dFL7_nDUymC*F@2`2o7sH@2q
zmSJDw<2!_N7IM#eg$$HY^u`2(ImhjBl@&MOP9d5*noMy9P;Xz?{U%^jZ9{~j`V9zv
z0D#-*N`a)3DgJMi1`A?H+)?5e4kLhGI0ik91YTDTf9DMees+TMV3fR&XQLsBpg`Cf
z$~3~S_H7vBR-c?0o(yx)>^@RQ7tpph1_k`=EEbacrkP|KCNH_noZJE|l0deYV>%!Q
zyosc9#mC5Ch06F4#tw7}#IjXWeo!1g84hyvgGAsZ7uZNvT9w{p>a*!`m3_dS%MY;a
zETHhwl{t^@gJt#fOT7|)bbfK-t)9y{9+MU=-yKedr=;-#dbQmzD>0j+8qQgRhslM3
z>-Kt4j-6P~yslI~S!&;;3ShLJ!R6Wz%iA-Y>Nr=#XEv8f5r=qrET}olNrR9F=Xh=u
zDZtXxXg{nt@w0f2ANZYThI!!YB$yzO7otxig|@g#QXf>`zh);I9>lhn)5gfFI-_33
zzBX(j0K}o1=H}7;kEi|YQ9AH55Kee~8&1yqWR^ATfmK;Ul+}%msi*)DL5RsOA1tk$
zM3MRaOO^!;`Bxdw3P%uFYv_-=t1$8CW)Cn`3q2$h-fM`(jf_xYQv}X<q8jcYSi9UH
zP0C);(Dg?hV5$I;e*4O|fa*$V5X$1tl6N4^n)@HzR5B#&I+_<1u;xL1RI%X>ngwXX
z$zY3h==aBSB@FP)O%n1S6o7TOX=Lyj6!H7G00_)z^pEz)lOO`uT0qXBT16c{qB_RE
zOlS5CpmZPxwui8dzi`L?(^xYivqmE`66<$Yo*pezf0@xK$$@0INSi6Jh#|U6LLO(e
zPrY$4Ipr>?knocTj!Jf;U-!mmSo8&OK5@_CHNwMPM=&zZ{_quuy*FyVxeYp1D4#U*
zH6)Dq{w%&sdy%rta4-L`N7Wfdlas*`tCwQ+fm3+9g@YjXiyjPDMI+7yI;*s3W=Fx<
z<a>R*NW#-9I;)(?ooX##%g^hnv~rZyS8j&yvbCneQx05iCXXJ5PKyPi(5ShkHqod$
zxn6sAom3syMX>mLv3#D+w*!TuYRG=ri@)3&370E%deMVVm+E&hISRfu_095=s6ZIJ
zz{w6SGjm!iSVUH?kamO1dp#^>V9z`1Mnrb1B*h0do9yAssbOp-9OcA0=<7DG|J`4Z
zlaG~Xr^oseceYAYhT^IorG{VVWR=@x5>6F>DDqkg6`R@W<^;8nur8gDmW$0ETnc86
zJH_i|%Dhdv0of0E>QU>&SFLfc$+X&~z*^Zi7ZH9@p)qmk^%-hg;eQH+1-x`sv8Mlq
zYHk2jxLc)$LV4t}Htx+sQ}vJ}l&-yL%N=nIGk?M}iarr*<_car+}oAU0yanuGH4N1
zWcWwLDg$cOrt7({uVN1?&s!}lVrvJm8L4M)49q<XaJZT5vH^59s-O6K415{P<7m9~
z{p)pbz0=wGQvbj5D>4c2A$&Bn&e+38kG%tpDCG?l+>JZDgfu)K*4xNP=eqQX9!g)R
zqwjbX71JJrbgj>Mq8Uj}x<!UoFj4S|wyxuqdm&@DF3VSVr_nlMCAIbUuiW5N^6r+T
zd|?-P<2ZVXoNNV1O#@&P3lux*03tK6hy<vueH`^#jngapk-Pyzv&M9VafsE#Qiqc`
zf~~MoNaMvd9`0bxbyNKza~|TL^PuAFf&}e;$A?go*BKMIJc=>#v;J|Ryy|W9mWTpg
zh^r9wwDsV%9p!>Q^xJ*7gTd^if4sWxyS=mRwIc~Yp!!=K(kFfX|HG2K|1eMMqnW=@
zeTp+=ksdD?95<{tnJ_0opy!6Br56^Tu;A~sDc%EBJjIihXrwl4x_>Y$t&Z~vvmaR%
zW{S(?C7808AoI2ioVgR5<c!}AC2+(}vpkt>4PCZ%6I2mtrvsi?C<y~bd)EdanAx?
z=U)v?r)0rD$Ag3nBSyQ~G2)4dRPiJA%^s55vbmZFM5-e*<xvh;gXU1FgH4y(^GLNb
zeXI&pq)0<ktl_MZ0l_H2fs31BtVl`Lv}Dd~xP$$sQ^|`Vhs!vcaR>5H0uss9OmTr^
z7W}wIDp&Ws%60GxVy~Rd^AuF5kV}b_R$5MqR&GR&m#Lk}1SztArr!w4@h#Vto!ZD2
zkf&l2tVz{EE}#VU7w7Vw_LFKI$+sz%{g^%5Qoh|)#^;kB_db)yH@@%GzURYJ0o3F3
z(ik<SgZ~fn;}6T(pN!s+P|H!tW1+VvIE>?K%c$dk<_@S4(0CB@$CrGz$WyxgSs!uN
zvcL@nkX+t|MZ*Mb4J#u11u8>m_fLCxQZaH9@2GnO1t~R>6P~#UJvM`i&m4PX1=3^X
zd%=L^F|@oYnh}Lmz@X%jQ*!oz@CTx1dFy}Ab(_+LKfmp_f<-LNMSapQ_5Vi5^P8~~
zgsk}rLl<(2S(MobPp2}gfpNaUK-byI=$hSo`Nmywb3VW<e57KVM55R<=Q&A82o;X_
z>)Z6*Eu)9f50Z?n+zn<6mh#<IWT86lJy*qtISw+GyldiSIPRtAX#DgEaV41*pOrh;
zcRyq-ph~9`oR{>WXAf2>rFV{M6<==Yq53aW1ly9Y5~cXhHrMXnlaWYUiu=utaqV>V
z!;3;T&E6)R_w>*Px)yvluw9@((VD^}v+{3Hv71Pvzqn@AQE%@4)~v(TV}Q85A*19j
z%Ly7y2LEW)JbCchGgm~ud)KTz-+NuFzbYd4jXmtIuj_(Y)%Nzr#bKfgb`gZIB6!M-
zBP|g#zYYWt3Q}8ymX8^D0TgmF$Mc*IA^?PKwD=|$YSSgqc1jCsBv%jd2p}P0cteCu
zuNQ0Vv?a#rgGd)GnRRhB${c?cC-Q3@w}ZN)5eJkm6H<7Hst!RqCU0(PLvxC;t0eOB
zU#}wd67JX3n<tG^T7^9=yJziH0(FfNrU#c$2E8CvytuLV3;SD4!SUM+BUdmfa#xuI
zGshLAH4SvXE1+wx+0FNFT%^$`$bt{wwTj;7;;|-e3HYbbcKNu9mhQERIF0!+#^+H+
z<8^L8Y2)($L;>!kvI~yRzE`)OJ&#AT$LF1ve}gk#38m4>YhgwonrBvA;uS2d_nSo>
zXN05wt3fkfXU8OO>ejJx7mvW%0YL@>Ggp9i`QsE>0p2f-s;}Q)a`d1XZPlfVcSW9x
zjA#8QGjLvcv^!k4N2iQs7Lzyezj|hGlWz_oFzs@RexvqI8O*$=V)UQlS{6J`S}E{{
zeV)@Vjnc1S{yzrcoCXV}CcyK}q5v}s56x(o?46uGX_Rqioc|dKvj`t=$>F=yXgTSO
z)Ekd5);n#pJOei%9Rr4c8cl%TGzl`_Y~pJULFp|wml@0<U;fWK5*XFly=c&T*~W%d
zyBTHbP*N=>Pq*Fm*yc(R!zSeHQGVgMml32((nl}5UHg#y`4aWanRe%6#*m-2MNG=g
zvR6@aslruoa<Q&!5O#_^D-Z%UHf2RbPOYKRp*xY#DI=xzV1SfX(cP04y|k{9T*w2?
zV`Z|@wn#;kg?#ZM&vd@@fvUuNJ9Hr#WQWztMet$6w<14gifig=@QE~;CMm2Po`HBp
zI@wj%UpV1+bSo80h=^vf9~8C=8?rXA1bWb|N7GKIuPQ3EH)z8wNXP5oW&YxBHq|}D
zOg|THE`jknFHlD7fXBxeYSV7=Sma*!%!)Vgkc6}p^IJy+nad`&`dB406o#;xzny(i
zk7Wb&o1sYoeOd3v?LyT4eM7kclIklE7&jG3TL@N@4nwgP*9IcP3mAsDDI^wJj(PQh
z6~d{e1cseyO*vhQt#u8+Ta?BWqZ5lV<fGENBMSsHVXY};qZFRS-iakg#9{2FC;jil
zXeJ<WjVfc26qOMeNH#%4Qi7|al?uQrZ~EoRX)@5o1hlov+qvkD_1Cfay7tny#@9{k
z7cA!pIei%?)(=|u@q->V-l3Q~IlrTs<cedtC@=PWsZf&~ng08gE0r?%By@7Xs&5}m
z@>YMb5g{CL3e~sR&#-V^x2D5SsF4roiZ(fPpS}evc2z3v3FutEG&{gQS({k8Z+KAD
zJ{qan=6a4QH`6Rq<FX-P0k%AvlVRtj8e7sLrsI{_sK}q(6KlGWD(N>C8Ik*p#xvb3
zAyjhW^D%)x=gBe_yF9p-N^d+oZ1cV*NY|m1dZ@GinwvW;5$B<3c;MHK+0PF;MAMb=
zsw{QYHJ@i1N1G2sBR}ijVl!~KQuDT=v~#*j@xWV@j47o#ZHwuRp8Va$ZSQcATV`)E
z)g-}bO`lVJpVAXM++%9?xOR7k=*z<US`2~araDI%l?{8mF#}V{Vpw3iX;xu@$2K>o
zR1{{q&7Q-s$+)Z7T%K!nMTMC1tJ+1uOp91^r)5tVSTy$d%(GF6VuqPu;j{V+t~OYD
zt|4*wNH*iMY)p-JDM(Yh0^&i{$U^@_ET%&`ki7^EYQ#6u;}S>MLQ!@x(t^di``#lx
zVQ-ZuS`XUJ@jH^a&<?tx63I`N6EKjNUQqNB+-&k*u?uado+U78B#lkaReY~DO;(bd
z7x~;ZA!dl;Tu2OpeBUJoQ87~EyxJ&A(;od49(o~A=apnw5pfAnux~)WDkOKB4QUw|
z_-4U|bw;!wxaOKPWWlD4x&S-O!rlR0h4aO^M?pdAsqPeKW%nT)$tB|Bo^9aAF}L@;
z$it2-E!_m_YIEt|E%^<d98=TS{8Hny>ha;qCP`@<e>ewh|LtwKj9v()a_=)_U-m)D
zr8vrXH&<<n#Q0~>T+VkW^L8k=`9d|nIDr$N9~c+7tU&QVeiwHULIV!G&lJUgsi{G
zw2gbC6$gcFiBVeZiER08*d0Hzkut{)yHzCkksd#=<RvJX;#>>iNY#)0P#2t;a?LWn
zlWUZW&1Aa!309~_VW1CnU?}_QJSL_I-R22cMhxAEN6PYAgo4k0fV0}REW8Zl>Rkw|
zq~RdY`wB7yC5F;127L@nl>r4It&_uf0B6iwxRW?Y%|V5hpwBFHDi*Lq7IGhd@3iAJ
zl}BYO>a7shwk1ts3;01c9X9O<@G1q!A0k^tEa)o8RvFzU=W2nE=TsT^p-+$a6g*vW
z=uX9VK^9HNdSJ!dOeC&!y5+8;7FTCd@uKE$!HjjH35X_UDd|1kd0;Krh49iE%Ettq
zApSa)hvL@fNZn6OEnhdY%{RwhH@{pr!y7{6u!3q;+2v^Ax>-fz-lM#0MfBkR)nh22
ztK}(5NW;c_0FGLRRGiAOq1w@5iw{ZngpdP4lW~o=z8ozY$q|o7V;3GXOL!Uh_pv`|
zGgPhwL}CCPO`Q_$SA13IUj6i9NU-VI&hJp<kyX^Pj#C6|Y=C22$_8$@NC$&v5>D$u
zi59pFOdI5K#nV#T3AH!X0c0(5l)9nPg(PDnA<=MSw_RS%7Wrkj&egz#Tc0J+`%&2G
zztp4*x{dn^dIiHLS08?KFM<>+K1r!#o;#I*pFoWCY$53EoxH=4^|W$W6J6PgaJ!#B
zIJ78y&#JkurRkGFmB;TJHDoyGAqggcC3;sRly=NpCj7kAR`e;GJ}gb2c+&60TqbFU
zMMZm!My16kpH)1pM3=sFVk&)h>B5SgQ?`}pPk^tGRlb+#|L29b$^+UrzMGvOQ5?#N
zxX>#P-6|pbKWmaR|JUq{1TRnGypt&k4bt4kux{u<Y*@)Hh5-b|EQuVHm1cZJVlhA#
z`J%LtfNIqfs$QU*$?hD>H)7e5AbqpA4v-PEFmI^w+F$le#LOnGg%5=@WW}Xr4&=l5
zGF~PG{ur_)r%>39CMa_VVJIXTDNUPr!B-zbzuSS^idpZ>!BwRGBCMdNr_J4>`YltF
zdy`Gmqew-R>N?lBDMF)o@wf#?17?cG+2Gu#5TW$l31<>x`>*&$&T4KVSag<nSUN1&
zrhop5QkWPbbQ6G?s>{{_)cdBU!-4@8FX9sR>y&*`y}gXY=j@#(QTIZ>|LF(fpKkq}
zbHd(}+RP0nzi3t0?wC!r#?^TkV0uf93p-QxD6S`rdz!2$O-MetVJdnFZ+1=Za^;vM
z|L9W;%<`~QffrV=SpRJLc8+QL{1<iLk95`D7lL;2;`K&V{V?x)5l=!mg%;jN1`m`c
zQ@Jz9?5<jRMf<3_tQJiQW)p*ngfI<S^;Gvxv36xmuRFKYWt-Y=a}o`xbXnSUcf3OC
zx~(ouGknb1ZBj^z`B`}Qs(Vc>Ut_1&V4|+@{GzK34<m5jVLAQAVnuR(Ad)K>83Cvy
za8DqCD<Q(XHS7zxLX<40n?%Z_I>tEUtYd5;EqHYv_Z>q**NWHR(~aTD@F&b!m#?@$
zA6-8F%CfbcRV0r7jMYXuMT#-{*pJ!>d`)((nc17(!FC_6y7}HkE;^kDdF$LA!R+Qi
zD{g?u^J$);`se=J9`*uDm5LwZC)^b0*uPVQP*7GuVB-Kw+!@IDveSRFa~Sb9VbFs>
zOk1oth;in~;L!qJmd8HNP+598Mx`u>BYhffq|TixYmyXLhY-?F>ZUE4wlPK3LhI33
zDi}C@bad#U4)n$ju*w(U-@Y&?&mC!Y%0)!2U2U2x6GWY2q&C|Ty0bxUjQB-(EC2Um
zu<57v;;HYuRZ=cu@@-<@8Yp9TC?M~VT~AvBWvm+g@!<Dcg?12wPdrWVOm%p6)KDol
z60(wc&uF%ad*zblnj!un&Bj-yz-Y?2a-U%Wzg14ul9`{^JCGPs8F4zX-y%vf%C~B8
zdv-6N<~R}U&I5rwy?oI!{`&maW4MDw%rKA*>wcVpv`xcQLP>P$up8pA{e1}7z1o)r
zSchBs=6KknlmKx<T|6QcEg7qrhi4`0b%r&w1`f1@!ys8Z#aqFljS3^Feaq30I~ZYS
z5eZt3+4M0<gtw=|wsS0f&<O1JT&c~uMYIpo^jYHSjrZM6ot}i+8O@J&RXO<<n=w?E
zBb4?+m#dL~Rr0}_zs^X^go|<3)+5x2%o=tu$oHW7u!BjTSx%qa^voH0V^jt{N@dzw
zMj=$H;}6~iY5WFStJvih+}X!gj`v~cAp4MJ3Y&kV|ISniEh6yal#2^gu*Af7O<fII
zvb@QXFl6iOhf7IByt95=Fr701_$Dv6wuTmBJ|A+=w4OQAb3sf)eQxf%nsWb?w(;Me
z_EjRL6Aqu4KV#5G3U-TQ2qlqaLs!6NBdIl7C@$ysU%0wBg#)!92?`o|JEwmK%eK!S
z%EF`9XdxN0nh5B2Ed%dJu>&SugEOnN(v(2KanqnNIMphNb-tXoDoi#qY#eZr4HOUC
zO9iQx`le$*j*J~BD?>H7@m9_`v)pgM)Hr-;sop~mBroJq?Zt)7*6OR;&b7_VeCwO+
ziKvl<1f-Ro*q<yzMo}@k4<e3-yu(CmV0@@NEmVCj4KZEv-mfD`sRHd67UixU_+C)j
z%EZ>0*{jD=-mluFSI(%O*Bs7P85C{WA%o9(@mM9!P-7a3DkIUL(QisU9ntcpr*|t`
z2nj2&=St7s-18fZ_3k}#kjIS;w(sl$;{P6ZaZ=+Lc{8uF*e|rS+8UhUc{N&mN}5ZB
zo(O9NNu#;sLI?S|Lz*O@4H1CZD2&$g6e6uhGouYz{S)Hy9{8>8(&+iO!}SQtMGp&h
zwt{$g?k5$NM;UJKV=cYk1y@dfE5Q5b-h+e&$It_BE%qF=#V!X4l*@bX@ReC-&lDhi
z?c<=~IiK9MkFi5HXXk>6i^rpA9()CXPsS{Mwx^hT$X7`*V(=ptdFci6!{~PFuCToA
zW4^WFxYUnU;R8wog84&Vs_4=m28vB(f+lenauU!sEL?R0WpFS0F;GbuWMEMp1jfM~
z*n9yKp<b3rv`G>2p!oUeD9p;LMrdx^J7bN~)^eZZOX&+w_Z;EDWnN!|ZbM8ngAQfK
zne2RuP6M$rjLX>?mlmm2{^PGRH75INfbJ?gYyOWRMUkO`iAle{j)n{ce_nUaqdwm?
zyKPlygR9UFgp0DNg>#rExD&5d)aoZ_9Zs0VdX{>W`)U4CMC-1$&hArh+-<RMs38R!
z5^Mb=tH!qN8CrtFb7@OsXnV#{V#_3s6Hh_;(QJDt#|bscS#XTUw_9%Q9L`E1FZ5P#
z8DlQ8!ijlka0VNJi@8O}bkpLn9hnR2BASHN>2QLK^+d35b5LUh!$Pwh`NKK#I0{^K
zMq*Go=9I3sBEbk|8IGy>I-F^cH2fk>alMQjb$z5NH7<G4LVVIzIusbVnvd-$?-4g)
z4^#pGq%-x*TM}E435q?IzajX_Hc|y9N%DNwc?Wj(`C-o+m8xaeGjDXn^%1vznkvEs
zYol%@8nrmLbgXQjm!f6#NgSj+lGOQUD!$BD5v+HSA@C-hzH6PSlTf}D#i!))ylwn(
z4ph#HfuWfHa}uFW%+&f;5{vh0@Q}Zp^mK#S7tu7x$Zxb;u*bQ@Y642xVnP|Dok}d~
zluWPd3bf}I@2g(@cpB%VT`;2A(^wHmvlFu66SUj7=6JuV%PE^rchFb*uOzW@_Up8+
zaqw>-S(h^KF^WMoh>MWg!0j>TBM=vwK3>LHX$|Rkgcr3{{dqQwE=g7%yb5JT{RkTH
zgHfVcaYBP!*g-cVK|y%+6>O3L6)CIdju_|WXqmvs1ageN%6_+Kzu>Ho#+_258zPdL
zsq^OQjQhaG0s?3LK&G_mpLTi<c#Z6)$T9f6oF=8<_pbgF6>#y|JkQ~Q>*ak@H*l*_
zwW-fNCh>O<7KyxZzmf&kC<~)czs>tV5(dN6178S2vz)+dtOTtP`ZF3xJ}BF2P~^Rn
zrXwPVmWJdfSo4v|_bJW&Zg)61ipmA7$D5oaxD3aG9JT?-nAvwME8e^^<&qu_RW8Kr
zkDHnAZdXgig%^cs<Gr-FoykC!El+0ytdd?C=Y@%9E>xd4*1e;e?j~WD>C9HFHStBW
zNBT!EjQKbhtggLh`d^cS?eg*b%*OEG3^b&|FF{IsR>VU3;db{>`HbxYmEz@OJi5Rv
zq((4*pzADquk@oG`<f<A!i-i~*&wIwwZ-^u#70YJju$pELYA#3tI&?j(2r2#YO{mk
zlh2k}h7}e9m31ieah;Z}PO}teIOO$|7h&VK;v8J9VOQfaVo2C_-1?oZ!$dA$TF?co
zUum~@Pl&HK7aLZj&Ew-4IA6M#;LHYMh+K?7BlO%faOA48HNkT(#|tjTd5v{A=HSZ0
z&$MuetL+rZF+CM@(<+@Ed=4;iCI5`O-iWYOrcpW)tpv0P;!IRxwAYeX=pb^NG7%$n
z;--@_YSWvbh4OB&*dlZ%xM$g8gn35_k&DT2Q@da~Oud~Wmsy|Po$9&V+K$nVwkI-G
zlIe_-3Rx_>E1xnnsbA1d?|*<s<qQ9zFDp4zuvsg`8n|AAQMVF1Z~PG5JrOBor$=p+
zukP{e>`55`Y4759m?;M%a<ER5DE4c+x2oF$|Mtsnw&;RH{?4&+!7M3;Kz^GN%v}Jf
zgY}FU`%_t&sQJILJS!E8!(nv0JFTE$bcnFz-dloO0ubEfq!Z4vx-!~Wmqd=C-ok=T
zs}cyuOhcy@OmS+Okg1()$1hY%Rv^Q3DlCq_LP#xF;iF=G&z;7eHkj7PxJLa3!`fW3
zw1Q5?(^v?6N8XsVCBumO^OR)r@ss6rO=pu@x@{$vp_9OOdTrV}J)yR0%BB<On10zb
z6yzyrM&`({w_<SX=uAKi_pI#b?2GRtz-63xQ{V8CK{Ir>rWZ<sV2fNjA=KG1Kgbnf
zQch|p#f-y8!8inat;s;85F;_7GBR=4{9#v3C)if6f@l-zt<ar+N`|Rh=$F?2GRrHf
zUW5h@wlZTl85bG6js|Ve?c<$^6-vXy67brW*QVdEl%ZPV+U&RV3l@iE$+}cDx3ErB
zRD^}@ga;eVdbqG>lwh(cbkWG}B}L&rBZ_~wG%Gn}=61HBi}0!?3pk8e9sy}R3B~8=
zP}LHAy8}P51#kiPu~c1=k#B?*(tZ|O^U&!c`OAx=rqX@qvX66l10hnoqo#DBf8Cg~
zzc?+q^&o%pf8O5u=%tALGn%M>TFp{fjwzAv8fiM4@7l>t;x-z@9vOx+2y7mAcv?Ro
zf!Ai-<KE7G`?~?93`Q4F>WyJL1F?1}1*R(9CCHD?ni_&G&K0LIgA&C5VZWUdWiL*O
zygB3AOs8xM0oz&V3SO&bGn$<|aIx%UvPf!*b)U|AKkb2e!ePR5-b0Wz13IdD&~ee(
zO-YzK1Aie>nN4M72lqt7+>G_+!?O<c`w%ZH^J*RS=NaAQFPzLPoI*|I=TcI&0e_CH
z{dFX+jT=1g#01bX;~#@H9U_|f7I5_b@sMZ9<oTCUKOchlMIa^|U<>hX{d?E)MNeKl
zm{LZ5&iK8+d#NXTSS*?93PL{EBVfj`taEc^3L8;I%gR?>QC*PTLWxN@*=udB8l8TP
z+&!CU=6}FvW%yS}l6``VSL)a4d$gSa<tE)@fd*dvBmKr`qHsoM9(hWg7K1R>YVYBw
zZR68NxO38Vsn7()$xLT?Wa9<<)SmILwFAmMh!10^ns<l+f@s$y1P}n!KG^;lFy2Vm
zqwE?}W*4ydHk)c1niAt+r4A;GZAprnj)_2K)C4v{q&k7pPYHoNMhsfe@Bc~-L=P~m
zj)i=<Sh6;0ZEVXSPgY{k<|mX-dl-G(c(=|(XD>7JI1&iRV`*sw@sB5TSU5vHL;Ypf
z$gBCab9MDKakleyetJ0SudS)It+D&1dl!)_!HW6(4*otEd~i)@9(%W@=<FgGJ;OXI
zXlsrrdghj@{GWV4zMMe@mYPJ&mwxy&Qhz`zZ=`H-64k#?2BnCQ>g1;$mz5;5-0Mtw
z#<VTgMX9saVd|2hD`~`)1jNVo(PCu-*n?LZauS@qecWOV_pzVF$w+V|il<fcoYn&-
zev$B`*nAj-J&9$tf)n)<UId1)G2xtPB8<B#1v$V~<7+0_!3MZHVSMmc_D4eV;zgOK
z#m;YC8KK6ztatBUD8C-fpGi7VrmznxC|o!!`QnfY{@$B(f9)Jdaaa<3<9)C5^4k-*
z`0tXe{ZVC+xEled+Dm#qc*hQ`;phvyB@NN2J<7c+kbV;L?%(;ruIGB)+zD1MN4>@;
zfSld0@Z7e$%em%=4=zqVDjt@vMpjhj=(_&tvW%LyOchT|{1*%P%TiO*fzu`UR;siT
zy-#!vSpnVOmDlG5MMg#eeNY6xOp#y%8~<#?Tqna&PS#zgsmEZ)XNl+?>($T+Q#y2@
zvoCJxZ}?c&`wq8%2cKlBil|R^ffe0v4KX;id3iatbon`Djg;e(<zzbW)^7%|-60+1
zNo0m(D5?uTD1DdeJ;P-bJcHheEQdN{_L=sx>OFo$`&*)*p*3g~u?f}Hzj|-P*M*7N
z=ZAT&u=qOxT1$9@G;JG^H+Ay%OM)$`ONfgoM&?6;C-+z=0*)yd;WB5vDke1r4D>m1
zt5`iIOSBf*4N9zMbniQ-2x$YB{JTmo;~YGttL*O>0i8uW_q~??yzmW2EJj$gYZ_CQ
z?J8LM^mm2O&{T-&I1npe3QI9<CBbS4+ap&^2>OWFkHZpC_-OyPgS|_wj9*m96jv13
z3lq9UFWnR7t0y_JCN!88njU)iZ?Y)yUvExOWI7$Ug}F^H4E4Aqzp$}{g(0JJV5una
z4}37)bD^0}eJofzan%7LymLc>-BzoTAT%9@t8WrfhDpbfbICjOxqaGYR;j2eaS|XR
z$KpwxL?&EUaK_i17sns4IG7#q_efuV#An^!T38-6>kMSEwpbnJqAe3;mdqbPS%bXn
zZWwQe2M7?&VhEHAhz4;TXJ<k2MN>k=Y4IG^*F)f%D9<?iPBFL_mD5~3$G8=fhVcb-
zAlK4+`OoS{;(#fxYc^OPB8loX`3cORCk9zVkm|SpmoMbtzw?CvV8c^m<}mZ_bYl$F
z^kv0;*IDM%hR;~PcZovqQ%+C<XwF__8{=2A_Y?=yYn6r@pT{*^DZE!qYeGlS@cXS*
zCB*~Ljq5xo2Vl&X+*7;g-CeUhaj0|_OVl>_YK!OGM9#+)%3np%>LF<Y7KM>jc!i<6
z<b`dW+{O$FE`#B4H%r+Y`TosIBYa&=_`4;%Xqok2Jnmg?CjWEQ`PK1l`u(rFZpGiH
z8pr?nh5Y{^HQ@*DYD@>p_f*DqYGUcsfAV6~Ir`p4C4*I@CwV3#y?bs>Aw&96%IdI1
z4i(!jCJAxrSo;n}-nJ=$nV|SS%8ZVSy%%&N<&H>8xb*`oK@|=en=nCf3jJDEN$0Xt
zHa9@Qw9K6;9C)Y!&N2M3kY@VOB-LFFO5gX%h?f@y80y~27A)nYpzeM_%HH&?z3x-%
zjpFfXJEPf3l6(V(jUJ^=7M0TRjn$<J!Q^p-CvjO^-mpL-p6R=_ypW*(C#PIH%X$IP
ziX&0T&54#kqUi5wYc2vcCKSclWbi>rE0c0^WDDP4%Gb^mGsB}9r+Zxrl*!Z5<0Uo%
zdA@FOGq~&C23`z_BNTjTM-0)e*2Z!~uECWs<Lv1SYf1SJPUsVD#{^||Zn5DE@x<<A
zn_YeJokHa5iMGiKz3e-$!-$!)kfjD@hj&}Kz=GRzpT^z&Vd_Ed_-Q<n+rq9y<S86K
zqZ}iJZute{f7h^rYwd5VRSc_*);Ot(oV{%VL$RCr1lXOykll@Qc8S_+%5mm3k74G5
zMrHs>GHJ0x;%YGR-_AKj5e<{<irxXT@ogBAF><E>xG)oBb{La;v%}V(|HITd#R>y0
z?Rnd_ZQHhO+qP}nw(b3G+qP}nJ?C%JHknM`%SqP7AT!UBd<9P;cMjGFc5xiZ3)Hql
z++n&&^tM@Vmv*`G%H9@4zqKqb2<teOLxf2bAJRqP(sRK1<v768-~w!F<6_Q$$E5{q
zA4m`VB3S$UD0T);fQ3_CcEHmSRglS3$Z2F!8Z+ougv5nI(J)K@@rmb~Ki>+{!i`Iz
z=ouBG&^;}MoqL@n#Q8w1aEc()95>TM7=F@G@U%DyP8|}?O0Mtw`}LlANtiAbMWZGM
z{64}@;-0zrmn*Q)hV)EM*e(kios`&(z`sf1xrCj0@#%k;$-BhbkMi?(dAf-__G|6x
zYFfJZ$Di}d?;l%j9dL<N&;?de2UzugWJkR<KuDV7t0Qec&52eXpoV<z@tX<AsVS-r
zH^URC<u>!KQ0)+}!7RSLjprU7yT3?$9(;}h=abgdtjZ;}e`U|>uisUM)U#Rz?jzvh
zg7I+j)P3+I%P`jCgab`pgm?%RhIiA)ks4V6xZCSn!IG_;_WeZdtfYNuHEmxy)-v+$
zrc>kmq_hfl6SuM-E3VQmK)?ao&>M~k9`~(!E9DjU7uJxqH#$5snmtcdgMPR267!Jj
zWig@00@r(wN+bN<z+YiC9w2i1^M7v}&`XEbei2lUDV|q`AMLXv-oEYvy{4^uGi>cT
zT8<$hoK76qSMrKj*WBGKKRH6E>Etq|`RdK@KiG;oz{3L1(%!N+3D>!7mVL(Bfiz!_
zo&_TyFE)w^_Ox<*D_WZ}1pRY}UQPZLoXqJNM)W}4{06u6F7y=o_1c{ZzRCE@-I=@m
zew`l^z>fKqQp_imgMJRxG$pJ=g}AHL=`4Fp*MU8IAVbd($e%44;2}#WyxpuQVc5}?
z)n+S`2yZsZ&6Xj-x2g+GDvpmfB{%yxYswS9+0cD$EYM(<LV5rh&=H%IQ4Sw8pquuX
z8fvZUy9FfHIPsc!6V(k(cMl3<wq!ArvmYH_r@yOj7j}e2)9E6lHumND1OoR21CL@%
znQ40W+1q39tXh6IbPLiZlgcYih;I$_2MNdL+hd2U0Hl%Du<7pXvQ=6|%2m)iA+^Pj
z_-#N_V6vT{+%bUUE>r0*zGQW9*s5oM3!SDIfnCms=KR?unCvkwsUoZ9^FsG(f}BQ5
z+s|Y+cPOD?jB-Hm0E^f^HwCi{nsMl^fJ61R&88H-Xy8!#Gti%0eC|!Fu1}NgD}Vdr
z+t+n}?xnWA#7g~rIehB>VX1fRNxf-b_)+iJmwwf{^rhau#qa-4(fs{T_%ZVgvYhEl
zlST{aI&5!p^6ZjsSlndOmM#n8*0ujrLY{KMriS3K&T&;PKa@XIs&Pdb+>XXKxqP%)
zY1Z1We~9=`rcq`=hTM68M?G`>c5l61l@3Fkt5MXM+{!bPh-!)QcLDa1dYK;e^%nQ%
z>s|PL31$lxANZ`-L2JdNu9(>u-KOgH)39He4_dZ@c*AcgGdGVOcse7XvZv%gcj$eD
ziu`V(VZ;7^D~&L>KduZCyJth^3LfWRO_6IJP*na7TJNsPV@aY=H_W%2myLcECc_(*
z+@R+oJKp+0B6wh|rvE9IsbG;4YuveZr!>+<^klB;_#^NPy1wb-djE%R<Gj!z@T>3n
z@AT83AIz^S=@0+#Zo_i1&ySD&?fUWq2l6IqX#j-Pa60YUrBOyx5<7z%d&D6w3nwS2
z11W@8>T&eId&1H(E@!HRSh`Axs_%bpj&1Qm_2Y^DHH3(9(87ncf)hS$Jf@!OVJC-&
z;l%1dcv0;$yL{{76fp?tBsO`DP?;OTzwZtaLg@@yz<H5A1x6^b_G*q@6A$yz5OJ)}
zkLMJ&*4zrIo7`fO0yDF9p-yUVK3{bVC!>&t-1?c<iH$Jm4|@<8H_%ssrFqiw760Jt
zhpA7wH?ijNY)tR`b?7?$@o#8&f19A@)!WtB)%?+}*S!9nHT-5o7K3v5`zEjBn_l~`
zsjKOJ{=GYz{(S{}>U&30_t=-d)js#8?zS&}sd?>5-C1AxCK5ZL^d&p;SnNYuo!c1J
zF#@B{BXD$W)@O<5Odydt_BlTanKyRzIH^^rYKT|QI&wdQx*33PVgpaeYjS#nR!4`y
zG7X@$xM%sSGwCbZ!Dn>8BaJdO=}GpD86v?8Mnoqge$bfdmovQdG~Vr)a(GQKGH$ac
z7y6ic2^hVJBg+g~97`3sL?gmsETl<L)bQdtr~jjD)r`o>%HlolVxH}?cW`5Yb@y;J
zS9qCLK)Zp!DzbbwPkpM}t&B5><LCXj>{*~ziQ0fJm~<Gc2+#vs;c|<_N|z?s9lssq
z?tvJYI6GDtu2Y`^)QBnZ&p|nU^BZx66<5xrlbw^wY@;{gm=NoUw&pd=RI^MhoYB=J
z+NxsRYMVebOUIHeDjyGk{PaleuM9Q8mt=Qx?}^6X$OLTA0xlZsQa=hQ6v21!BT0^O
zV7Mc-!mjPovB~&HPQ)*g%Ug{ia%eaUMxQy<Jh47Gec(Yhb`dC+c%6a!?P%U@u`Qra
zZdn2z^pAxBG{m;!1tUnKc<|Q|SO1LV_OvEE6l^|li6W_+A%pxzUL?}uY{6aY%AyS;
zMiV>kO!@q^Rm~@|#Fr9(eYgney&=Bb0}Fae3Jmx-edt_6U{5&R0|u|&nisUbVyM@*
zpnZS&pc=&22p}Y7Ui}auKvsFEb1E+%f+_K)TUMYZC1l~^_*;iW=0yQ4w|8t}uGE<`
z7twRN#F@$FOqS{6>kcaQzFPqP-7LxcODu3JLDTotx}Kbxy1KltrpBh$UN6ft?O3H`
z!QYnNW!B+c|4|TsQq+QpCroeb&GjmSknUh+!0^kU-gTRg=ayy`Mg~t3NU6MaZ}Van
zN-WP9<XH9T{cH8MhTwP`ihvv(lVKL8nzC01X4?SSZo$)-S8c^>y%dG>t(P^+n-~9`
zFrvpm+!8^QB`It5JSZ8FeS-@qgIgKThc5qca+GVvCWhF4z5biOac?&?SkeUCTR`C#
zC1~ELccY!x*KJgFY;}ub?56v?bjXZ@Z2!J1O86(?bFY{IZ%5@t`}EiT=wURf_;1JO
z-)qO>AD_v$dE;>EWxLXkTGgJ^i*}_S^@@GzXHVp1JMBuH%?OY&RB-#k3zB1}1KlVw
zq88`iSPMk(+W~7G=+lwwkQ!LEj+_7=colvU)YC1?sN43j$MFiZFuE)%?Q1|csSwIo
zaTc8>`z|+@JkDiX2wC34eH-b&e+^B>jLJ!+7|p3lJXG^Ee)ziPtVl&K-)hH5vew~Z
zkhR|dC(SqON10Vy^F7dbnXdR^|0oCk@?GBY_S3Pa07kz5Hb*Z<k=K5WeSLcl|M27f
z+S59c0I4twk^t?<fK`YDNP$#{`t>D2B9cd=uN;>}z%w87{FSsUV7(`Qd+;CL7^l?`
zUgw1)G{v%zS`OARMDNsAP;vFT79$YANOdlB`#l~>LOyCj81(go5^#cONj2!agGP8S
z{#m-YZQAZ%pUHDcKl}Pox2N4dNQz_FgmE6G!edvTXEryA{nah?VL#uI{d{Ho`U?H|
z3IF*?|Ao0~{-yZ6o$vAeU@!EYw>%jpfBUgj#>!e5CvUEAC%mv%=E_=`CvR?)xUp5{
z&LI5x%Dmt!|BR{pHCFv)T=JKF##jCt|BpQVTpk<y-<DV<|KlU{AG_fxbjwrVmbakG
zQ^1z5h$UYhO}01rAzz+FwldExs6kF~4Qi58ScRPA64WH8xRN*dalO3xk1qDBdpRA4
zwwh#mU22RQEf*dyF<uI^J{h{4jB-e69z?2jP#|(qn2MAFN#Kx5!2TTv2X~NqM?ez@
z76MqfPl{$WARUTCoRTdlVb@MOo|!fSqvA*MTuuDSxd$%Pb|Xo9e-&+B^7;p8KHv!y
zGdWTkv2M3KDwb~Gc|pOG9TTC*5CJH7x$u$GR5Ho%3wrJDftk}}F%^`%fJwSE*)65V
zUqrfD&!pQzbX>yH>3}fe7BP5Yy3qf~ffHyzGYSn3mm^VmGHFgIRIA1<JAxE=-`$2m
z1AY}f{}C&yv9I6GuuBNs!aZBv6Z+$cMgj5swZJBD57x|;n^X9A)K-T;BM8ggluFZr
ziz3oqG@FA|qQCgA&OMnrDp)_WzCpk<hXth&`9N>nuQV(&Jh8O@@t@>x;9yax4?VRi
z#~8K<maeNAQdu*il8c{9`+M0JJ|ZJD+pty!>#Z7#EqBKH0gOzXngd~;QOz9d%*w2A
zE_^G&3$Th1U{Py6y=?5qG9**GXH>R7(Dh(&vo_}wSJ2*>oN{y9RbPzn>;6%0obNf0
zpB@D?dqjwVilF#vz6vJuE}>YW?|RN6TBp{A^I7eGw#1Amh79e&?8_KdR}gC%bpwod
z2-M`<(P*b*XY0j@wMf5hDHe(qpzh(a3hf6^!=mGE1m&b2Jr4i+>M|qPbD>Lpo41Zk
z-Q`_0BzG^gBWk^8-krI%g4w0LeKJM_h3n3VJhM!yd-_%cd|w<{XpGEV30)Mu;Q=ss
z4o%D!V_G7Ii&9`vEWI(ddto9~^~Q0doz$I@Lwwz*_*3G}03<NQ>^8n*Q^U5Z+uE~U
zEF^=N6Zalkn<W`u$IMMO2I9yxpGXK?Pe7rC<*Lwquqx1Ol41W%;B0p;W}I`~FCV1*
zq=7}{NIxX{?qLvgeH6qi$h{zfRmxEHBpue67|4(Q+sNPx9nqX*HX)a;tRr9S7Z_On
zRSx!xkN56xu+QkTUYxt@u*w!&e#qZ<%3^cZ{B0g*xXq7-)?SvXsYS891SY*moM`z1
zL#yR2am&xcncXYUuMJ;Zkx%7Ql{WwB>_&SU@^oYO=fUSi{v^%0a&oqCE(#n>9P@Nq
z4--ZiAj2yIJ#{#n-LrpB&Z<*AQV@=W9r=`Z8fEp9pQFV|k;m@Xhsx&l->G;nWOIn_
zvm0V~pr&U|#Y>yh)d#swaVNlHJ+<iiVJw@sg21_L(l=?sacQXLTiGUFDFTPN?l9Zm
zqCB2oc6O^J|5{QwAlr8(wf~lC-$xXB{p%Pf{K$(;CHwuqHJlxKXH?Vlr3S`0j?43r
zm2HMW>_X_vE^?Re2a||8VlF42kVs1X#_CAM;Iw6NJ9#|&;vEaNu!Q3pZexhh<x^9L
zaz=l@(Re~L%)TU<{E_=k$QJX%7r$)kfuoZL_r95#QIzAG&!p)9X3OTB?&b%C(g<V3
zI|UiySjTsHyPld`!D~hChd~-z10f{EMvvI()1~*n0lniGEaoH&eBwl2=rU>GFufA_
zxZ0MrX*oG=d*5hk04qy3)O>!YmW&bTI0Aa3Mdxw97sHWuCun%2_6G3}OSOK$H4$We
z=w5IW_b}zKY+@E2m``@jO5YY@^r<qVv`&MdQsf0<^ztiNX<Tbj93i&O7<?--b@5xm
z3*NNtW1_>1g8tBSLmm`y5CRttF7_1fB)umRS@JcF?hLIXghrJ43$f=t$bjk@`j<2G
za@u*7tR!vy96ttPsDWVr$s4M!CJdg`nq;4*6Tl*QY5bG7wP6zaML~G0p1k}ydH`RV
ziXX4u)72M61OTEmfP_JJrFT~BKgQOoQn*x}@(!R#MG^zUOi1o1lLF4VpD}v*aWn>*
z-Ok9Hez00Ty&R#@nNDpNb2*A1hR+k!037(Rj;ENPlx|sS`X2t?zkG<L0L?@43)z@}
z@{hk-ezzlkzKxnkf1Z*bd$&kx;{8d84kn;F96)O7{Yj{fCZOI(;{8S#b6S+F6!Exi
zKv$U(ZrEQl+`~aR_3|;41An*c=TR|0gatiWYG<_g#fJ%mr=!TGU8Cd<{{XRR&O1K{
za7Ka~*Hd23)xuDnEYBzwBAh{++s=Ad8k9KHRNvs8AH?{8+D<97&cHu??>7qu$-d%<
zJN8HrrqI8=Z5{udgTFPZzrOJ2zbU##b%6C6kN_!wivR20r=S6X7WM*vyrD#0+Ay^w
z%S<BhW{Qhf2k3GdTMW3v#rcoiF@duz36mC%dU63Y3hXPp1S#&sB<6dX<-OBTQV>T4
znbgYeN$Ox?f3Y2_ff=2|zz9H+_29Kak0esYI^j*_aD?0wmBLR5uZMpn#l=QJhY^_V
zGIoM%?&T(-ie}+weqQNv<zk3@qwXeqP@Phth16gf*cUz$9KrG*e8Iobni7+RbM%1&
zuyE<c%r66dnmSZ@JTq75@;GUHvps`x1<Gkuu$7wl8JV`{1V)Wml!|J{D#KCm7mjXq
zh<Lk`3cVK^z$*-xP#|PWB!NsU+<SE#H1=&KP?1}gDZ<LI!|M4U8uLDTOopH2PhH?x
zUKyH;P`5k+p8EixJEji7+vV2`{b3ZFL%ze=RL78f?U8`Zw0`jsesn(UUnE`so&p~A
z%_6I>@})kjOZxtw!!840ido3a?yGO%KYh4~aP}~g4?DWYF_&(G=ehGF9>dy=I{`Wy
zm`bOW+2H&8PyGePRF)MHj(mD%9@gvZF>i~X!_s{-1Z;DVSPmK>${nmQidF>CCDlX)
zOrzuh+SclBSP9qW4oxupqXVGzJ^<Z>CH)~3g|1ct5vW%l-ODy!G1sfttc}p<3*lD2
zb)Dzpf*cLnvq~9S7K3}k=Id~tCW{gODX1euH*|ocSB6+S#2_jP4OriP>NCLET}b{-
zT9b{<q->6Pf%Iucz2eZ?x4mld8Sf8rm>8)Y{gEI)ygDA(Q)`G0u)*ji$X76kcd=o4
zXC^r8+s<={h*FllY2!J_&>j_|MFlwmhjs5&^;-Nr>uRXTYb`E))8)<%&vjF(#{>{1
z6{00zxRW*xurw|U7~E=~2{y_`8m`K)ICPKqjl_Pid`ut6z7a8|foqsHVSi}GPPnIl
z9y{q?T_NxISB9GeLb}%0YmmkU?PWr)n%Su>QfRkWJ{C&77H_vn7`5t_3|EWp0ui`d
zB0R!rb<4N3h-=HZMbT@azGF0m!fIrbjC~hC^iH?92a_LNJ|S?BZD@|@0Zk}<pk-?D
zWZw9PxFH;VE#<xT%5QCb@6Z1Cl^jcMo%;J`_|i9v^u^lm`fgsVrwOFmwJ(LPa}h+X
z>re_^=Q5BNus@6}{`1PfkJCY;jKTcRwZj|))*UzsUK$MBefOk2g4k+^KeyzK4U7HD
zSKyjO``98SCRvYZ+!837A>{fg`C!ZN(eE7>VJ2sj(uSa5VGUL=k?ZiS<#11YEtfHe
znzKH%dyvjRiffkAIl+ajC%P<#C~Jx6;*(Mil=_?b&Mj#m0JC!W>9w1o$0JlC*>fqZ
z7H1Hl?XeDxDgMWy%R%a64+d$q>&Cd~q3G>CQnG0V3E$#p(PjSJ<yG;KtW;kQaPHH4
z_b!wd<IYvC@zx?KPCs;LehIzR%p-X591{9p7|RH9nXNWw>LP+S;GYa_1Mu~N15axP
z=7v6sDoK!88u7B-hyE?j-JK_Uscwy{Cu{GorSpllx6wb-$~(^F14>RPdi@(aw3PdW
zEE~%HY!8k5-eH2q^#y`y&?Vf^9*O<>e^$bh$5Qhoj>KqFG&UX-)!rRn9qvm;OF9MF
zs(1S`f%<vUoEV6X_Qp604E>0x`dJGFkk{F_j?U$z%T8WGR!<&SGu&j6;tc)oz<Y(E
zXO(xOqwef$w!J>iF58>de<AG7vU4*zQ1a}OANMx{O-fn-APGv*>)Iw5^njq!?78^@
zgI0a`xuN~8j@S{D$_GcK6L!}1o=tnc&MqF?p<rt4tI>q?fQ?zWTq;SlK3}!vp@4`Q
zd7P~BP@#~di!f{uEc;bJgKh7zifixMw!21hIxgo)KO{){J6v@0CKNMjq4y|}($;sV
z_`njNiF4Z<WcI~|-+#vdfUNh63E-7?(ure_fb8_m-B3tKAnmy!<B&u+^T_xV=?zA(
z*f6?Lfbo`kM^cT<J$SElViOFRCBawQ)XG$3U)i69;R1eRPR1DtF*Q*tVykDD<VLTv
zk;qu$Eqjj0Q#&y__4e+J8X^)D%V1!Cz+<rN2KccQEUEt@lAR|UU+*7j_p7@Jq`VJM
ziH7{i8;*$OxgUvGaLOL&fR+p6rzh449|Imi;fl-0%TPw0Uz=O5djQfBClz@F2+{6`
zM1+FtZ_64bTVig*U(n7}D27<>A4*^P_ls&>(pYQs;nUP8cUiy1hiaSCcF;E%?62AJ
zi)w_$Nz#t!%{W!~1o1;ZLbCQBnN`S9g!<d_sY4#ab7?ho-Al3c#VRDRt(~llSo&Ho
z<C<BgjhN{qc-h|>wKF(=j-!fty0v8Fsf|BO+epd6xR}fp#=e#V1;)l-!P2MS{G4<!
z^o7o9=`6+0i7tC!6CBS0WzEA89!oG5LMq~97SG$ToAELR0bDNa+^NJ8tBIGbvNuOS
zMn{kFOW#F++vZL3izL<gKXDk(&`?PF>flBLo}AU!BiT1Xpyqr!WzJ{XbdvndGVjaL
z*6jC;<{~r3a7`lWhm+380ue9>VTjJdF;`iOxuJy~CRmy7VrkDxgeMLgkR9jaSjKU!
zBeYl-YQn|ncL^v;@I3@!t{z}?<s79%&P+yt=KoE8DCq7W<%oQD#=ctLZe<bJ=6wRz
zWd)zHzPKq4+BPgxI|PoYz{^NQu`FW~Jf-WWj)y}$YBTKZ-2Kzetp|UapY49}oNhOt
z(THkZ!8&{q(x)<9$^vRSr-l;(p>EVG?8z@@*08~e8?7Ldq91He#IDdF<h=|k2SJ}x
z-uxtr)eanAJ4Rc8pYap1CRiLj%0-~ipTPkPCyHO?>E4{zJfIzI8|GkX!Z8Dq9CJ$c
zN_lJ5n6>3e`FWPC?8>2a-HiPj)F8LJQY#b*8j&42(OFPSFZ8#mR9RM(vfiMY%K2??
zV<b-)WAX4uT&Oh>vqA~S&66S(73V7-??G~Cp(Gv4$|RG=F4asr5zvC#+8$@jB$l)j
z$%+kE;-hO&347E_HW3DWrKsTbA~Ch3XI_ppc;kSfmTYRvpbOpc*@?<XHuMj-UT!h;
z)_v6gXqHpn>zt4xQn^}d*+m$idNRNZ3vqsi=sj?UR4mMdxSKXb+xPD@1+ywXBqn|m
zx}a=IlVEl<8a#)~?2Ud~yu^^e#Dfgq1HTilZ6`E~9ESJtbg<|_L3mEhmkcg=k3Owa
z#WN%fZvQW381Y-eOJH3LXufF-!{;3?LNNTfe-<cBmt`waaGy-RArdVF{xvD0#9rj6
zd3lvUV-wpTTgCaYQ_@k^9^%BE@OsHs4RnWB#Ko$a;H8^Dg|&dEtA{-jtTHRF#d<x|
zR^#-dFcWr~B2y#G5FaNk>}F=zDLCStD-?__hVSXnZzs#Ht%E+3LKIe7kbxKHF@2Da
z7OQ{%W38B^&m+3g)JVKrgzvgs>3C5?X6=Pevvo&rTn>t3U8TY#e`IdM?P|m6`<qhT
zgAJ1heGly6$!y<CcEb>aLIyZX9wYl}gGq%&0H*a0bqOAFiuU}iWU=&7^>`L6PfYi5
zT!6<0#*(uIu-z})oIl6Cm&Y3ScE&t|MFe80U;uWgzCj)Ljk!glJVS_X%YS%jb_Vba
z0QFc9jb*Oy#K4l0fGK>Hf@_X~2h9^KgeW)8FPp|IUD}zdY$>g?*#<wKw!W2<OFEGm
zqeu?}eo4!@&vpA{eStaKA`x`%_2-=PaVrBbD@BBn(oYN#^A@64*$%IA3h>vl1tl-I
zW^tTbzw;DP{rO!hvMwryoWdXNA*#d(DHp+a@)a~zsL|9U-uXLKASw6*LHhJl$siA6
zD)bP&I=L^ahJ%RCQ~;6|u4f^5pm-@Xdmz4p)){CJ=1^0;0qTUu=1D+7%&23q`kn=E
z_=rPk)C7cLs2!`<v})1yMlLfa_cW0~1eNr9_0Z?zlzt9jA&;G`iUL)^?-NQ2(u`n)
z^$2S7A-4(ML1MMSCO-#Us2f~*Y+)K-0VAnYxq7J-gUBBa_F~yW;H&w;4vR3xSBdQF
z%r?#C(`na;5?b?xNy9%pSb2AGb&qDuj?5RO)ZjE(gGp_GKR9)tl6{nWIsZzr<Y_cn
zo4$>NRRx<%QM)lx=3%!D3AxtEh{%F0nW{tQRP{vtJxzR~4;^@=or4{^`u?)HBK1%P
zV*k*{Zl#E?(QPpx$_EB`o2ugZK@HNM*M+1n-E^e>ae}klE>Sb<`@ND>Aew(QB-D-Y
z_PPtxS)f@zVx?rm><Ik>VjX%qK2!x7cAp<74>K>G3`m;?WOeevG<98$U}=JK>tng%
zf{X2}0bJiGKiXj{PnzmxOtS~eDQA5~ydnOD$x~eW3@Q#rnFT>OqQHpCI*mjkw3z9b
z=Fh@sa43-pkfHRGO-%J#H$*sYpFG!v^B*6Vj3^DsS}55%4TGUP7RXQzU?sF7ohN>I
zA}XdM$-7a>9H!e^R13V2u9Is!&fUPlL1HpUiug6jWAm3I<%hvlqm77G`qQd<Uz@HH
zLNq)3Sz-IMPLufba?((?rJ^|&kdYD~_gU`7$f#%r9z!mLL|FB@yR?a3i@0?q2aQsN
zR7}y-v93yY<YsjUjNu>^z5MhzqLoVGp5GsDhR%y&@Op+L<adT-V)==~_ccn7lh2>X
zw!f9h`VY^>*B3wCPZBS}N%MQoV1r5B*chbl79eS9w2dQv*a-%dzIIt`4Cs`?ds7(j
zSXwCIXWiCzn=ksKC>XJ4`czsj#51kBe&n47?1giw!%O1kT&M|{oJA4-BQMkD+glNq
z-i~x^Iplvb$H=vWvG3K2{W`JZ1btS4^(~fL-z+OEF&D8!{AD1ox1O!KGf$kszhmMF
zA0U^spV#T?ju^mnD9N6Wn=<62O?UY6Aq?mze&423%clzVBSEg8XB68?DA4Hl0_TEP
zV9S{TY!Af5cHCQ+$tbp{wZS>#Sr=?$%%O@HnlGCtDBFXU2>AKYD*!1?@@xi?W-Gh_
z;O2y_oOs{ICWTzPrw(Xqo0JC!CQT)tYPET1u_+5vA}q~L;w68SR4%S5W@J;itst5<
z25|d!rmiGIm%JF`<6grqY3MOAd_5NTDfgmttk>qm*t0UN<QWsmurKPdtZT3|6K0LX
z)CL0T7CdGcVqN7LmNA&>f0U~<ry87OnwEK-t1x7mrZY`W*T{A@yy%WpW(FtT_7mU@
zX}05=q<o0kIBcb|No(JDcLNc<#k8}k8D)3Liuw=}a?k#nNlMf*`+El(pDBjw-VdrA
z4Kx2pIpks2VKCAO!Vur*<jQ%pZnpP1aQ8tZpm8~YO68MqX{PetYy4l~_1oZ6U|0Of
z`(f8igrf(Fg<46!GgnmHziHy~5-4W$6=tqovT}VjBvFbEpPTAvrbciwpzZ2gs(_n7
z0UcL`hCpa|?Lk;TU-m8fAn^y?_d7nUE>>>Mcs({QSmHPRePoylM34$&A{`WMCcEaM
z0`zX>@?g_nUzjIxy&z+g2OKfLAGFk_y^_~hiYnx^qVmZstu>`Ra3$b<Pc1$zD&n_U
z#PT*Ub@dzdfqx?3AqG+{w7=OZw^V`8slPg3E10+sr|<?E%h}eKrJA0eb{)?V007g(
zKH#Y%leKzkbemGZ&S*cj<+OLM_3`d_m<ggc!WdT!QE^?7;)l}`n{yCo4)h`#OC47B
zLpmhric6gZ`U&qo@M3vp7zUqOw$4D;w)pVLiY>@N$}|ggAbZti+J<?#%Ay$S?k^qk
zB<6NrBZE-3%jA<lc5T~5D8jQ{ZLEwr)s(Wb3V0dAwjuWdQ(|yV|A~K7SWA5{%EO=>
zNP4IH>f0=yO1^ck3bywwUAM)o7Pjy&mOco{f3L=p%7dISB6pjAJk60<M6vy5>zML5
zq`Z^RrA3AbKY=Sjh(?}5fjR0DOa1g+5{<02dh)cS2SmSK3tI60^?tnV0oO%Wt>jEL
z@8!*%HXP!rSWZy3mm%MT%0+uA=(vMXPXA=DD2~3d2bZL2ufWO^kLv1lmaMCfHWc)`
z^rRq>S`YfUbERxq+SqyKD#=wIUG9Gu7DC_pp#13w!MpLs1w`J~a*!l|2khwyM7EF}
ztsff%fiz?7hZuQOi17;vXKLAtYI*^ogOM)+Y@Am>OhRxA4>Ne7!x?NZ#MP)>)uO!_
zm^=preTuXut^jk6cjm|v_=Jji-gxMQ`iR1;6<+H%QYW7R7NEPL3Rcu)7AW*-B>oGs
zAOoI2%X}M4;!I1%k2nzskjCg}Lstxu>FUT~%BRL(9h7G()}QmQGZRm1qcQI++k9R!
zLwx{H6pMO9F~7yaCt<QWix^s$vRhc|KAJ%2$;9sp&Vz8psiBdIehI*N4tNaVayKBy
z7<X})Q6Iu-fkBHU3N5GlWeBsp{let|2l=+6H4zx+;knoi&wSI>Io<C6E#%SX%`Odj
z-oYoST&qcWxPZ50WvpJ?6t9=33cGaaQ3{>WAkI#Qd=;136rmE&D>C$t43-)ioKH{j
z(V<9s@VGu5PYoCaQ%G5|T`@V&aD!~-Djgy<W<N+zTtslGkB(jGaG9CKQvLkQrD~_I
z2V*K0<vnkC)#l#83E%6^*fyakuJ@&L#_)xpKQeR)PhS75{HXGsXL@X*sK(rTR5a~=
z$@Cqv-#5G7Hp+2%%PZ(oW{P#fJ3h@D;d(%_$8xl=@T)Q@kU#Eo28NHJm)=;T<x9|g
zH8-mj58Eu>t(isYK;m7}c3fu0`5Fdx^XSjRY0v7m_v5j1*FM2?7vL62$B*>0pCj;%
z?(xa|ZM}gF_3SNCeWaUi$JDV>Si^4mc9pE-wjr#1GOSJnsT?2la(ia=@o9*=IVfh(
zNN>k+P<zC6xJKpS`GH583Q^rR!XM7ll_E7VEQ2V|RmhPOmGaC&@F}$uwcym!bK%~v
zOW*TzLh-4&awMn$YOST<cU39<Nj3TM%6TTye>zh~Pkj<4%AcO%J4?H6(E^WFp8spF
z+yWlQz+A|ONz8(rV3c>it;iWej4EK*WeLJ(#quaLPQe%{7GHwCpiLFmobu4&Jj=@I
ze!czgII(VQ=RpjaBl3$sB)B8~HfYN~!*jx)d>H{ed1}d`B|V02=mBL%t@C2hT$~J6
z#I~0gL*(orKp1L(i#O}zSL=5Myn-V-52@lJ`OlHew0p3WhE&jqpBtC){;|UKyUKH(
z>-_eHUEj@)Z!RE-LMTiaA>tJb;~DcD`NfXPl`1s+#POajFTwA0*llk-5@?Xz?W)|Z
zd}|_alYy5A`zcPM+y-x!h~5rzio3k?Xcdkox1CeqL;Y>OD3nYs*xYDk$^kdD)-)iU
z6@D2}uQa1Jc!=5)kMu5k{A}@ilE!pP54KKJF3``Cnug9_cw*bUw;k9>w4F8i_-gi$
z?gU%vCXnx*$7#0s@oD_z&H>L|`>R%a_Q)|L82yq&iT?^ys!Xn|;-Jz-rqbaILi`Ld
z8~{z<p7V?N((_~E?9u79vwL$Qv&$vBmBS8~MPF7M+RoKc3|SRD>f9!7#Hug97-!iK
ziIN2Nlju$Cp^`2@Wa8l%R<54&#pvufl)q#j_m+I8_%V;WX%hFA3d73$tm8+B_Ozw7
zYok{<ef?i5lTp2InKdisu0d;zlzWFB!9DMEl;oN`4oE%pl3c46!-aYV=$uz|&8td*
zBb8KI-s@uw5laguR1nn;vG@W3jxn3~dT$Qzybf0vx*^5SISg@whdNISRpFj=)@XS{
z+{1srEHxD(|L1xAHmJz|0$OYZD=6}Jq7KDQuqOYanYf6ljuM!7>5K2N*abN^eWz>2
z7$YmhOia!mTeN?-E_0Ak@X8wl$>6-1alOobij&QbWW-;MXPL27I)Dj2u;~>U)#=wK
zmT2I|$G_xF2cSxz{DZbe;e688b;*-A!o1=_Z^ALBQ2&-0D&4KvPmTK$QQs}KE3{2u
zk&LK4YJHztdk{_GnxZ4iaNvw^PzO2KIF6rfrAPoqBo$sf&YAVO9|1i@30O6cb{vY?
zNT1rN5RQl!b3g$W<b|pvem|PJ#PBk>3=EUHbRavVHu2<=F)ZAF#DQ6=3G3a~&;^sD
z^vHzM^Kx;$<CJ{RlElaBqk#fhJ@2MjdncpVK=B$`li5l&YJB$ptxs(C53lQm5zWk@
z%xpZ{K@A0DKQB0=`kdkVtnmEywt5HOQi^nq&F124cX)%9&20|E@7CszejGI6gc5~`
zV+^Nb2ti!Z{L8E9-xH#fyfHVHtz9nc$(h=x|JvB#vbdW=xSqYnW%GFH^RO#x{WSvi
z-W#qyc6#eek~b{t4><vwJDj3{*HBc)-5h*>Q+m|dSc=A=a}2mgtjkks?ZPwI*}iD*
zE}o#(KwsBWw>Y`LazHVu5Www*rZ48NMGQI4a4d6K*ZKi7UyOpfmTQKArvW(rQi}vi
zbq%5n|8YkuUBro<K`8l1-{YX&i-?oO`bN`e=>5j?Wmwu;H`JW9EB`)taKCcN-a&Q^
zHTdk{j4zXfTq2}dCOLU)lpbvm^$>{m{Is2A!=RXk8yR}xzRt`rNoV%Dm;SCso|^#d
z>_{Llj|{>j@709a{FP`zNn*f`yw!^$YR*UiGL%Io3L<3=FfGi50T{NAZwqe4#7T0B
zWiP{uviKf!e8>120gHAF)$?M2>e_k5#PSF@bwoI&%HrLE(`)6*J)K&;smss(QoVdw
zjkQ5scC>k0?6v7p#BWY-a^IUte!UgED&Kl>MtXzS2y~$Tp*Dt8E{hKXo#uUZu4-Lc
zru(ePvD;Ptd&<U~J$CPj$j!8e-#bWIGV`Zma(pQsspV$se5YpKPK~U)n@r8X|7VQ2
zMegi;+huMdOcXA>Wv#9II!~JuQ2n^agyRx}DLy1%;(`r!oWQb`i;r$pje+W|Vn%x~
zhOv_<=2VN+83ES`8Kih+>H5xcp}+Qwx&)e#VKIE|o!K9I&yYk{u$*qH0&)xnZG^|7
z9zX!$dO|e4L+f2iY^*6!0hAG-l#tRb6qijH9-)Q$Ru4vit`l)Cnd0Z<fy)vfrv={a
zAOUQZv*0K+0T!Ny_s1;M>|=`<a{wVmF(e!b$R7|grR>3vR2OGGr}1dIi(5d<w~AJV
z@qo;ckCQ_j*7?}*M?#er-8`19oA+rVH7YvTl9mvS{VKKAH}UrB9B6E$^-mOyZ<Z!w
zjz~V4%+snQV(+vJceoBm&x6L_WwA;ss@HUPy%D`q+D(c~JGaR39r>(1$>nXzMx@#3
zROeY-p;fDwt+-BNM`qSg6>E`S5=-fQOlF1xQLJ+C5|q4HPIQR<Jag)i=~v%NXV4gm
zVxSb{RF49g@8C=5ui_ezr5*znb3UqH;TU+K(txts{0TI2NFd$uFISE1dWcxf@t|GP
zMF&Yklfasfz=(Gk61dMw?9IYGJ$}>O<M}CtQmbzQ#S>y`hFtbsj4NP^2}(Ty=5zjT
zc?-$1vDwuu(?~xV;59Bw=Z%#Tj)#%KKP%%9IUp_YDwz-c%u{hTMFOpPy<dsHAdZqh
z3C?_`Vc@?68sf&5bRAuDhZJsAbOrOcIpri1E)=<UY{Iy&xqhTK+mu?$%bp&ogz<^*
zq6`sV4l!XX^2Z?l5E#J>Ji*bc{s&#_Gw=EiZ<3wraUp!$9AFp^sto#n<S=6dGd;$6
z*R0)+&!G&NV#bJ*7^P}R8nO#5jKKozGB#VLjKrOEbU2eIY<#p|;kjxPl#<fZkg|}o
z5!_Z3OeCyTU6!f=Meyp#F27FVkP<=Y$KS7Z#$c^}TwI(U8%(i;rgy&}V2Rf>W5hXL
zzY&he(V{R&dqOKbc(&20qT+e?g&AC&xt*Qdkd^eN88Y9HsQ$E`6P(SkhbIUkK3tK*
zU367=Sm`OC=8As*q76%B*pIM7i8(SCDaErQP{|?oNcHF=2}FoS(&YF<h<+*Hu>Gns
zl)NXWe}OcI8VhHWSMUc<OICPxKW6Lu8dI!_(oI*air!(o$oJ9|pWZEN6il8pky942
zXj8YI=bTs|LnyJ`O~9<Zxlk(JYoH47s(+=zzB5Thu4&wwN3DcVsmE+Sq|H**R(NK5
z+FC`!NQ_p0$Z9@T%rGr?YrSs)%wi6>hFX8F52DVr@#`2^$fCTL;q*6@+1snCKd>F-
zTCVfyw?uZTJ<7OS2StZWQmDTUI6$7|lJQ1v-a<tZ-UtqxmJ@4DnTQ<KZMU=UPh0H6
zhzZ~i&rQQ(^Ca^BsgzI^S>RwE=@*t8b<0<Yuajjl)2qy=O*R=i<{Nd7&)P3RB78Gv
z{CV+tRKb9tMiWeKU<y_mIt^}HySn!dbp$hzbaVIBe0QBpA!+;Zilw+>i+GOAj0)e7
zU+dz@wPot!R3Z>W)7|m<46g!kzKo|WtKZC`Y0;U>o+Q>!eLuA8I1cDOb(w2ePw>2z
z_I9ZuZ|sm7(fITLL>QKuu7#rK(RmOkV0m=+%E{;l2!okl7A;M8m=Me?-{g3u@%8+<
zjvk4*>=Ul8yW35?+S7Jta460p>%<5(9n>s83cNFN-;8+^BFoEdcE5={7nbr0A$dAG
zi;MUpMf-NvQ}XS6Rd}wXD`%Sae}Q2Rmx65cjR=0L6Mw%tK_ANRC4QzmmVcRm5C7WA
zNBk&>hv2`JB?V_XG%N=)Mt)UFgDi#%!u<YB$U)hB4ah^<v<yQ!Vn26ajodLP1-+%T
z0mbUP#!?KD?&#Qyx7immh3?8Nq;Jk0`Ee<K813;`x8zIETfH3MeLwxWNGWe}o-9+Q
zUvTCmF9j*>u13$9MTH>Ww3$v53hx=aUwZD+NNtS~R8f!S2zHVrlv6*>e$a=}-K^zM
zrvDxx$@Kzv(mbrK>T2#H1~ZN<cxtq+5b&V4#ABv--Ank;Ettbv_!cki+T*&YZWoBg
z%ZJzqcMFLe)wG5=%@?fDqeU({GDZ1#F*GkJ&zfu@9}uFNEqj}hb%l@7QgUMQqWeBt
z%}gEz$vZ8o_vJ?(KRZaEQRU+(x=B7G7Rx`ta;@c!Mp$1zRiBy(3}P1@m{*NF5w^{^
zn>+fjf%F;k?&4#9@O%!=fjs<bRE=a4*B%Xcl$$@Ryd0i+a|o|-hm_RyI*~p9+xhST
z-YFXeRt~(!%%5MMSsZIpe3y4c+ZsQ$lC}@=$lsUgRUf%v8U6(!cYz$az{f?$1-&^7
z4)TX_z1<<OEtFg1c(<=zYVCGtF1})3y~8T*mhN%LOaBx;57xDRmV)<jZoQRN>6)4v
z(3+2*qv+?5T3idJXiZ{qcs?($iG&02e~<2>?(SUNJXNQkvnv(r<<ZWWSjzf*AI&bL
zCO_V>l#tjLCATIwfUvzr0;Z=(U@pr%1r6Ns(xY18$`kp`%oDA=&9t!v@lwMK#PS?T
zs^-X65VMToxI}nd5z@L8zu>@g+Rxdt%$}wY!mH@yOP^SA&FeKE4Z#MH0#+3CbSh@@
z-Az9EPg}os@RKYz4-Fo?)bN&;0e8S+4<^txBC}?X6%nE2J3atypSb_Fr`v<!AX**m
z$@$cT`6|^E&H?`M5Pqwwd&Z#h;<T;VyGS>6lN#IC!rHuKhZp}AsnJ38EQ|ViN{{wQ
zmOLJ{{mD4|s=D5s6h<nX2mxmr7WSMN8+6o@YhdeZ99yBok0t!Ua&+ibKuERke)ShG
z3C}G)gNq5+zeYr9Ye20kGP>>L<@Iax1y@b+?_QRxPA>9Ti4ACAoyBRz3}-nQ$`oE&
z(2e4NSvetrs1T(1{o5#qYB5ogK2wgu&4Oc9o#_B1I3(@}&$(E=q1Nj`l>_3Cg#$lS
zSS5I?pCQpLC&}v*Oi<Jnc-g-Dr6Zod0Cmew941=GC#oK@>f-20XO<mwa2@*?<Z3zB
zKpGrOD~)Gz*!etc$M*I*U3RFNX~xmAJ?Ao6N{0M%C3QRN{B*S?f`wPO{sNDS2x{hj
zQxFq#=!M5GywUh6fe7=Hi1r<{xO+5gdw9|m&FgjK1ZV24vA11IV1_jURHW}qk!bK~
z(sUZ03CA+FMB~!|<seqk3GRsx0xn+hnb)rGyKc+#$<+ZXekuDnY2;tAI)r3>!#I1c
zL^jic9~<Yis>AJ8kNafrzZ}b}suWUn4(~&S%eW$lyk#oPNhACHquvnI8R_3n53>GY
zZ3pRQ@Q?lF(qd<mdI}OEmw-Xx<(@=JlT%GHbTKF~5J9*fYd>AvBP*u5N}=lLm;M;>
z0YJvC3C4lc@-6+eTxQAcH-*)d=OyKj#qziJO=RE5S*qK$T-@ys_nFbmxC;W$`8St}
zGgHd6G`ew{MZl&Otz0a47b}|DU(7j}*vb5-g@N%~1+TpiMa3VOmGm^NeE1#riuaS&
z^sqh35x<uwo98Z~AyiC12_~9{CuhPzYE(oz*^pAqQqnyN+w5A5?-_4yT@!TJaYB=<
zef*@uTYNk}f2WoR^%&~i{oG3|kA1Pz=Rs~eR`)3Z?9?DJNKk13X=%Z)hmIZFRKT}h
zl#W-ya@TEIV0t>xyYmKRs7+N$3+@J_6R|C7(D>AN)BP9e0qfgdt8}d`hXMPZ7}J)G
z{<QAUb=P?!1~AQLWp@Tntqmt9Pla)5w%HC%w)kgX89{2ugmD4<v!pw`EP_EcZ?3bX
z$-VZQ8!P2nWsa9UJ5zvID8K_gT2L}tkYMZ<l#I7f0}J!CAY}Bx16oa}c~wG!?da0}
zXQ+XDG8)hdeTWEmeM$jCLV`^<<$a%LTlRr)NKL^K<o;wlWuT%%EagT0qgzQB`*_wC
zN<DEi^jSfQM7~xJk3=Iu(r0G7frJgSEW-?>h#uMPFeBy|+nIFp{+J+_gQ?l0jfKZU
zGQ3~<$A|HL3^a=6B((-}nP77cKXYPS3EH5%>!u=HZOqL@YBKUCI5v!-ZzVtaP_;m4
z2u?f5>$g40PO3+5es;Bj3|8iIpF27Y-fNIYcPWf3agvw~^HBO@hBS%d2fm}x$(9=L
zTb!5TW(Ot={#<I-u4%AhIqux;$vs~xzMhW%JPgb8_~Ext3W>3HN!i)h{dz@g={5B&
zg)CWdUdL=Ywif>gtQAKY?BSG0&fRuYcMw(;v8YE_^&<|wd>-wRx(GQ<nvN*3+G-wP
z=6D(WhyEpXi$KHis`^o-bKq#>`3ZgBC7iq}5Dh&zcSGHg#7biD;DC91H%;qYXcVSB
zuYG6Yqc4b%YWI^!vfJGhdCv2i^3ONvFKhHLUroN~$)cs}-`jD+R!)(VqD6vY5_fLf
z#%SdvrWltal**{_%FU&%Doe<8`}~|mScpP%lfV;Iz3<t+P*vnt2o7<=AVX&ULCkcH
z#F;1gVs2HQ{;Ui*^{3iGdsOdF<ApX^SzVDs%Ytrk%!Vw!t`(S0bBVdc!|fB3tWwL-
z(9mtJUd2PGNc~%8XM{iBvTR*T^O|2E=hR#HNctkK8%pfck0Eu@$G!;ini!eJBx$z4
zYHz~`r1+9$V<D5CZ1$d6Pu663q>yXGxGR_RFh#o-nesl#Z8V~%G1HDFOUkb4Uin&p
zU0-8!t<XRX)PRSDm=4nv-%HBMKxz{Up47)sph~7*wM;$xTWc*-&o3*m8V$I#^uPoM
z-tdt%LGMv?N&a9l%#`fJ5a}3f`S=C}AQ9dxoryu1!Zs^7una5~+;c9fm@`Nhtp!+G
z*P(QB&eb{dSC@w=Vxotk-vBaZ!MWBHI5adpXf@sNHH~ynPcFGrE1m>|vORQT75zFW
z><(wh#5BZHiFil-(1Ppkaifj`kF67$LkzVCed>hv1SQ8oDps)W@!vSn1|~fw6cK;-
z&9~4kH~EB#qNpu^VYu9SRu@t~%1`a*7xmA`kOfbKe7YKTb!@BYst;X0Jo(hS5$4I2
zX-NsU0LR#YLk`<7axb*GjWOsi9{+B^9Qo|o<mBdMa_mZ9ZC#zc4G?$F$NR&cnp%F1
zJ>Q4+eR!4x-gMM|aR!Bzqice0k!Ku_2wT$Nt+^uxwx*^%aDO)Y{Mja*J?uc770EfV
zd&M)acnyiZh}&mZ25~jr#vIUz&0%xYC}H~)KAsLp5H;1wq~jM6mRxI$dd9WPHbkhh
zHlS-0B<G<O{O8HSvlk$;oud_{`lB>glKgz#Vhr~%UdWAy&~e*SDo{%6{ZkeZ<qi(R
zLQ8(z(FvvS%4o%l&{2Nu<d4}Wu>YkSc<?7R$}ePvZXp0X&|wEI4>G>`kicl7O=No_
z#JR5gJGfGOHDy?fKBh8?_rN&%@?e0Fd#EGg%7(dw0E7bPZ|mN}&2tGp9he)4pE)`@
z6?{9Rje^bY--1{PfPm8VcnAq~EZqBMb-^e6XN7y`1Fo7yzxj1@JBRXGzr$<c$~CZi
z;5ZKxmo>KYIy>R_#KFPE$;Wlwf(1A+d~EsSx`2|uKov_QY;+p<2Sc4L^al2`PvjHs
zq(&dLCwL|l1<C6Bf*>J*R0r{hQ;yWx7UA1}_4k&Mr3*IGw2E4NRazTduxz~?Jflj1
z3bzMm@OXd`OBB$Eo)7rlBbXF(IS@^PYy6--Lja)E*45O~)!T#xLY`ZOv*E|%BaIWe
zKs?YH5<I{vXaDDgp%<0E{1tK!HSZaOh<W4^C!~mskrO|)gn>)8OJ3ZL0^BfXcZ<bW
zBmQQTv<mwiP8+1)HPP_DAqRN(t^Y!I^+|#wu8faZBs<*RD>8}vqrb5PB1q<DY~N5|
z0Rend`YTn9$rPw#yMYNW1YkSF7%uE@npoJ!BMH}n^T?dS3RuQ{e<b1QuMw1<2`^<w
zG19;=|NhH3D7flWDT{c>3;`^agUuw%32dS1mT8NLI%A*9cs$c3Sx?yyU(gjYn^
z08<Ru6$P?gYiz$e<P)tjI0Wd(CL=H=2s<DnyZ|&OC^DUb+tk#a8=O>JoSVl$G7xt@
z2aH4^PubhkAD##H0Kx`$yNX~VhGK&N=gJ6ox5cU?7+Ia->Vv2^%o{~4Chk$^2yT{H
zBB89piUbH4jwOSp7-uLXBn@ahL!?#U4=fgP2lO-A+XLoRx4$9+0%o0#Ale$G#ne?Q
zu|(OM_9G~xpPSJQ?P;&@1OBZD6zK~5URd46iKO?5vWqEluU1;Wsw7suPVp}squgW<
z9T=hD+S2Qnj{ZLggFv$4r_eKQbmEL;^khYRhSv?J_Z+OX&mIdGG@v|f^Ve;AXPWw3
z073jXRXgEY43wfp%^0^7A+b$MJV~rR-03U*C+(3JI5_zD1~5N4>MfoNzcsFaqoNI4
zL)@pgmaeaG`Ymvc$ler+SaUq|aQmf6cdf5lT&lUfvQ+z@WuFzk0>Oy-C94Kc`Waf^
zY_+^vk47H<Y_;Ex(8lP7|0u4*Wc1wJqgUvvB0rgGS0{rNW<NRIM7QuYV7~G+&<Yha
zzVf)@|L)>iAUxYr!QtVy!nr|yoo_9>e$}-;@zP@ZWomihV-Qfq>X#WKTH0YopI^D~
z*aGp7dPMyvh%<y4g`8fQ{HY=1QSfgj*P3u~8R~s!s@)dL$%EoyFFg0|FDKE9FBL43
zVmCy>B>u8FQc%Uh&SCo#w~~tg#`L1p`q-pG9hTg;PWx@G#d~4})q4Df#N5v&!~_1^
zGwh*~o6fvinC)GR_e&SZX&>Vo6jAa9mG6uT#Z=+ar3}|4yg3uTC(pnBu92;FE1Il;
z%~zJn12hOvj+cg}Uq1Mo2?mRA)o}R;35#0>)7rVmRR&p3uMEEZsfb5-c(n}38lSnj
zkX~7;JyxFu?y#%O;ZkMBo9Q>m>b{4Acl|dVuH)<u>7P<1$|qcf^^>Jm@BQ7(*R&Fk
zwEijV+}A_BPH|)&ncTX<g%D2VlkFA9s7NWOxa>yTs$Ltb<(2-ZNmcRhQ}Gl8(REne
zSRTG;_kS_<PtmymOB*d58!NVL+qUgw#kOtRwr$(CZ6_;s-hc1!{AZkfGe_O@?bBVO
zN6qTE$y3KvgGtmD09L#GR~fxuR=j{<)=Km|XkUu3%h9F4Ef<83AO0LKG?Br~$~nV5
zn@(4DedG;I%WLtof`%Dkpf)@tRk>1gbog|3(BRv!&TU74uY#f9&o6m&h!qhqNlE9N
z1`IT-YkZrc-ln>$F?)Qk)2;iTW(4G+U9~kCwE1ZApR)QX)dosuaM{5!PA7pu|DEWf
z!&Q-S>Y32__*$b)CztHLMDb;AS=IZzj223_hkg^WGjY)oJ#-5h4L--)NYzc;P_tvD
zv=>8<pD*r<VBgA96jd)5v>J4epJRs%DY8I0ez4-%tWxz$Z_UC0!LpSvWJ{lGjM}?S
z=cE1WR_)JjT;19MB+Dv9$-NmND?-__5-zTT4+YOI%GpRQqzd%*AY$#$FbO*4$eX3>
z7Ub^QgOIbo2CnPbePxKF{dsmP8*$8Yt?mbhm(jaW@y=(&R_-gn7R&X)d@@T<2t{nR
zP6~rZi4)}Jwou*!HQcnMfn(N}Kj`gkI?@*1Ghtsw-M6t><f5TjrA^pc0m}Ki)}|G!
zt+Zl@k7k!CSU<Aflt$W1%s^V5d7|oFyY1-VkfsVepR9UD+CjASUPl1n;UTMsY9OP&
zV~^i|^7^hbX*I9|i{EbpPHsjvkzV1@p->zc--LEiz{g9elLO(a={C^(XkV1Y3z&8|
zu9_0ku;=};4!a76Y`Qw6t##Xi<f&5nRBCIg;*=@X5PZgb8Ch>aQ_%m8S8aJ*PeLQR
zMHlT>MMFO54kwGAFDODrLy+~V^G~g_QxtwF_A!-$A6;KUgO954OR2M2pnfTKIgJUJ
zmbe%4_K*5oBzI2hQ^8I7Qw22BS7q_fY*a{Sc?yw+hNfC)*}5Q2FwKBQNgpm|%07jX
zVHHXBFdG?-Q$4bxu}irK0)h<bbbTsCqYI&?QK>Aft6%_oKc!OWL9}D&YAQ-s`jyh8
zed_Y+@=T_*zz&;wpbW3~h51S^WSBFTJj7?8+i#JZ|EA}|C^+8W#koovh4i7Aor>jh
zdqoZ?JCqwCf4M#?tA4L%YxiWj=&YLTUH{6<rhV74Z{xhzmT$l_%Whs{=vRh(BzkQ3
z#FjM%V8EI+3sbZ4Yi)|suk&hPO%tEbTX)lsKle*xAK9yJn0l*W`lYt%r`Bfh|CYdb
zat98>%S}vJ<H6!Mcs^s37BpX0`Dv*2#x08(+zytfhaMu`^i8>c=JI4LS?wxNQg^}^
zMkR0MDYLNmm>Xq>IQl?~oA(GqVvi3I0+>p_1vpJD3Bza9Etp~QeW*WUSeP;11EkXN
za6{ZS7n4ojFR#Nh8$9`*OGN%`sX?;j9yvR$Z%$Dqx*kJ+`CjS3Z`C@3-=4N8Lvog#
zWA>#xxH4H>CmuqL(sO7HFoR^|RM|Q$rwQ;G$?TIk8Vom{OO_U49?S6b5yj@_T@TWn
zYd^;gf%w&vFFX1W%xz!?yN{W5oL*LKePF6+4a7r;nXV{Ck*j;rDAZGvZ&%QKdv;!$
zZ*N_0X8SDT5vqAVTWB+y@iecCW#A`h6lOnY)tQN;Za4|z=KVr4^o5VEhVONS{2_T1
zWWB1Aew53L)`0#{C}-}8e42Lw<fpdI%B=&;O<ik~0QmC1&S@JouePdx4ai>A{s;Lq
z+m#8-IxJBkd)W02i8CsR{VK5WvxdB#*b7d{8x_ln>klT^66T_;0ZEg%H}2<=BTa>8
zxFN7fF((5X=%8Z?Cgf_g<YcRPq!e!N#E!Fv1LSRMaI!AodT{~~6M~q~c<Pu8$wi6s
z-qKJKT>0%Cbr;%p)ZW0YL@;#A<rnD|Z@kz9N<@j7`cp_jY8>4!jSWhx23LL?IIti4
zR|)MDthyKDbs-1$Q7l9%UMO#r1HTb-x@?YvJaD+lW<2WYf-u6a#>fm?jbp+dkfcrL
zZK_m&fSJ?hcnc2h+?@PjX0+OZxG*C&W|15}o-aw;!M6;OL`H5}uWNAdmzAx{D$2rZ
zEC=I|Z4g^|7HO@b7iLCjV@FCM5eg8jWmQ42Mxu5IHw$8pBSc!$Vlhj6&dp&rMU{AT
z;Dv>3Jn5R5?Tf9M73n{Ws`GD)u5I#Wa|N$|m8c93oQ~OKXG}TkD#gVnNHI#{gr66i
z%c8p?cW_x@9A!f4CT{{mJ>mSN9vtuw<12x<WJ2I!^M4*`d?y?%ju!2qn(?2nRu&}8
zX}tvgY^>nNC1*;{lq=f&!E5vmgp!L_x=x9oF0$p}AN@~w&@}&{w!svMt#n`j@gire
zg5sY~8{)gG?}ul=TpCb<0UI#<CNj*}C>~}q5>fzb%(PGh#>=AX$><JuRsnlAPLSLD
zy9s5yDJQW^G?aRJ*S?d>ZrW~1`CY$~=NN@)_`+B}ljbAHobj0ok|p7LgwL9N9M8Z;
zmt$i@ORsD*W#fS=S{>Jb4!%G1;<BSw+dRrK@B7hsGGNlix9WR^5;XLoujLXH$kemy
zBS$q|5h|#G!B_Z-`ETo4(wp~ml&&T<8dU@LTkniUiKSHQ98RUGYcMcAUNp6)N<FME
zRS=_H`X$iZHA%QO6zkJ5V)c<nNKxihP}cjSjYpNJ-w`6Cw~ujfq6s&DS=WlkF8gJQ
zkKQkv1`04;Vc1q#jCEa(?!(j8_(P4e*+Kb*K+*pg+Ek3HIb6_;>I;O<Bu6JCiF=$<
z9(5s1?wbmAMlC6=x0r`R=DTuP`^v>!n7cp4Em3i(Ls51#qFB^=q*>0VKsq!OQv%8s
z*3(>I!(UElFYbdOQMB0|k+02ttTq{tvBzrISN5aTGh;a5ajE{-%<+)<I}7fFx(-1G
zl2c8?x#M&ioH4hrDf346<8nb)bO2~j$9>ymcA2!9AbFYBZRjeGmku0K$h|Nq8t6~-
zhsCwO&cdpgg2pMG=_ZNRsvz<aJ|eeczVGui?9vJs))EdbB)-1h6)KG8^Ny&$;;%UL
zw~0E3cMIDh34=mTwmp+`7q|0-1TDFitgja0Whk3AyhTRWc_}T6EKbb<W2qa&=u9bf
zS*jjpC8rLmra0c*+Ns@olS+6OOfBr;9rS8Nr%=s{Cd)dBM~f{>G5f;<4XKO2vH7|M
z6~=TjUMNiIm%`A%L8aDi6Wjw_r6U_wv2bx5Vfpxy=}l`Uq7H@`vlBnx#*8j#zTzu>
zDE7<1k&ko9@Xl10ac*nbpk3N5U^<KyKj4mag>AH<a=st|j}0P$3w}CdPF4xyWa`(H
z41hxckwg68KO^I4nbbnZK$1*y2rE<<CHD<|45*U4awh<|n;QQ(vj5(XNt*(m8Xo*_
z$zI0+wXy-!C<Rks70kd@|NnA=aXvCREKx!R=b}QQL6HPuaMk=lrpwDlkx?jPGkBP=
zH5hxZwKmRm3RGqG+Q=NJIo8AFlZnrWZFOHvqyv}CI3dtTNcFLrZdh_xeyli(G93Ky
zs_3-Z1w^!voWMTP$JBAFphO#Lr!8pxmCJU%HiX(>@(;%ez!Yo>$1?!q32=>bogI${
zu;)~w`2s+m0e)n3CxKIO75^8eCjl7i#}Rdqh!n@wlbDy{9-XO)=waaZvQk4n;j#F@
z<+;yELh^}~eNbHSJjR&0hnhgi^X=Lpg&r46sl?KBA=|;$j~nt{_9QKmgN}p<AZi54
zAxegn7BbO$T}5(J>Wu1UdQI?>_aYaQ4eW7WT@=YtDFIz4G77%uN}ne9#!N=KI%jOl
zREEP0{n(R)m;76l3D7w&M<(|5uCyX~b~Qd<9?`+6;_0H72R(?vPo$U4k(T(BsRM@p
zH|?~{y}`9(OVCrIX_H&}-;J$#40ljxYZkqi>;_9>OV3F>;S{Kjj;*3vQQht8MF-Vq
z^9+BU>E*|~D)ki?W0A!IdoA%e-u7?W5%iTKerF$33!aaWOU0muDafL#O33$Zv@j(z
zxU!xXvbUv?>xlUn-5%9rkkG)Zx-|OtY89IkHKyli?T*1scv<N)N|B5<6rei~14x4+
z^_ZxFfh<aF#r!l=O&R$(&klG&qGELqg-{POF>wxQ51#RQ5bIS=;1_f;6)0u9<zP^|
zs7(Y%RAI*A0;>k2@giESmbT>D+1b#z_hO=UX?o~`{J%-wmo6B-Q7hFqYu4?PtjSDU
z@BNV=>4y<nm*ER09)LWdmKQMg`C)k*u{yGZm~AHh-W*TUIA$s!7A)1*4Nga;Lgwb#
zeuWK|7Ur|&17*mj0T(%!)4_8({guvG<9hvJO6XcU)Hm~!htAN2_PoabX3>Ps+gk27
zjq_oSnN37VkqDHZl&g+L?0=4HbIt`(Sr7!i#QqRi<d4M?Tt2UC14_c1Q%!ynb(NoF
zm04Fd2kmjC!vw3-^seUv8zOk4f`L2YiHk|(&(NKeGlU4alpuI^BkYEZiJKP(d0#R|
zImuKu{m*3%^s*2qrcfNwDg^qZ&2sSBs0W9}7K_zmVO1tKuLorWM?>L}A(a>-?wzhd
z21lPm(iMZm^~s{8+Nvb#<mwYI90ZDU5D|&DNQ;uaNg3{juT_GL8l<1v7&?8^Mst#6
zI1Mbb?=6EF!ukw?**Qb5T48hu{$mWU>g4&CZPq=h%Cqz202h3*V_j%5{@QCA4HRju
z135xl4jZ_C8(4{85wn->9eUdi0+muyQ{>QvP$oK>R<7_M^KwZ@7_>*VCdpcL_G5+)
z^zV-oWT1+`BW+!|Hqz3bz^T{=89RGS%U3D>+2&7zTKUzN3M^d_qQ=N?_&L4^k#t_%
zIlk{#13Eo6n~@b&@~El|vFkkWsS(tgIEZ>u5vl@5<=o}cCGi`C1|z<Fh}k1L-xe3|
zMH`}g*6!<}(4_ukUk&Y4r&3vhN_Wu9?OlhT)3QxDnVQ%$BfGkBPKB?^)0V3F*`UJ@
zPW~DTvzXMdO*7(xAnRN+zDDi%Wn80IE7r|eZY;E}M<>6R&GR(7@R35MI`72M2dK&5
zB*uv)byr5RjMd!MCF?Tp`rLK_1Z!Hkmem4DS7hDr51bk&xNz&EkuTZ%kNZZ*{KHo}
z8E#(<myN>aeV!cK*BtU!5yx>KWoV!9)m`ap-DT=F+wr2S<%vpm3B_C!J1+V8KvQBK
zgx`vn<z(z8>}3UKhHjpCF`v`a%G;XKhn$2gJUmZvN!4sSRIr0n*vj0pA^!XLl5Y;Y
zf^_HB&TX3IJwLn;WQBYA4m`y8Ti{^U`>QQ(oG<52fzOBt0ipM33%+?<voo3Q%HPA^
zd`C0X^$SJ+lts<qqEn!!cvDV$fzV(m_x5KWf2pTkjVJiwZ`ShovDEUp%jNig|AgXr
z-Bo;t&k=t%cys-L5*-1Yoz%xxrVBfMq7SU~O`#zZ3msV{7;KbGXuwmpQJa|JqKmfj
z(wcdG!?J*gtdPT@40|J~=R!q#%b>T}bd-#0mA+T**tuh(%g=|c4aC`rpbaQ}a$2LJ
zE&ufLg2P%(D;DhRA2{cQ-h$YsTRIGjTo8y(&5wqMjyQnbQGYhhvv2P2Pno>S$MM=q
z98=nL8OF3a*DMv+ZNT)ohA++h)QMI){{3o*I+neTd@|w*aBU37zfeV$>DN1#6}T^a
z&X;Vrikbt(ytaow`Fnf{1%CwVa=d}gyl>LB95@8#2z1>JokeC3CuZ_u0*#LN3td=C
zyaBB4bZa-Q1%yn^lK-T?o=V@f)iVc6G473yr1MF+#;NAPs=ZcG$1Wc)DMzl$mvEH1
zKa)OX-g~NoS)%sN62G<Nt+`PD(>KJ<5$u&!^tZ2+o9tdVM}w=h(t+oce)GT_BgxwH
zB0q)nZ@%^6ziT0*afPjkdXSG72PKupTN1p`7aWX68$Q=t5dNaeG?;~k*X*F~XJrdB
z&Ww~Je6{bRh3wv?N!C4@VPx^)40z1ERI~*ZrbyO-we`e?y@k&$H=ae7<S|P^?-%Yw
zgj3xV_$qIUBXEemqb<7XH};S)_TQt<h#fQy@O0_3zrw$GnTgVl*P$&F&c}g-95&AF
z6n@j`I#kWJ#M`%}M|=B1dV7`m`bv0vr~U99<IhWEX5aa*nRvcvmfkb@R23y%6$z&*
zLANfh2w`85sN&}$E|Qv`zt5SrWKZj}Q_Z9f<~`f?5mc%W&Hgs)Gj(Vj`=8;2Eoob%
z`A$)b_nJGl3B*kfv35f$sF^N0IJ?@<#r4{I>d8vG@o1BH_t_R7(?;zkQ8aF}aEIkI
zQZKvJ@rFJU4}y29Q?-o^SR*^#R~b7Oe3Y!~#!JN8^&L!<YQbbhPnvS-4yi=@P(-bK
z(Hzp<&y2l!v6yVa%>HF~kmyAAlEgNG><(I+FJ{8|Ttb!Hma;?&FZ{THQZyT?u{o>0
zsxJXBkX`o~UDYH1_E%S1XIsmE=a17^SEmB*hd%&UWOd5^YQKG{{r0E#Iv-!^|1Ixt
zRs46E`_m;MV>RXrB_o~3e;Zw%yF0r@g9ENc{#2#GCb{B8H)ZPTSqK&Jk$WX0?Zs!}
zV-fb7vbea00Fa9)qg1MBf_5ZCrNV6Tk(0s@4kl(4mc)|wqU5GmA0kyEthP!o5*eaX
za7|}1b@k$h=NyA2HI9*GFcY(-HMN`{2O~YHLv{-eZE;v=^P;CPw>ER^9@56U>CE>o
z<mJ5>tsl!uB!)G+e!=v+(no72=M)&9lS!eQU|n;aY%EtLImK53je7CO@5A@>k5q**
z+&cnMLhkRY*sRIU$A$z9n!hv$_Vo{?eZ(zi9iVGdOq^=-?6Wx($dVH?D-kf`C{CB2
zb1cu=y0rM|zcoHTur8a2j!ChqO$40^xv~kB<ou}%hExTXAHlF9Gs;cCmHAkWQ?6(D
zm63?KbJz0(+7nKL+qn6XdM!l9n>AeID)deJcPw7jMeVex8iX3~-BAtTi8<6g384zG
z1u&Y+1ohJ?lP+j_tT570mo!{vSDAX(RdXiX??V5+eu^6YC$up)%S~R8!Z{AaNi2d!
zC}}|^^*B*2Yd54Y%Mp3#dM;eWiWB4xQM)Z!lTiNRn_c^pC7BsNYVp{3g%M{HO`cIC
zSr1%Bp46{ctWy>~aaM$5H>Cne3gpp!%=3-!&qJgWh^GuS4`$xTcX=x$=-7&i@Df{F
zHLd<Gt4`s4#O^3QRrEXvw;zKA0yycc{7bukN<Q~Ba6_9xcYaBG<KORbsx#m3v#S-T
zf9{7b{hMWUM}2Fa2U77nmOwSU4x|xvF9WA91GUy^T-(Qy$lpqUf=R6tLm^{mQx@m$
z<&&oiPc#v&@lI>jQ$g5Av5wbmBHL3Zd&8GN_*w^6nAxtoI#8JAs3DqymkZXEnqcWT
zC_8*KOkbyJveAfG3Tz2iL{{Dk!BQ%1*uK}%7^MFJKma%x$J2v+8~y{`6xnq@kUo{~
zfbe&3x2)&-{P|hFZZCd`nC=oc;tcDmN7euSCI}k;b?JxM%E)_$xg=0&Z#H3++&eb%
zKnETL8hUfN+cf#-bGJ$8w|6uX=D|-~2?{ju{O2LJWMmu2pk@dkBG=5Epv~=`7<cUQ
zwAmC$P_gSP*AVl+%jvGy;@;eXT4(J!FkCawOmcLWN>`-Z?ITqQH`r33B-J+J4Yx&-
zr%Nyb8M>%<n~{!}FK>9_UG;4iuRh1Nid4!vn;G`&e4aLZWneIWej~qrOM1Oq{Fk6r
zktH`5&f1$UnB~qN(dgHh>$iIE0f!Dgtv(bw$Iv$qJwNf=;BSEB*wxlqiiS{i4t0Xy
z?|!XYfPyT*@&^&2>n$5_Kbd6>cr!7!?JWWzmIFSjcKxwU);oXcHEnstXXnozr#Pmq
zvj?xXmS0-SX|4y<^C4>yH3l_Oiz<{+)uM{BsiLU)lu9Y8o<q&`H-9x$ezH4LXID*
zx4T=NDBsg_O6%i_-I@HnrOQA5bF7;&rF_r$r@AJvU=$to6mA8YMRf!(k=G;%nL6A0
zQl^*)MWbMf3Pr|CS{V!{Y4=<kL6Ty(07+ElIxlFeDAR8t!p4?c<#d~2h8=B5bl{49
zo8{kI4gU1y*mx(;IUt*aGa4chCWN;cj4>3j4`w+(34pw>{@zEN0DPrwOmEby#rTeH
zX#6kyqy^F&5kEpo*T^eVkFw*Zm0r6xC?O?Ovj)V6p90)Kfzd+$Q71#RwzW02w6(>_
zlk^Uj%)8T6S;j>POBUZ1q#yn2@t|_`ge02poLM*j4c=XZ(}_tRDvEIQwFPN_IA!Zb
zBir)T$+5`KOqSPJ?!X%FVf+hy;LSW@`;tPl_=E00Tus4?{ib98itzS^=znv8vEQ22
zy;;tH;O_61M^EG7Au$#^JrmUs<Fkb_1bQtHRqURa_$xI6kAx#3iz1R-`W1WdXC7k%
z^TMPg6ADgTSZd<{%l7*anZGKUOR4fi_1k=E0D^)fEBM$Jzr_K5e*T}2Kz7XgpJ<?L
z!$q<6<HPO~IwS-((IKN@7p`QU$#gX(X%B?7)qqgU*ijI985#ze%*#_i)#Rbfgg++l
zGy5Sy*&d*#zWcR5JD!7s`s4oaM4)vfbwLB#_kVGY@>!xgez;mVPxX*xDK0FcMTEO_
z)<+;?z}un~fqZkfec>eoD7rZqVN-c_*DJG2b(nEK^lhLciZoY6$b*TvbA@DvR4g2T
zfBw4Q_I4!NniwiFgDLn4xNrV9v!`|!<URrD$0bXy&ffo(JqP^PSo>-auR;A)4|xLk
zPZB|cq!KFrFT{cZIu5wTNuI~nc|PUC=?MZ#7=_`A->8Jd1*zbQ+$qM4=2Bx_zvmr{
zP)d*AlSuI$+e7YRYcdp02gQ2d_D&U#j%M)Yw0Nb*?{ZgFHC1UqVIeqpC~HyzOV)(O
z1)rcPG=r|RSw6W8<Wmj+h(-cE(Zz4JaDX@n_vR=dm3IIIO}2vWSN+Q>_3sntBVdoL
zPBcIotm1#6#m6(_u1S{MFogp6AiR|I!l(s)8M`M#jz-DSTRjw%9a)r1MCLj;$en_q
zw>FG`6g(h)$p`^$cbH{=5Yk06&}&=wP`ObIl+t@H_zA2;Q~-cBi2h>u_7^JWXEw^H
zm0@naWuas!p3t+PYnan62e_Cq1Blnowx`2|Sh2~788R<Tl%GV!dOr?Mt-TeUtJ2$^
z;!u>(C=o+b!n!(o*s$!@I6SAP2azDiOQoH;BUd#^rMAa9S_7!KaY@UTw#$y-ON9S#
zKVJPftZSX&NbJnU=f~1KQjUw$6o&&_kPtjQc7lmU!==Pvm0#hR)GWpLwyKI*YrjXZ
z5D*!jC>!lBuckH|OWNi~(#XoPlk~)%(I#|?Nk7#My_y@$34(8#Qif-i^z%xE)`pNf
z?9|ZS?D%b4Wq$I*;U?_K`qB`5W?{Qfi`gmZ>?H99ejhBP<9<o?a6~(;AIpOwycs?I
zeSw0;tj}Nzq4pDksWigKyc^JJ?UljHZ#(q(V|N9Pf%xUDs-ViVKs<rHUDszmh1z#K
z<O^V0>qr`0wK9MjO|cZDN>wlwra~Ebw;3j4X*5AluY_c11JAvLyXfoe#|?5WD%b8S
zT)lYPZ}n;?op^HC81*WN_td^no;Z-jpnS-^n}`i7#&{m>KGsFr1^#=JCZ!Bfzdn?!
zMyD#J>563EX0A#ehL;-d$*Zp<h)ly>xlL}ToHY~mM3ajv=@DjsSJ|74jj03ciH%7s
z$H%_LG?u(XzPE;jQlM%}^pZaX4dQXPb?6l3BJR$Q^{vd8iU1IfropiYBpVHA?z|fV
zXzA(HKlbzbd=tzgYaH^?zzdcFSFi%5LKG|ksbUArFGGn+--La%Hn_>(Q+348Qldsb
z^kKi5Vla~hEx?<+zb)MqgtgWTuAmFJ5B7-)lFe7#4JwPumhT6F4(OfDo(AuxZL7Sj
zchWNu0XFpsIUIoU$N{yyb0>h2_0;b-!^eE{$m;5S>3`K={r?}35e8Vgev9(&n;afb
zG=(+6;t1$bWA_r9uW-TtrEq~ECk7zC9h1=la;M#wdHrJNz8>i_;_5sXwR~S}(l;`d
z`8N3IFRf&ZVn;OF)ckjG%ecKQ8!s%wVqJ>`wTMhukTKuTM|cv=*-%y_-D@vkbY9)t
z##}|o$aK$4G;1GAep7G#a~XTxX#t07`=4Pn09*db?|uTs-zQo}GJx8n!T$nH_q%f3
z#k<|Pr{A0mWRlL66H9?+t*Zyy0MOF;XzR`@HC{hN<YJtnl4iG8#=Zl+Jc#foM+I})
zJwPB>*F&=jw+9*YGHyt}1rc(A(A((quag`4;NYG04BfG<)zxfucJ`=eIwI=VewaVE
z)V(dM-RoDw`EsVcYa^6MLw>m++ktME9Tf8pQh)T!@S;<S{jgaJUO5OJ??*8&W=!TG
z(5nR}wY}#rGL$WI)Uc7z>iy93k($S;XRuoZfCK9j2r=o&xkUuSBN>3tPlXkubObu<
zl?4*!_Ug{aH3jM2q7FQN<*|1kS`pkaM|HMaq$^6tN^ObF*Z70&`L$4)5=M33TnX+b
z$&s=u+c+mlQ-a(F5pK_1D4TNh5Q>U2pCW?|-iUh@h7SWB4;&{NepSgcp_HZaByiXO
zoU<qf6|VJOkAI$F353%0@7uosr~ZVdA9*^>><r|uN90(wJEQ(P*^7>PK|9lfhH;7J
zAdnJ6bt@6Q{Kygc4QYdY*Z6F1S_E+cmA)<heDud?t*p+^1uYnQ5C*hHTL)QB-M;ND
zo5E|ieH=o3_rlWERl81{{sj0A(L^aQXq(JIcKmv?FaCm=@rY1Z=odW{;k<ShR#*mA
z9W|j%^uEQ;z`oVf8uWE`BdlRJ6HxxipMUQE_=(2^32BI20^^i|CP2!2Q{l`ZVwS^Q
zoBya3;dnKXIu*XUF@t^2PKQi`NvzC=;v5%}BeQ-KaMA|K7M*~mE?{W_O`mEugu&mA
zM@Nicu=$Rx4X1TW>t2_vtPfd&G(p(dB|OC^t*~4<rwsr3GXaB=v=Uo76r*(?+e$qU
z@U{qqJ|Gl8!rAqYY$DJI?^HZt)t6C}ixYPEK-wQ;v+~IO;>F08e(~at$gc8Z?0YRG
zh$5F`c22%Kw)DSJTs-PcYDdjL#}HkfB<x^m7SnS-NI(~iM1!K$d`vAoE6kRJ&;)Jl
zjy&e6@`9T?>^5ubnN3+GyU2w5y2+(iLyNYAIsW@S92lVo;bUwpdE)rYbzvBd&>td9
znJ7VGLN;gv^X#;)BKT(cKWmxnV(h!^6p<Oh6FJpVQ(F-crmrljK(UGDNaH`0Jh8k|
z^qsSF5|N%FshBlg6IJE%l+~Z_$lUg>QV$fp(G_p(0nJnwVuHeR2`)tuaja-%Vy-4h
z;#mJm)n)2c`53vlE>u5Co73SnV19dPd(HCk8%?=(QB3BQ+&R}ILMKrlb-dAHu~R4%
z`TtPRh;&}^SLrat%m~C$-aKd1MB(q)AbJ_bou&&AKX@ImrKv4ACH+PQI%0IUm_J@1
z#vy#Tl2HtavWomE(-?7-A7wvavT6~Z4?^3{dx7=pp^JPMXrB2{k@?d=Xa2(0CR|_}
ze640LMl#b`6*WCA1gW(~gMz0>X|l#R%l3dKruAv`+iJEkVYWgEliwbi+WNG&8DVC_
znbAS}%D>*65!*19`+r)e7IWwG5x!^$OIDJ9xRM(m|7Uj6{(sC)G0f~M41I*vwi2<x
zBhdowlfMI=I>FBN)3sO!R)ZoQ+A4?`WCh~!l)0K-cYb^hla&QYAk4Sd27YQFq<@F)
z2YuZ8pX^-w@MlD5?$^$P<C}!DjzU_(Dy;8iT6MHlRxxgaC@e>~6E&m+s<h<FO#n>T
z#gAmYQ2R+gT^?jiSgyVX8_hOY<>!~Tx?j%>8-hD7aGEA1>xfp-`o~Uo^T<aNe?t>N
z(dRtK+I48(Qi`MNMv<wG3n>pJBf3K9t~TYe?oF~IZI;U`Ysrvl|MuQx_s0;&Wko@V
zDp`Z$6P8qSN6!O)d}*nMt2hW_@BtrE=8=h(a(}^vj;FCR-&`o_?uLPeGnq#4V=dcm
zw|7Qy&hVBr&B0%6zp_8z;RBLnm`3-uK{+r()#IHIE)hEfKZ%Mlt){({;NMPrD(r_s
z;Afgi)C|_D_ZuV#I@2tI6no1y$WL-CX?4nOl@#6v^7t5NcGYXduKuC_RZ|FaL6dyj
zpNL){mhiv;;NJsIM{_zv(X}qX!pHxNj3Qsze}F`*=;CO#e(1&c{Mq}5@Sy()mmPt9
zUW9`zHyJEW*`RlKu7~^vKBizye9*y94gtqttyNDH+h#TH&9l$%`RG#HxTtA>n#WUC
zo6|<KgD^oNfwf4aP|oY5oPk{8HWe3RyQDjE9SU@D(Xebw(Brc_(sNe3TMk1Nstl<u
zOx<I;#>MHAtRshDsiV`Fd1m#A%Nt7HpDGag{I+uL1Rhgb)`?tktYT`sCghSAvsgh7
z^o+Q&%4svNWWtmQUs)v#iqK^|8uB2?e9!>i1lj_v8Rr6$jKxg+t5ClDH<!)u)>5>q
zmPkkCPu)RwwS&*%!kQnWw<Iu-G@==BtN^COrW;TqW3YTw?&y;puVlG#Dm(uP4EV}O
zH<l$#NF;R?u+Y``wN=(YVTL@Pk%bUQ2eu9Go+b%+U>j27B$I+Sw><q*rQ<(_#Sdf_
zHrFCvm(S=N%deo$BL>!KSCz2nUnyX3tN)X^IFswo^mwzi^w52WE*fmbENo@J3Uz;#
zx%&8ce5>|zTH*CuxpUtR2Xl1S2D95Jm+(ohTc|CD7zV%_L?2Leoy{FqYs?1kazKXf
za*WsJ4yQxj0%)qnV=IW{a=)ZT#38Sm%&;P;Y;?F_!5lxV@1E9$oCDP5&N0xha>g3Y
z|NglZEqome6g*3jV!w<e8#y3e*(K+V{H)+Qo0|55stwLP&X4cnd>=mMm`%QOXG{X{
zSrU-c4k;T{@QF~ina2GBNp4GB!_VCaU7s4Y?}`d4WX7Yr!1hyDi7`kg(CYWZdu8g0
z>%juIo2}0lh@8H=5{;%9Zp<e6#p$L0Yr1I)NYg8A_zXqmpOr}<{{4WRv^#5D5YdY$
zx$sfnqP*14L2X(tiZe_7p=FB<G*gK3ux@6Q%M>D=<@)A)6QjBuBg9cUhDiW{-|)7-
zTvDlvY9`7#W}6=Q^1$p@0P|9Xt1IQoSEQzTopQ#eu>it0y-3NJX2ee_CBtq02WPt?
zJ}Ac80TN+uf-iK%z+4D3@#h{}j$UGs&<g`$yvdW(Lma&9Mc^@@sqEzneTtJGFEi|Z
zJH3O&g9R<pWch64A3s6otkUzoRLl}$B4^*DLaOk0!OQXV@o;i60s^w%fIl7+?ccwf
zW)7&ig|TOpAwv2BqhBq4%({YU?e0j4FI9^CeXtTtc9*OJ(q8X>cXo`UoWzY58m_%S
zT7IM!1d?G$#+ggm{e6F~)cOg0-gZyP6q`fS>$_l;UlYQ}C+wm>u}DxSaizbK4!)7w
z!@nbO6#VLEK^>rR1c~vK?+da#mIa0}VE9pI)GHSy>6jp@9$YTm3FPK65dtahO)s>=
z?p7>2rGS*1Wt=NPCn6`un9(K7vw?SaTdskX2&`5b@aLTf!Qx)VsCU<sTL1Y~<M(jx
z{B+{`tXthRDUs#$>zxjoL_XogI750c^ga|$D#2~r3SQuqTCG!oq6Uk!Jc$`nQM}{}
z>VKdLLcb$iTeu1`Y6&tb<CzYF=D^u{iXb&=MSbyZEyS)5mc<TJx)SLDk^B|rmit^M
zWEAD>VKVRlZGggZL}OWuTAFY`4W~R<W1eK4f&nU0#z`nCQ&)j-#H5G`kk5mw7j&q0
zMMK%T%kGnx$v8Y<=*nx;(CPNCK3!34+G2CxbP#;&qi9T=xBMxqg-PqmrW-?T+WGC;
zHqA1M{vk>~r+0&^pQGIdfBWjS$FMnh(nMTM4q6>xc5l|osO<PL%k)h&+o1Iy810O%
z9kOPbR#%#PdTSK>^Zrva)Pgmdb`P<eXx<0w!XU87Qt&~MchD`UmA6lv!l&$YQ)ti<
z$Y&wV4nMvV)_O=8HMN*Ol(y=m_`4JBM=u3NkXem{7^)mlVmBXaf`Bnue3n&&Xx9^h
z%o3TN*wt)swkyS~t8Q{R;CNqq&bA19tR6lz9ID}w8YHylytffe?h^VQR6IRddnBI0
zV#Dp%eBFun@y?iT4pO;ygLlNHd0zQ}z%=s(g0$d^<gupv)^^sTt17bTbPXG8I&r1i
zXtu_q?mT4`Cd7)0zWQvv3cHqNwpVR?`8=MLd#wJ}vHLmLLx%cNhDhto)X@{O9$$jx
zy=-~g?$%YScV244aqfC<a?Yc6c5t#9z2+zceZIQrcpd&qr0wHvK+xiMWv0J-LBG5(
z+W;#7uXX1=A4udxR<bB4c46{)%EQ=#6jqTM8DgBkZn|fIjqc3HD^($sex=d}9Xg|`
znN+8|cHUC$IS~&+NcxXqb=xZ=*7jHf-M;oPbxeDR2E<oo$AK}pxXXWi>y2cLsTr<g
zri5(W@^4P?eyseq3<PqtVW-$`;keuc9KOt{)u}pq_qU>Tq;2oz(Adhfk*oKg>^C9i
z9XkDmlch<a9HPL_#oY~N%<3MY>Yd-Z4-p!707^WH3NJ+Gz43)(yj=N&d+yy47dqN*
zk(D1JodssftxbVv+L(*}xG6B3AGn1C@%`i6OmyK}AxvW_Hrl~PUj)&qJ|H{5@uvAH
zH6)g|HEFJ6X0n&WqTID1Ixhk;w|A|PmoM)V1n2qz*$?I^LhQaWI{i(69nt3m*8H`u
zLG92g*1<K|!L@($61|Z6U)N$cXCR6M@w{4+vMg9|`-==WiSLueH1RY<xZL1oQ(@}_
zSpH!|*K$ZIumf*vHWURXXjr6fn3KSnrxnIbGIx$?%|9ndZag_^qKFPTvE^(PqYr=f
z!3(l-AIFIY8JkE5KFUp_`a`}o_$jX88$+W!Y|IwV(}d7VB$p{$6CJpYx7cWsNvFqG
zk2z!4eM???e*L-T@o7(=bDT7!2WX<AKAYDkq${uO@@{D=ZB(~_e{5>}WU?pAn6=cY
zkvV109tPA4ZwH~4`B){S`enHLByjY{MA8dPjAX+ePFOkeq=?3NGF3!<k)>0(xS@Yn
zN{ox+)y2@r!-AR=!HN!%x4V|8xL_`^cj+M*85VDf63BueEO%A<)XMSKQkCMs8%+MV
z6D8(9hD(rZ$WX22%9xoteRVnl6mnCxfm^{}JDk}dJmC)swxE*K;oXO)(0zED%bS5^
zy!rbAIO6e7y)_eJJFi0$>CP_Yd<UHd8+I9+PHX#qkO5aO`|lOd@fEp?x;s43%GEz(
z3NgQ$Id>dMn1zkgvr>s?U>m7;!vwJ|In0;p=1;j7x#Lb&T^0#R-J}|As&v~beSzQC
zzPuL(VaGU%?3YyrS#6cj-B_kP0sJ(D`mg=?m?`w@w{uMLJq*GT7OW^(s>2l#=LW$Z
z#9`l5lIQI~F2Aa@9$Hdn{3?YqWM7$T7ek8}(0bK0tWp=<N4cbr?u%ZDVa)4r!4F?1
z(2UTa{_)7q@_$s8+jh~!KRud`nh#o{BGT6Qasrx2Yy`wK<sPNH!UikzRZ6#xnoU1T
zIjte0LIU-&!R&p#_~v#<o?2PJX@MK(fp&37D<SAu>BxONH^`W$N;z*klFM&+Mi1o`
ziXXlxlhAtr(#4eTJb67l(AqR{EHZYP8peZ3ULv|(kZRLA!Z^n&ATsS#s&i25mEDe>
zR?SWEx|dB|{F=T9snHjrjZ#?Fv>}bJ6{52GMn-eC72BUTi08v4aL5&-6bxJ_bkcR7
zcZb1rT`PHq!j`j{FVa?5am`t+%rgD!Huu}^%F-|CzNPgGzvF#VUAYVWQ)hhu@1<9i
zAv|5heigIi9umj%YQ;`jjDUaSgPSHJFk~M2RT;&^UU*ljl&<OfzK{LM9kf^jaRR8~
zH#Y=s4!dp$hzN%86G#)WBm?jLYUnt2-fQ)bv4-XX_u>Nu5ND3`mchI$OThaW8!|U_
zu&LCYj4P#(BnulG*=KWakl_MDeV&$g<KF-1OTjh2Ub9ontA}*`&b!y<-p)Wc^&?S1
zEnj&elOEw)`IB>w_H2B#@3wgj_P-bG`Yze~?Opq=-I>E}$AG!K$Ccm&Jy=gs&gHM)
z!(ZjEXYIO4b^*A^KTp|b(`H`>vwqFi+pv^Zxex8EG9P->_HuLJ+{}C)t{vMj+`Gr>
zPpRqDtmDQlx)NClIBOD^6oW_#oYhHVl<My}zB2)-DT#gZ_4UGsWdcB6P^-n&hLUs3
z6Iuz@aDsB;OV8;+0nL`^$b}ZDEQmwf(4A_X31&;UwE~Vip>k?ZCos8$D&|n0`or(?
zUJt3n_MF4^9$^GF>5A4KKzM!z9ZWtJeBjqU-6Zixjg^7jjIWi&#%)Jg0YEJ&IdqF%
zkjH-zq=(0Q3WG&q_+5#Q1s&=Y&Ck;d%AV>lH<E4)A|`8$Js`~_eQv4}JV#DLsNNYN
zX}tBNQxZe%J&<b4IPU4YBtIvj&B#CHlxuWDA1HCl3Mbbs<qbg7Vq)F-ZR+-NOXt&@
zIX2wq)4FDf4%-w*t^^K@5_hL<6d#uzuPyp}jcrXY4iE1No%?FoPM}5JR^<$XoZsh!
zZ+kj$S!wya_y`e5#TnDBD*Nh%IqSrSA=qjPQhO&6D3zdwnCzQMm33K7*sHd!^CSNS
zNN0kAh4aiajJ^b7B1*HB(8qtK{f6g`n2gKplyFeN1ef3z>9~*5CPgJ{%qhxGH||T)
z+4d*@w9+~3cKZu7XO1d~cDiDmV(-3Iuff-aa`uP??@lmHCJKh3o828~Nhlanl~p}S
zh+;~cx}fk{?-`KdCPpy_L7^_Eb4>IUaJZXwy0TQ7FJ5{|C0W*-;AABzSl=4vNaitZ
zVL$S)=qYkG6*_`dIK6fJ9D$KN%X1aAWmfKTTDi%nv(tL~Lq04!Ly6%CFzr?H8BG^u
zKB6bz!IGXjE;U$l-6Jg9*oCM>^Q=%UmNnC3ffIT!!whv2ScGujyr8A}QAy@|ci@j3
z*TMrJr8NHm2;OBr^Hb&NXNH$}<*V1d-m6#x*PYm=eYCu@UsHQ$yQ1yykNFk3uOA{#
zxVQS*-1J_}_4R9h2jmXCo!$gamAk=!tF^H^0)SWdMi*@1sN*A(XNrS*3|B4*U5={6
z(DK24LPH3+Zt8{hkMaaa^CWNQ<czubQFk}Gap#?=Co;W2Szas~Z{wZZtNz{M#M<Zn
zKf_B(uY{9f4XiQy%9H!@f8}Y%E=l%OI<Q=+%=fC%oq}^GEBTS_%uGJUup_<6sKe^8
zzI-52ynC3LLjDZj!Wy|=R6LCuB}RmffzU=Tvrt7|1I1^|#u&?8b@+lFrcWbKG=P3c
z3d^xKyr<L0+i$GB@2FdxmW;yHsm;~0epzbEJS{z&8CfUt@HSgu#Vkj8`#_qfTCO`G
zQlu6Jbl@1^06;>9H(srp_c4;uDso=*aQ3#D+Ui!D#%s^)uHV_y*(Al&hMeVLPnUzI
zO6X8G>=Sw<JX>rEq*g-kmowR-$x|R-6e(t75~W>LwKH?aK&Lyx-v*mmi#>tve>ssq
zGt+n7-1}W=qHkDf^ivW@X@f_6yal66QGEb>OizF5f4OCQF#lx>5VGWtnxm)sf7-om
zIBR|4CE~?OVEiOb;S1HL#D7uZ=1I7RDJ^7Y(kpbbKtL$F-+=%;bn{VOKYZ3f-An07
z_crIFneK_BJux0^b9#IGw%U2ywcfugeH*qvD`S`=Lc{YFzaI%ZP|sMSNJ{JQP(F9)
z<c$&U;6<K}nSHcPR+a0c8!|H$NT<5&qY~LCiEW*nOJ@5&JJ||NNe=SkvUZiZXVBEX
zyOmEhMtc*ZFFMgQJH9Kw@SLKSt!p?In$8?h^Hsd~VzzepH+Wh&^D&HIykU|P{zfUy
zjYq7v4G88NZ1=!B%}~1m{}!ZFbeWl$8-Y+g`Z5!@`oW*TB)~IimwDY8bv2r@|ElG%
z@0E=QPg1U*0d0?4v_8fp=c<<H*xc7e1m%o!P~@)JP~_cgnH$lSgfWf64KdOov&CiM
z1(Qbrmn?eZ=a5^`bgC+_MgDP;L0gn-O_R^a?$J77O3my-UOw+~J5ey6Q?;_BLXq0j
zSVc<XO>rd0Hs_9hx2VCM%-tNL#%?JKfXscZCNZS#F{3YpOAsQLrvg87rA%fnq?iUI
z)aYuWg(`RDM-&42Bjp>w)iF_wQVL72*2{ePu=fxziCBRc{rkx);t%J;r3v>fyLM=5
z6gJH**8m@h42Ij!6D?h1KkD?+d=l=yE&u(e{xPyl6zHF|JHO0?g5+?$7lOPqd)7~u
z-6G!xZWZ2dgVz$b&C^y|GDzAn8QEj0#WOqS=YaJ>KV+1nzourTawb4o9wi9DkrY*d
z90E672#rljaknAhv&mlpDe)!u{wGXld{#rI-i_mWa9Y0Hb7jsv5O*U3tN_DclJgs%
zeWG>|N6hyddYUITNRc|Iqx|+@rV@IpBXlo|<@0p?U}<YPWm9_oT>>r3&jI^&?{8cb
zu=XFl327s42mLjd{G8cXNMx*`MjCP+Z6mo?jO$`1T79TqUQTE?@pX-x;BSEWy&le%
zWL(&y&zZ)|Bmcd&HY}0j3-&QDb2=L|1c--wa%}uOAv|Ma0_yAp`{w)it4eyG!n_z}
z^PWi#e3IFx@~)nqo|<0zfoX<rKMcj7Xw=fT&L^_$R<hQV)Y_M7WiF7SE%fML8V4v<
zd2r~PzYuoVq&S9uCk=IK5c$(T#8nf=VrO!?Bro@5(3fR53zJ3J9|or#lkh_`ObSb4
zrC`xV{#as7@J6!F7pRp~G)MRW%oHc+VY>)fAa>xINcTM|7N!DQeeuYN7c+@ZXK4@i
z&Mrke<wyo}ru^IFWLCxs+F3NybFEo7!3J}Z_>>0YF0HLwDkY?-KDEX3oa$-g)%uaq
z4Q_t?;~b-*oR&@^fq0cO`xsSbNwdn@;fHqmaq~GZB`^}1=|+oP8u5OdA)uTb*sv9f
z@h&=mW4HMi0l)48r%Z1;@}WKw*B?#-)P%b}F?+P}nM<6y&7^a(G!qQ@*+$Cchji1d
zl`AyPW;rPUYO}l<)bT=XG0&P!7&K+|F8v$NB$L34V-j|;%VyfM=X#0r-h9(3`s2~T
z#gDy9rTYaxS9kOX`Ldtje1lu?b;V$1w@d^SOmi4Ijibewx%ZQJvL2Lb7dP+oQ4AGo
z^-wfeN)nF>T^6<5(Zc<3Vu_9Pc+|-i(NZh~*SS;L*ov&J(By6hyL2(g`Q00OkOo{h
z46dRZRTiNXg)!E+OeuQ|MDdehsp?l&%@uhVgAh;iXB}-A{qhG!dHdk$&5C)mBpI2u
zCw|jYXf|RfXMLm;F+OsdI-8`bHU^fO*uU)h&^7~SH@f`9O4@a?al3G99DL(?<9FUz
zY0{T@xYA!}tSaPagW(aNeX)D-`%;7C)(9?dla`mb*IjQ>EMbrw<9Ty(xz!6)%Tdx2
z2$Wrz2{h9K3CHMoW00UVicQ>bO&~{W7KD;xY-V~Gz)tPKrFUtAGEwK}IUyKAt$S9=
zg(CFCqo+l;9;^}k_y)w&>qmX?cSzHG7z)io^}BZ4lQ%)ZQ5+R8@B<r(*vRFqZcm&S
z+{uS>7!2hHaONSF2)?!!7CB~MvqmU*<W`+j?bGVX?AoR8d+e3|5&w2a59?rSbJF^s
za1JarkWJ?cYgQ?nq%mt;y2GBI@eCc3igaQaU0Fb6V^s<mwp`T2%o@FHvTbh*uA3S}
zCF=QDLrq}}P3p`;-p#hLaGT~cd++#+YCx9=qACdKyCj@gZ-!9m{xa(98A~@xwxJo9
zg^<GiZ8yoTXsX^dl+DA_=f!Xnb>pcWbBn{Jds0|%3nhs4u{s1^hJPkCn?`u~TAfdc
z)6k98M33E@5OT;TRK3|fa2mEj05YhxBIWeQ^iYwp^f*3b>>&3ANd#}nrviyrLm>C1
z-;I!$Wnu{m{?Q&nZO&mDQi9K%Ns62Gf4K`QX*X)!Ll4j%Spn7kd9X>LSKa&bWD}MY
zo1n+dHo`7+N`{m#g3sR>+#gduxz6DcbTy5LFCymeYAjLE_q`A~{eCqz*E7$Hx{sd`
zzO>)kJ(R~x>;<l!OHrBLFk3e#qcl)`W2B5LbZ(cj=LEyv{cKJ!N?D{KLbQ#3(tGr_
z-bHv7#Y;>R&QioF#x>-vKSa8WPrIIn!1uOjC#fBuh>|&9QWMKv`Sh^&;7784HWRZr
zzs!zna2OXF4^bu0I5RM(PowD*$Bk*6Ihr~s=%ws<b%KFOR~#5=t$5226=!alO#=7V
z^6k)Jg%Mv5!F<wnxVl(y9@xB9k}nZQlKDiKvLayNLXEy*l;QHOO7CrwRP7<645M`&
zUL=QJo#bTpmPNC8lv=C83C97Q;uuq4=^bEcbcl%dE0pSjX+Z0`sBFrGHDC5)@nOZP
zGlntk!PFi@cr&mk&D1acMR54_5Ao^edZ0fxmhE%Lg`X#P^=@kFjG$|Gwe=-+_2nft
zR=}kz+uC|p0L=fL@8@{XW##<-grJNFKtDnM00Qovxg&pEea&id!y23_AHMpMzDx--
zH<o@0srqsY%E1Zc32Ox9i?jnxLP99RH&~3*My{_0t9z`uh_UPtMV_WR?h)Hs5iRP{
zh_(}FenA21QC9?9Tu+-NOlyo8|3KK+Gir8#B}l@nu>{rp0{sK-_77Ad8%DmQ*hYYu
zOmGO}Kkx9zpshjdFRtMon4%1qTnmO!^UJ6k5!Q%IpvFY;4C0ezLs`@^sWjDL##Oba
zuzCq9yr|&o2T@GlK};#1|0KIA9BMS3;Nd}e@KgGD{e1A=qCs%JS9y7F@l(HDq8|i9
zBk?nUg5o%P^3SX+zaf2)Qq(3?L#T##IS)gj^!@1I;oye>)5OQ`;K$<LrdET$5B75q
z%92zs|4l9gMZJPT-2Cg7AU#Q9>|u8iRbPo(kR|FM!^!{vhK5FXK)H(uO&xDpT+VJ<
z0}%&0+}H`3m=9GpU$BnuRRrW!7Hk^NeEq+6iczraA2%A!d*MolX9OtfJV+%l+ne$9
zh2LKX-n_OTjAPQ#qk1o)1+7ar9+}-$c@8N3{1<ePEhUG)Jt|Ce;DRO3F<+?naO5Tq
zJTUen3LyGmV|a*W73sqTlKbEsL~Oy!j?PHQeYU*57zG?3f}tZR-Bba+;W<fTeoJls
z;ZavYGZ=WMUxoEP{rtM|;YjP_bETaG@C_171dC?S+7TyA)IK(7hXD`}Q%0K^&@tkH
z*`PXEKqZdCI+41T68AU%)l+PfC<MDBJB2YmZlm_SEmFehMDdINV139K|I4X%;12j}
zH6g;ppG;4hxXCQX$uSogR|qLA@@<!Zk3gFSgcxmI>}`4^0w($@1XkeB7ssR)9s$g9
z5F4XPhf8j3XoEE95HCv1JkTAHM#p1eZp#NtBOxUqVk{Ye*PB=_<pzbe&s7J^EgP@0
zQKPaIJ>h4>17wK5hqoXJPS&R1_AcfD`;;Oc6Zfot2sg(pmrPn=MOp-0V#}(fqB_5j
zFc#3Lfy?9og!`!(3?zW`iYPd@x{VG8ejEXgYjwj@Y`>Fa;y*201`YZ5^Kod1^rnA!
z8i6%f7<*T|%);6z5OeK5q_Zyffl(0gS@bRiAa;zH(QR(zz!yv3POu9EEjz9Nd0Y(`
z6(3)&mY8?1D`%u&FjJ|>;MGCM$5{QA;Islg?LaVU90uGLqwS);FmuG`0ife1H6Bwf
z%sqv46UGV46&OS|7}f&&|5?Yu{{N_Wr|8O}cF{Ig#kOtRwr$(4*ha;v*tU&|om6bw
z_FA{@-e;fvpO-%R!+4l4qm8f6Hrs4GsiCnWYFhm*=T&BdkE*ZlREhs{K-h~fw{~mg
z{9soRO=qjALt@Ocd%MR|W7^gv9hqwQ<B@UpgG1|l^&T?<p!G18wkdSbxFA$?j?Zk|
zr;LQ1Rt_rWC6<A2D=U79;Z4V>FLGsw#J?WY2{AXxf7cMr6Lb`ds{6MWWipx9FxO1r
z4xnkMG{<#e4`OxU*W2PoMkSYtZ35W8%V41n)2E~787>;{)(raC44fT3UukCd7ldO3
z4?0_C^+OU2_!@WW@Ck*xtG*@%bMGz$G2<2J>iUS45T-Wyip5~EkkEAJ#p!}%yU@g)
zlr15t(WFZ9U<Kvan1Q?8(?@kY{I=M~J<C>V<7UzS%QV*U1C1=^tW+0_H>op7^@p|`
z=$tVNC41I1tA3?O)iPD+?u8oH>qiF$Wz(N_lKH$^Jpi*9KTMxCv@ps&VFv=E5bCn}
zT_mlYc~|x#T-q%v4<R`smP!~Y5wihtS0;;Ly^I*d@Ruonhlkt5qprsK<ZQTrXvg19
zDVW|Mie*G)2S#CIk6Y-PulFj5bekGs<Jv7G?QyLR<Ma8PR1fULD46GF8}^=DE0?ry
z2+$tgVDYFZaawOIUSeVnVMmvQ30H#XN?>aY`K0_nIHk6vX%3I@odvllvnHQgkcu!B
z4aj5A8OfLg2aJU^zRP;pFZ9T??9LWNzD|$>R!)*gEJRaj15gB!Yq}t+AREmb7$rl#
zte6YFABoohZ{F}?+*$jWpME%lbA$Y6*26!_ZdR*V8O-fs@yRM>dSnuK45=?vI~q;C
z5?ai$H3V2e5qHK4;ra}sS0{heDVcezdcenQHvh%MaRx1m1nbP1-}A~b8^o&%5L%_x
zo5m^s$}SI_eF^!lJ9cDWfOIh<TNPo)Q|>M~j3Xm-@9Q$`nbOEl&`TEpb@P}@`7ZfL
zpENB~gtpMmcjd!h?<K%5X+k%=guUcwnK8nRTcr2`^Vpv+II3{rvmLVY4wa2nc8o)T
z@{8;R&3UyrM%bfV9+fdP$aiZX4Lw@rS2Xk1fq)WmSiuT5k*M)^k)SP#QBl@9ejT&f
z%9-E#!|+$cM`X@5C&n$f)ukWo;FH`9{&-+#_Dxg7vRh{s6yf8Ij9O`2+a$nvd`4+Q
zGmj{5kY>70Mx#_6MFqgH<nP<^I&}XoqBLGrtyCp*=CZrD0p&va#ak4Ul7!%h<X@jg
ztsrSeF0dNWg6<#IELd4VSa>F29}efAHXb^kaU+SaggGMTJ1t#kx1H8x$VPvk<3DLO
zdey4$SV(`xZ%t-pMvvafTVTn7&CFFKLbWmpiM|Xp$L&oVYd&f=;pW-?yx+riK!)e0
zo$G7T<#c^%f}f$L8gvCum<^7mt)IWtVtP@8?SbR1g4bFtX9Lmc|B<?4Sv*J(G_a#}
zS{S>Q50@5`g3?sAMVz)jE}Qzt$4C9iNPM7m|Jz~UFNDEQ)xIg(UwrFHFb_9d_79QH
zscePo<}3%9Bew3paLQl!{qkptB$zXtp+e483jNqF6AA;1L>ODnpEPrOD6>%v!y9zj
zPnRt>Wtl{OIc<T;B{}3s0-2vP0slH-hg#_BQRX#JmEGB+xe1^3C=`>X!Hd0C*oTJY
zyeO48N*5b5oS9DXx69!j!sECq#rmi<`3P#-EKN2_9;<6gxWr?$5~U>il#*Bf;+?qS
z2w;Ub?Zb^jhX)TzCOOzc`_oa|ifWr&Sk8U6%{tt)eZ)w1R%`5H-%|M;Lu;9y`_JzA
zj*%fzgO3SAJ{smA2mGFR2}|$-Vw4wXm_QC}oNIy_y@uu#ieeB}p8Nux(B9_!o6(;Q
zY4Sj|)TQxOex&^=ZVR$;C0+|XWvHbz+-jaD*FNW(B^yxApP_Y`RWj3>RMS4goah^v
zzo$FL-MXxK6mVmk#58_N61#vM&`BUQeo-u>gUko;cO~0=v5KKhV8%TKh+^qOe)JaS
zF7~Tde6)|1lc($89m?>KQNmW7L((;<&ob-ylAn9?H$c2yxLv(oLxZf+Z82(rFr#}J
zcyBVL{#ahNu~Y{Is|;Id6#aq8pF8@e(g!)-k>HV#Gd&vKL+XZ^HpzVM#GpO^Iv$M!
zFX=oX*y`j8(Q^6$&PNOreN7;z^HA2#e$1HoPV-b14*Sl(OcNvx6j}=U6c=6QWSquO
z{6<A)ZjEARHK!qIrD>$-t*b*rbqJnF=J|*x6p3~f^8&*Fk4U|)mR%xtmPV@+5@lx0
zR+OL44*Yh$fiNIF?HIjGIkPV{jnWL4IxgSB;ywgr>cC28z4?(4_ksSO=~VjZlrSAD
z<L5D>N0wS38JhaoQp+1n$`ZZI!dQOMGx!ou5~|bD{VkoKCC%N!=iph$Qp;al(6Aku
zRiMmn%$r@D)tgj3VHc~MqHQ%Tp-XW%lS<QNMnkCNoaR?Z(``h%9=EJ1*Vs_Ot_et8
zW=+;I79bOmD$ODtX{wnJnzFt4p;1vDpk5I!M|QEd`haUb+#dDsNmY3NG`;C)Vsf+4
z#^#qkoho1XeW(WO=H?x)kE6%-Wh}Fp-%7mq)+Cq%3uE4Nb-~#%ja}a88KxEv8rl8n
z7?ea>8o2{#82taB69`qO`qTXAr)OYj8Zi9{&@~hb4xlxzzvxm2c1H$keDxP0O|%4J
zjn&HnjB=k_6E1<R!Hg!Yz}2Dmz;g;l=dX==f_~ev`i=W)&#$WgQlQ}&6LE2c!q8BY
zfZU;p-pTiZq6zfm0vr|>{014XvCA^!VfFgbhO*ZS(&M{_ico@>mPvq6%j#cP3m&B_
z3G)xBOr6+5Ks@5NB+PJ}pCr(lf@fq6HXytl9v=sHh1<mx%yc6XBrzF_PEc`Hod73E
zjN{xr5VQ1kz8MB28S|-nNW--GIHkbJvv4!p`2!J2JP39sY)icZQj%sgM_2(#9Xn6;
zNu8VH6(TXKQq?*RHna)Ro)6nY%|dw|5BBVm`&b?9OaY^>JQU5FF|H^8gCfA2I)aMi
zB(S+hM+C45od9SyA4q;9y3DozN}RPVPyg@#nRKLs+Tk?(C#K8?ytmRjrMNG@HFxaT
ze5K2f^9Cg&&~HEgK*de+I2)twL`of|!W6<qW;lFtpAyIn2yn|gvPia{=8lWDYLIhT
z84~>E9JgUPW$_@TxXUv-p}eI&P)go}SOEac?*PmYw#7I!zc@{QZ?ZHVJ;2)2vs+Df
z776fe|Lpc^aqIDQegfZ#tvr&|`=O~Wb`>Lez(2N5;<6a^dDepg=;?dS6oUUA{c<XG
z=8P&ocbKg{Gxa<kcQD)M<n$DEK+Tt)FgQ3O_--;(QxnHM-^5tQ#-mJZ2?d=iuk$O{
zDlD|Dv=CcU2PI0PZ-GsAV9C9p0dHU!BUg>`4wI(qIpt#K`}sbc1EDkU`Zi=tm>|ZP
zAeGqyF=ewqN}7CJPnu4(kxvN*;G~LA*{3}}R(L}}Gu(eIi&^=sdVSm^@{O#2ZBw6w
z6U~D1vx7pgzT?sOBK!7-6NsY3;?}GkCYik<Z>zr0JouFqiKuG?B_nD%heCZ(%ec6=
zPk(gstDvydK%R6kTZg+QXN+rsZZG+;WdG<|RqhtapDqhD&elwD$er7k{^q9F`+H~+
z<Q$+@2wU_nWv&lTm|;O&g3%fCHw8I^oXWHA;~)prnLZlF)sVeGr??UR9LZFN?Xk)Q
zmcK^KDz5CRI8Gl`gH!F(fGi?{aT;IS^GdxgD>^?07_=DlJ!+z!3)8W#Au@cW#43`P
zFZdwt72!G;TIyIT{bWUGaVo0#Aq}UR0z0;418SUS{3+^$iwK24wOb8mgW!PPd#gk!
z^kT&ngd%9heI!_)H!KVfRoZZ6Y)tO(mM+s^>6yTx(a*e?Yl#Q|W_2?~F)N$ODEdb$
zoIIUP>HPBdjO5;oQc0ng^nawLG8-OF)OZJnyz&u{t=iMVz|WR<rL4f8qegGR%7|;!
z7$`sBU>L5Jfi;|o*Bfb=jyL=1t%(7&Zbi8c$#ms~k~0@ous2G5(J><1;=1o3@>7c%
zI38Y1=u#X>W!x0R)e6WizPCvWRoQby1^xgjFVPJtj*`--;xcxYbAD4>#-qyKrZU>0
zuFpxC!LwLqDIqk9pxG_?TH+H3LDNu-ZQB_uO>Pbwdp+>`f$V~1MI&8uy4XO(;eLA@
zDijL>dhqn(7OB?68cW8aC{_y&{3$hahf*JxQ!VfJ8+M0DV36OB*=;T>1(@Y6?t<{#
zNq-RX(~`y4@iyl+syMxq&9dU*k6rgGv93|{-MhRx1Lfjmo6q~;9&0UpIb!ztqFF<L
zhi+=v^F!zmRgv_nGS)ClX<Pxz#HEdP3h}7@nACz)DlA-jXd>ZnCb8qE_)W693mOJj
zPNTa>Du?DCHqS)rgAHb6N$f;am8zWOqX2culk&tHGL5LkGcJpGLbsx^O746#@s2GQ
zMVe1N?hC0yITp4@L^X!tCqYTynAeVyuFSvjjCCzOQ2uZ0oGL41#^}u`FVY=p_E48U
zJ5;S&0fIimTAPxvv(3Ro+`K9xURBMGN3o9NItz~IKe;Pf@n=QQ*->4z=bo?MoMu&t
z(}27>F`wp0&Z$Uqgx(3e-8zWp+9bMHE~e$=7M!B#nDNVE?=+gq;3qR7qT(Ce()g_g
z9KMC0pd&oY?vD!ftc_I+3&+eoANnwjm~Ci&pAL8N+Jr;}zenYNFEZD&ZgM*0g@N=C
z94U-wV&WUjrBrLuD6;0-*>j@RU$XPn<fQecda}klf-Ik2P(ja4Iy4Bnv5f2k1o3A{
zFN0nc*fJ&s_B@oDc+3-*OsbJQeXc=NT@}?Z5Tb#8o){-Zm2H{Q4s9pv&YU=(@R!Y>
zYo`6T8WRLnwnGsE>VnqKv3Zttnl{b0)LRl#`blz!p;ZhSIt<ilbscWYjjL>B@bTE=
z8zfnyep}J7dlk@&QHSc{-~?>e?1T*aG&~RibFA?gTmgO>6<x&aWZG{Rh<jM}87$pD
zPMjwIu6jpQm|XR)pn{zmZ0qOdblxMS88h+{woRDC8m=qeX{b!-EMRwr0)2xioGeWO
zo0W6zIaq!6@eF#NC|=;5KXDE63}}hQZmm_TsI0@(j_~O?Dr`?YWNu7=(qBKR6ikGl
zFBGn=)xTPpjR>XO4E=JbApL*&orV|27EwkIBIbN0<jcqD0vtt39s}Yni9^>@Z_UhF
zx)G?K%Z0&{*7bJW(7rW>I5Vng(C&XiTanO_&T+`TsjDcxCrc8N<E_?}?TA=xVE({4
zDx6YtT>sl+wE4F&Y+DQ02a4XN3q~n|Vsjn7GDD_}8U{2kq*4tThr%8$GTIM*X>v!Q
z!xv&U%{ohU1aoOzLg{@5buyQR|8O|&_tptF1d8YaSKKt=P*T)G8uY-i+<?|w6*8xt
z%`K8CR<lia=%`_UVqDvAk-sD=B(@QdDzNm0gi&v5k7r+yG5zbT<GfK{;(zgcQcD<y
z%FK!??(X4zgP9VEK2dTJ=Zy}yp!yLCFy|Wf3Z_F@kofd1r?`&Oi3Y!#9q#Q->zcQI
zvpk9hjQ>Uo7a;t|ioH|?xm%k2HT<m|#!CK2Tr-yClX(y781Und%4C$sur7p|CB6hX
z@wJ1Pf*Ly?%78LGlVXBEY#2j`IH26rHYaejN)xhFeW*W@GD()q&JT?qa(m8hd0mNH
zjnZ?05V@i{vnt)Z`0BZY0`<&{J}dFN@cCTTQBM-NN<yu466E)3+dO}27uyStX4~Cp
ze~UT@g@^4#(Rv$2Y90(rOdYCn))pL`g3IcUZAj?T98WC#UM9vRHt$(*x(MbQxc3s&
z)Rm)Ob;P7h(0EWc+}>>jXYV7AxQ2U43`9dFIbwcQ9)h4Zo7aR`-W1IPqm4`_KS%iH
z=&CXkW5Z;}ssBJ<Xqf>lmPFp798Iy6X=n|^&!Me%r6>+C$kCVyT(J7+88B*63&va)
z`RKS7)LlG>z4!OxDpxy@zZUe9%O5&Bg;WiGzg3^ZDdtzf%;i0>>CkjPG6pZ$>OTFA
zwBga)7OOvoU4eTFHeX3^PLaAc11Fg#Q`if^!mxj1dqiqZa7T_BTpIWqO{KylTCDN^
zbTF%$uyk|uQ=%N=Ztco^sM0pywx1qw>LrGubv|Q=u$_{F-aC$g?(I+tmVA7kT-Lo9
z$SgK$><S@Mbxr9(Y0_NlU3=QL)uj@?*%t!hIOYpLYL_7Xs_{tlb^n9B<e+n@63p#@
zp6a{krgWOqqAiob_L5u;rCOrt?O)mQ#4^PN`qRseM&u~uX-K<IF-BcTx0`xc+#w>?
zeMH|^l0p~VcRk!)CfTg#co5g%F_|ZFX^3mcwFFR_cAfpGlxr|MC>1No$QEBRg3Z+F
zKZ5IHlig!GhyXPf#cz3k+U&q^Iotd@HSQaMLs__<KYRL*dUO^il4+j^WW;OuI*Wi8
zN?Q@Un6a#jeyfR#@5)nP)uGceU^gA;l?bW9dD0pC!@Cja)!y9n`yOa$v|jPuPc#eU
z>mF|X;e8Ee*d4UWxc5Igw-07`@CQLoOTFkW_dKNbI|q$(h%)t$7%H1>LL~UHEAcqt
zj@HodC=EW73GPn*A=@WYv$5Sh3bFiMa*AGyfb%Bb2L#-p;s#^~oPLSQ7vGjt?eLgl
zhlaZC>swAtZG%$_I(acmC);Iy#?uB^mr0k=D47Gec$Fry$R;+_r9J0hX*3jqI@}+c
z`=zD;E&+~01QKhJ*Ysmp-FN#>g>+}+4sX#M^!6htFzqs#dF8tMCDql&S`jN*Rmir^
zP=<5cZa0GGT@(!7EiHvt6+5qve1{zZ`O$eqICHy7N|W#yd_kLJJcCts0yYQ>llB^O
zf%sG2Ai{W;^b~3eRz)5-ClHn}n>jd3HHE0Nr?DXuZkZ^&(CZ}z4(rv1vrak)3buF6
z*f8q#Y@J)yz;r$H?6P7N>n7mDAm}0PTI1;DX;Ev7CzFJe4VZMAt_U<D_&(Vhy+aO$
z;h|jNdx))(ITr##T$o^Ge_@ly0A^%a-{KDZDC7MVIJNWr5B4`nV-DK3hDJEROcmor
z&+gGxj41TV<GFp9B-&v3u1y+Kq;f=u1ffh^uEv!uVF<-n@}Vz}zV1fVk#S{PszA}z
z4lVxt&VApj#=;BjmZ?Pm(TyVL9UeTeAvy!p6?tcd?Mdg*D#48{F_(Kqpb!y-Yi^7g
zV0#LChj)+suEV_z@<*{%8Pj#Mathj~VdXvGFn+~eOu_+^n*RH4ooh1!_SbUaUATjA
zx5Aid^NN~a6yTQP9s4$3b?@mG9i)jI0Nfbeh^YqZoiE)J5h(*t8C`;3ZktvB%NzZ=
zpIUZ6+iRoR@6X$1fXwq|l-L1yWe^(NV-4=Y(YmJjd6t-UwD_Qr<h>>SUX2JECZZ@O
zH@5w`)ha1BIX_Dk#}+O_cMlq~5(YS6>q5MzKiy@>+^LS)%~6^Wy4&Fd4_*$Bz$Y&r
zpeH>ic5VWK#+6owzw3g#Do&J<YO8R>;?NjBOP49EJw|ahiIHRu6HR?R5sck8E{fkg
znn@5XtpHCP6`Z1GAfm<pxE?Tl4j012bfDe|ye$`onV$j14hP=WI5?WW0cR&|XMjR`
z-~GMA&&Po?fYq!2Kq<a)_q?h^BXP+Pr}Tcycavza;4;JSpaNk;G!sf@_<iC(gzvig
z%Z`~CdR@9xRYx!|tfbi&3jF0JPSuVhYPO)c*PWmC*aL1T&e8``0ZlYBX277^X8N`e
z>ZwsA(Z<1N+d_L3M5v^$pPdQ8j*6eF?Z5MEObd*%n&sPFht~~%-px-e5o>?j4DY8`
zC3+!kPI%f++i2!h3;>CS(!GhKh;DK~J5qs0w;k_+t6ZzL0O0;xpk1`xP5%kt{{iZ2
zeoE4~bga2Fl$pyK6v}pcAj*mn;o0;X9?1>&-((wsDV162>m1}mrb|xN<u5M~Db_Qk
zvRL7^Y*@_~@$B1)JLUMCOQ;119@$v0_h4BCUuqW(VQi~8o_+u1xZt3Do>+CtA<e_R
z=QNN+npa(6BpxDq7Z&6$7vpi7{;x9OaQBIU0#o3`Z_;`@+Aa-SYK>qZS50$&-1nR=
zS26Im&T-`nICKxZLG_ggTGoZAkO=yJu$Rr1T=-SC03l!}8OCB=P6VQ$2!fM|MXtqd
zBh~~@L1m^do;hbtea5y!(sB-R1O7fLOs&E$otm->pGBCJ06GD$ne2m#^4BzrhhGX5
z>9d1{(MpY!r>Xx4AdV6j$kq#QVrQxUN$(UvemILv6+t9+*Y8s~t^dOs7D6dKG1Qt@
zg5YQVUnKaOv#*9{qbphEVBpx78LQe*E<_M2Ah0>56=<|w@eD*72U?|f^nq7E2I+$Q
zFEX`2#`Ph_o0lIito@*BJ_sZK-2FiDykM|Vb`O1P7sufthYk#c=iepGv=1g|6cco?
z!11HQjZ`*s6HXX;DZ)rJV<wb1TC|5i87RDtlOp?-<?R1)QEgI8?i#P6M;R5rH90#H
z9sp{vX>djtJ^0q*3W3OzY@7GjGs}IuA7dFTJKV-DR2jxiY+aOpy<=D%BBzE5FQ&Kv
ztnKW`bor8`cmMhl-ia{WMw~BY)@$-B^z(+4w3E)QmL$Wl^o3r2hc?6b5^>_SNe0dy
z`ZFR2_IE(l+Y3~4v}+rdmo(|jVuc?!t>xL+((F0^Tn7x=Ip>TL4{oe1<4F!<s-9*?
zKemXA(6&nlZE!li_&T5gD~WVK8h=`jMT2+LlJNldlA-vQv~3bcuUH~kS~w?3oB?iP
zMEg$_bZS7Wj2wAp=9Pgs1aF>kC`bvoL%1>`6n8$sm<e-TN47X}M9#Kyq;|!#kh$b1
z4_pE2_uS~mNmn&+{cH(fgj4~XV05VkslpI$1*^~mS%xdn1y!dD5|jcB(@-osbl)OE
zigg$F(uA-vD)oJ(G~@U+LV&Ge!0@bGcVkuV^@NG|LltDZ32fZ9A!a2av>w-q7UnX*
zsgh6T@=wwg9oFhMIOW=mN~%mko9CpuCcXB1P^0k8k&a9d4KN2W?`$du&ztpB)o&U_
zLZx*v6t`2$TYgRR2i!eS29kJ?LsI-EbQu-m^aM}~&j$sCkg}8k$3YbI8L`Kj7XXW_
zZkp%p&RT|eM99^e*0(kKqf;&_cCm=Yj%AHuy(e(<K8tW3e){j>6xR4KMYtY|SXihv
z#Tw@wT;T+I2m-GU5#pf;vCS3r1EpMo=0+y`Pb!GI9+N&}RODK}v<15AwZO>xC%KKl
z8mGECiz^|{%P%G*8{jpg3j<*FAn-pCn0p|EU-xhG#22NMGWA0?WPd7kB6u&63}N9n
zF2qm14uw`wva3;ILA8We1$E7~ekL75kVnR@Z+%$(7V&!yw++^;d)ORye-N2iym#9p
zxyD=~&7K-kAZWYW>rp<&6;O*0@Aq3iUMuicY%=f#^|Q1;-4m=X1!RfUm;rp{|6=1@
zAlP7fZeQoEiG1)>n?63y3H(QXGD}}Uo-;0STBwx~W!W!FRMy`DGbaAg@Oj~epbxyX
zinBvrD0NH%z2Tnsf;O??3<&;xsT&V1X-u7M`_c}M8V|I{6z<iTZ1Z4RQDJ4ROawaU
zujNLxS19%lbqibywKLPf?nLKGr{_yjGS19-C<56K)Lz8VM0=xb;uJ1}GO$?L>-LGG
z#O99NZ_=`3Mb+dv_bC0x7XH)cVQPYkqZ9a@M1}aV&B_SJ_0seNsuqa-!?!_|FnLMX
z_```rR;S6#NNzQ#<o&}=Z9VTw-*U+_#xQb?Q*wo;M(vo;b0}r9GLnI$gfN){=L`h|
z_<9X(!WyPZq{>MvtlCAOR&beTq$k-YK4%k8(#@R&@Xb!f5up@w*?c989H4*1GnP>^
z;UI4KWCU0&sDe%uHh%NDrrj^rAFSgfprKp`AJY#(gTD6<7rHoDb8O=jk6}D5J$-^d
zH7?i^%CavxMLqcRq%z|;4qMr9V`uVM|143b<CEh|bH|T=QdU)wMk<dfR^3rIx9ogT
zd24zF;s@QZ(SuB00r~83{+&q6T;M(EvrryI9RrB%wNmYOWvfu@OZf_5X(PXW12pI_
zK>!IKNH!aF3q(U~Km>#E#Bm2q34O8EZm34kZ>YyhE<|elewNg$`G7=9aLb~FPiB1O
zXqj7M9%IF_B&y{oSpdfxN`+rDP$lBxxqnv{AT%Q0?WG7%Cjt1jA8r7h+`C$VK=4VR
zPqbZQKj!CuVwdWLt~-u~Fbk$7*ZGWas3MX&CyaQuiC(e11x;cbD;Y`bH;TPA3F@e*
z=dgerY^1$28*0ggdgptLCjWauo9+2tKRI9(>j=2o{rk-bFP9ha89D~6dIx3!>;Ds~
zfPBY8$6ty<!o-Y=h}npCnBZ~al6pUQFMfR~2D{6n*`TOOLGRmQGqk9Gr`K(}Uh)<$
zNhnjm27A-O(^1eLRKiP@3?xS^(kA>0Ve&<Yz=!dmY!FxQ6uDR;qt2gMr#0~?7O9SF
zL`8BzzLh*gB6Dz^Q%MeXjy>cdf=d#5LB^d3A*L#WhsbN}e(g(qZr-kjbpFVnCFSQ^
z*h!vkV7b+fUzKK!7`l(77Haa%xw<2@>~7U;$HWh0ddMdZyS{v~e4{w(pWPU(q65}?
zbhUoHA_Mab;-K5-urTu!@|c{3zr=H`a%>t(9|Mr3c!yKN2G0SCci$(J&2n7S`!LDj
zOqzXxe{~8d&#uPAIBcMOL+tQ(+QfHY&g6z?s2E|Yfqq|yJ2V`!nhXO$qKBd8p!re!
z(cYP)%>IKJf_ixspi!F9y>QXa3ZDa3&-FtpIa|t11B0>fP{~z=D8hlbN}2hhE5BW7
z;hM552-APo0b*Z-HYgZ&r&iXs^yjvL9R^2$-LpM_(IprT%*y3AkfTOO$0t5DaQ7=~
z!1QC+z`*Eo?I{p4<<II9aQ`W=k75=U3^b6_*2B0Kv1U2AmA63?dgWpOsOv%eAgRL?
zgw<}d1Rmm}hktIu?t~7b&stF&B0_v}rvsA=SH2zkUc~xScWk6)#ih{|QyQURAKX~*
zo1=KL$bh+Lq}Sw+qjf^rA^XMQxg|S<MSBO0Bdl*^X@5_PJcjrd%&ondO?7myN+3Tn
z90NDqf+|=80<|yL7_%Uil;g(nDoK`L14`s#hBBRG5leaG;NLe_8T#2{=AOxw47et1
z`a-m{K-364B2rzIusc)koBra~XOKT##_RYPBg2l-+ZTw)q3*nw#qHzt`)t%9DK?c}
zZEns5QdLmo{C|O1+8nx&wE4{ubq{kQNfEtyZIc}+L-!5TpgK>(f$YNhc+@)Vs&)y`
zDwYjk7$<mAQzM{KScGPKiQ3}vAmrpkF-QUA4w*?}=<1tLK1r2;x4*MC6<XO{w!2}A
zURJEn#$;#&;L8JxvO4nNF`S$!zkKwPRX8m(9^0ZdXxXPSkPfG&E(-aY2wFQpanc4;
z{8-GnutKeUoKPADZGSS-7>vBJa-rF9id0ick#QESb}&<q+ZCX7d<}zu;GvdAEx8ju
z)ms&Nxf3H>G}~Ta^VK6J>=51%d^-bBohvtWbecmgqb8gz$b#rQMEGGa_TOS6#{uj)
zXL$M{qPF~Oybe%->>}P(;B}e7>c~{Xf<^htXYoI%u0tYlg2fb2893t1=@WkK{XybC
zL<rBmZ+k?g#Ca>~J^Yr3z_cAwP&r6c{j;favf^P{w>LS*xGD3+lqq80xHygez>yVd
z`VOk$8d7!Co=|AU{4sro@_?Ut`wB)O^M^-J$SOm^c)#H<i&I5JQLYBa_fg#tQqb`c
zN5r^)M<P{7ZxObGCEu?E3oHgn6O<yDAw{0=gBG(~r>fe5n4(Y*^vyjyAw=D<Xp)vc
z!~%MKFN!vo4=5|B**L$_Ly<iM__T=y6avYF_7u53C69bYz~)Xxn07hu6Q!7f8<~uY
znLJEF4`WNOIOm2`vheyCIsAVR1PTtYz_+TSj5O%q48yvIC;CQ7L(IQYQk(t;&A`^O
zPGY(zg-lFYU*<r&p-5xtMowyaqG9Su0=GJW2v+cClT$t5=7<&7r*f>hN7t~XmY8s(
zAP7z6y)^1ZNSZ!!wuEYSBB1dHPswp1Cq5{iGw-)2dJ*}8NcH)^K-11l^LGpXY4pg^
z5w{t6+Kg$xbx+dhb7Ax14CLB|3q51aL0Bw@^xmis%h&v{(Ps_iK~F}6G)O7YX9j1O
z$#gE@m<g*Ec90L|)q6U|QNjPZ_^~3XegC7!FBE%W?zCe#j(d8ZO3Cu)!1Qinv{?9O
zz8f+80*P|tenvE~w{7Lx@*vRb;xiw*2k4T%=KI`ixcjlxF8Exl<bSF5e>4%T$A-JF
zOO%3_pr|aP!W&ecx<|N3pkO4T&6=sWnxcq=zq_$*%^%YFuT(cqPjrA)y}hX-I$hJ{
zF9pIuDydUi88(;hIVE2{-vQtHTCcXM^QVb2zSg*IltAPGOnS;`xMb-z&l8&LsCh0%
zW}lKWjcz=L7=~?FMFPi*{#DFQFkm1i<q&~(l~(tq7aEA{RllYtC+oe@zuB%HX#&_S
zcZC76VhdV;3G$y9VBPQ$kS)Q8XyKfMea-^u0ukwQh&{{iW=4IAeoWuDF6!N1#ttyt
zearr;n4)q{Xg_DBTFw1yN+{Rdyt7_-$kOb#;oBNkGbj*C3AGxGSp9cOx~BkTZ6XXp
z4+(l#o!5o8o^qb?*Z5%i*j*Kqs<b`M3l$U$Liuz0W|`SF*;H_g85sG_q--YZXwXE9
z|3W}j{gL}8fhN{57=4$*c%eb$1Sr)xPkb`$Jh&3wWX!m2kdp=}`kQf`XWc%3jHDLr
zZ$%@)hG`@EB^vl9x-6MEn}P9mYt%G$d@|<h(V#X+_3l2{PbVj+O9bLc+${Nbn=$Q3
zTLby-GLwlGGF5oUM2XW6uT^Fqo^FL8^Ohvvu93w;mh1V}tClGug0xB)rc37Rm@rv9
z2oDb@C+-=INZI_{AoZ+Ww9rx?H3$9(BFynoZHm8EbrQ!2Myq&jZBcperYmszeKPYA
z4)?8FxWeR1lFEY>l7~rlVVU%fKTT=$tXG2C^h6ZfcKCU95NWSyxg#V5c==KZ7PpNb
z9F*9e6-H@vRVZ#}bK(W%hoZL<SZYQPgpEwePIvQBug}xh*el@D8)$+LacUO*RRb5P
zJ*JVbx=rV>r%{6FT{!~wP|gHT34>v%5*i_js#%b=*N~~yevOu$*BVV7X~UVjrh$_2
z;NU&2bNKiEo}RCwE>Y7;7I7H(e9&-`BD+$nd$wnd&wJG3A7`F=nImqAr`j*D5QsjP
zf$Pz3rm12yGbl{<h2j(NL9_>hKUin|CU!&~ZGzx{CKXdBh9Psd#j_(7$c~a6>(K-I
z#GP~%TLX)+Cf69S{H2lkd*qu6Y9fw^E7c|#y_{rL&MTA&u>PbK<-siMofsc1zMPLx
z+7RMU_)FGsVtW?%o=q3tgT6YPSFPYT@6TZ;Sscc`Mf5C~Y^h-VE&o@{)dBI{c#h!q
zs^N~3Vc~|jxA6l~@E}ZD-MKq@e(b?<$?uBbBgMQh=bfOixXh$x;Xf7ZWik=2n_wwf
zlUo=8QD~>{qq^yT!Pu|YA;K=Bs%%^*ZT9XtkVX)?kP2L|y_WR#w`emQz&s(OCW-HF
z-E~RVHu$H$GzQnUXsg+0Yax*L0U{vU#3K}<C~+$>OUKbE^v-y*PjBm^x{B$-Q1=;c
z3U@$a=!Py6c_h#x!`fH2X7DFyCU|33;RvM6z?1hjdY@^?f}5r}*rfML)6S+JX6~?Z
zs-07f?Q)J?J!&E{9Br%XI{|3l{r`rz*FS!*AQUe`+1qIzt3co87hr(&ci;}{>H_hl
zZ_o!}K_7I*E$GF6=^ufC0RAuwnwRg{DKzNYtC)z#*Gxn0on4i`NK_RxL67Fll3pQ6
z?zTO|p}(`qaSi|Wkvwpl{$a)bK|eH+J6%>0N8BP~tRu}b+5_TH)2ww$6=s4x{X<Wt
z==?JT`PIvV<@JszqE*#XRiI4H)%I5$b3EMWH@obVL;6dC=Ypw?8QmhK!U=&c{}xrE
ztSxdt=r|*oDFCis_w8db{oz!(kh1j$VxGDUd$LuUv(rnK+Cts&y4x@DlI6o|5;TzE
zRM4Owa0;=%6ZF^2Ns5KhDRT@ZD!1$SyyyOM&^!56TyGfflO=Apa{$KkA94ib$CD~t
zcOH#+kG4P;0oU#9%l+=>pp5+d=Z{l0QHaJ(QgZV_MF|O_z3MlNRdnl-_t<<#{Ewt{
zHlFf-2O%|>O2wK{=e=}AVXZ3YuPXgT?jdB$mm&5M9oKYU2Y0v{pL*|Ug-h75SFBz0
z`-Qy*4~aSJ^lsWBbY2N~S}@caEoZabW)7FpqoWIDI<--ZOvg6QO(hV>h~H?rVm#D`
z-oQ;r1w0y3uOrmmH<h<sto^^Z4+9*8{?2OiU|$aE676I+n{aQe!es%4Kj%_RuoG{x
zz)SRj%?BHSjy20K`AGJ_%8ZUSKuxP)>q_~`$C7To?xmWY|IOmZV{Eo<9{vkrLSI-e
zr3-X_X&NQy9mb~h-|4{g;IfvvPlPMBA^gLM$L~@q!l{W^e7D)01Any>$1v4B0Hd}|
zNSJRS<Xsvm-_$R30{;&@O9X<)smO>hquPt`nk-OE^T*itc=}HyM>AM?(wyL-po|hu
z^BM#kqJdwHZL{xl&%kiA6R=E@gi!8Yt<U`U_?t32VDo}b{j(6#74TB$U;|LT0<oM`
zpo^w%7h<cRTnk%O0Na$p^`^mFk!JHcVzZe(cC(^iIznPli)u@Ma(N;Sllr*K3ZbjB
zkTFU63nOT4k=mUzHrqKYrDlmUYD>2RWBF#`V_d%aazEJKKjZU3iwzutqURTGYECKw
z&Pb-3QKfp!rJaFBDTtgcs|%NlF;&cItS$8hZxo@AYxrR1LXd7y50oM+T?w9!r(f+g
zj~};xUKSO~#I`tPq5!;^rC3O;EWHlN9DErD`hH+xQXEu-hTKm~whR$542jnQ?Kcn5
zO7>UpK8!dWTI{6fcnucj?DiSyAPQM=NeiCmncA&}#qfr%B?=<Lc5`NuOg04c0RfT@
zi};8Qvvch08EPh}d238~3M}qgIXYc$JIt(~jiG^T$1;%b{ojF}ib${9PeICn&(CP!
zvcbBx%Q0|ynHPBbuW0)q>nwn}FZKTk+Damt3@LVM`5SMhXd<Qw5=s)G_}s<gA%AZ3
zLz_R*b%?LG+X7=INVHZ#TRgQFEZ`LFp*OZGkkF%^(B;{_(_-fv<?y4a4K{SU1d&?9
zx~*VF__P@wBP@~^(*Y)~62C*|72vVIP}ZZ$a}t?j%ZY!T98JiHpB*RCNErm@!ctjK
zs{w|itrR6T${?BrM|x6NkdoJyOl9kW&%2^XCyh6t`{ZaDo<`YUz&XL)0vr7B#TRLe
zpE`E)70|rOs>=DMC&a~7>AI4yaz5}ixfk?Te;qXB1`8FVv~f_o$%$Y?dHoXje(&#D
zx0bIbv$C;5o0O(-&|fPp@eYq3b;fHTHqR<}fDd4(W~xoCI*Dg*$}%|mRQ|0Nk&c^Y
z6|0Sw#cbE6i+>p5-M!>7-`yimKJ)2w@`%W2BIzp0-k-b)Z?}naOwUBR|6)N|+5A0f
zntO;;S?oBl9VlOu?RG;}hp(jocZMe$n$w<TKR46}A_2!yzyNR$5ErC=S?KmYkbCsF
z|IwLBaHCc^^+raR-EjB2$V*x)qZ@y1dDe!^1{IAH7wh$;RCuUp*sv$SYmB*Z*fUGN
z$}jYuV7*Khf~Up3eqOFJmXrcIc6W^`1SwWEJ4Oy7#0Nx@2MXaK<zU_HskE(o$kr^7
znK^N;cvF9tCVkPKB!^e&cBKdV$#=5ig~$r%f&K65eSM`7li{tu*XsOUwe@N~ya?3l
zs%{0m?mzVd-I%>`y;nP*1wodCmv2v8t;{+X5xTOK_^Oq(IE~wlAbp9b27%rjBiZ@o
zGyt@xZ@7|WevL*yV15x@o|=X|V*d&Tu+C#dB#Dv3GEFl<Hg$QV!(SOgHcPz}p;f(5
zb9s>4^m=E6p#T~(`aCidT)QStt3KMbNEse&Cqv#{&%u!)xFVo_jEI?vmH6`08QV-=
z`j00e!ksWIEt`=3u51uIbCmiPa_(=eoNn_+)fVS!;=|Rq3-*g<0ZGcs)l&N)FET0B
zL1h8ba*WiJeLdISwiAz4*Yge>87YBJj?1Ir2F@jKXI~vkY+3!AAN@oqpQXWhEPr%B
z^Jv>cB;xWH6-n92WX2XH*mgB%ety?o0YPc{^uHpPTbJd(BI6ZjfC4q&{e}m;05ShZ
zBR99_nZSSR?$;_8u)q2ZDRNB6@An(hFoAk3@K@(=A->z(+g^YFHZPwg-&QHhx#JNf
zA1f;%f_zi!S;z1LzOi~m#Mjxz=d{|nM9E+TG@lL=YHLEmVS52%kiANQKrbW}k@5S_
zDU$R7lAwE3Yu*P&lMmsXoM<20ZPv9En#QTVXyoD=RM?>ll{U?#VsT}sM2!#|2;p%?
zF>bHC4_##u2wtZL2Cy57JBS!ih#Zs#&V6zLX+MocRl3K+>d0uJ1?0zTMek-rX%*;_
z3gET@u-S`E#wpP7)-O;nm*Skxbs_r_wLu)%yfZNzc;?AV*9th@e-e6^)4vR{HBheo
z@e!BZtBa#f7iw!U9~jFjeeUovFxKX^r{o9A#}_}@G^qBIKRBK@K@p}~KG|ebpuANy
zD#4|;2!%&(d<~1PP=k;X7GtcNw>TH>BCjuy;CXk!W_eYZ;(;9OoQZI|y=r1AYR$HM
zdeg{2Bxuia1|1^~3c<=!hYlq%^#E#6s6Tg&7H;?9w~jBHf1ZkGPUIQeCMS$)ub^fW
z6toO0`1DN7IQ3d5hmMi*lx*u)1{JXPEce=>rg0I7t@!BM2fVGTbF8tt0e1h_d%?g_
zRA0!xW!=4M$i9DI0^0qoPk#k$9t+Ue?=R~NSF=RD`NjnNH6gk-i-uy}-(Td28aCL>
zfHfM+Mc*2ZrOPvq5gen(bQi4i(0lU=2h{!D<@o4U4CFeyrTq364eT%PH<4TEFWB|}
z;4A1y`R*O=za;pO^RH1Ox?CFj$g1gA{H{?Gx?Gy$eQy4zX7Ip7JoUiP3n%4o1dac*
zR`rWKb5#JKR;jSLU`7!_T*y?PVP_RW+)_T)O9D76^A)mU6)wzDKGsSEIrGOqvX|y4
zpPOZCYgFl~mnLajoBl^H%4+&$=j&AI=parfDPqj@ET*`n&m`lGY^=tIyZSHvt~`(Y
z8wk8^ilg{f-=z(I37$$>$9^khba{S&8A0$3R2{5&tl^@EU^dEhHQU=t4Jg%xQkylq
zq*?ixKF+%7m(lQo`fAUi3I;9D{)p5f?kHI$6%5AMz6&A4Z#Xh*Nk(%L#pOeWnRMc?
z(M^Ix&B7sGH;p(Fdb+lNl{440@D64NzorD*VV6vI*}_Nd&98ISa5%Cv#-XRv4XW<(
z+du)iv>iB2hV5pId9*q1O@4^@W9pv`gx-DLbEdv_85m77-phLHX$9RRuf24phqr|~
zy6`H>lDQ^*C3lc~vOBJOgrN)^Gxs!Y`)fo$WuP}8vB0}rlX?Tbc;qMAJ4P6H(bFCD
zA?ETR?eyD15fS1us~(G|PO_7YBYoEBEQW5XT-v!wXuj{b4^I@r*rspv53ekt;FymR
zmKF!#IEj5O@z`1i6JsndZqUGyG~=P^ocbPUK&{<Q$$#8bAt`TqLSwTVFXGZ%uc}8>
zj#EA@Fo=bQGURhTOJv16HsXDsu&4d5(9hz%=4-K<%E7w-_&Go5J`hFuWlXcjKgf-<
zzJ4GoC#NUvl{A$Pk>V`elg;Ufx}>F86qZ<YmYJG~=UYsMOzpb9?wSBaCJTz8JRoaI
zD@hTt^mO6?$H<E<0EMWzfzfdmR!fQL(wX+)%ARF}Z7NCRBFR;N6~2}6y|)|O%B7hb
zg$M6~0(<41jHwmWfSPLB@5!DD=BVV26ye^=UlojXz*A5M!Gjjl7nD;e(kz=3MvN4$
z@7LM^1w}qr6I*y3#BIMYa_msdC?h%TcVa5Fm^L1-xE4V(TBbFcpt;bJd8J+6nb<$P
zaXgsRi0+(ueU{H=UDw#bu5Eu}R&=q6psB(EsBh9>7$C4QfZ3KlgU6d|F5Ow59ULs8
zI*Dw%5F<BJ22$IY^V>oxQJ~=_@-L5MnYKCZRZ5*ZlUqS6mP@o8!V_B2{sbgdQr42-
zezt;vpIEvaA)DCijz|hdO?sIOA9Bly&<a6M;&Tim7oWH2a1=;i`}s%#=geI7Ig7w|
zi-D&fqdt_t#YsyC;OhV&|2IV)xbP~M$g_g7Kbrb3#PWYBB-gw@+?0J@@d!&^u9xRX
zm18mQ3A|J|CW~c__d-%fRgrY5H0)wj8UEa4n9Yxm<iDKdmbZLCEmKCBBsQ!m=?XRC
z33c!|gx=sg)S@v3D#vB=s%#j0(;D)>!z5sVaS63x-kE<H%{6*Cj{91qPVR6K<!cB0
zVjXN)aKy{3V?wkR@31J&+D5$f1$Git^%kN}=N!fBrH5}aMpsiWz+*h^tdUFVvJAie
zvasWlZgO~$if()AlD^rEI;&!(^rnU!wM`F3)(-S~wj5#&xd08`!!m`pNKh;HQ@Em`
zOv5)OC7324I!l)w+;)pF<E8xO;?CsOtTcZC{U%Iq76J9$T?fN~SB{SMEAN3s%7E%T
zU^!$Ur|tjy$@@Pjf~ix1R;7R}%LHAJ3wxjz@WLwRLR2V%Jw20y{!tLE3@|2Po=Xf6
zSr5MRkntp@et>KSm+<f`;N_dwas)n#r4lB5?%y4A%lP`E0#4OHGuD7LwY7EKFG6>{
zD?q^<0GCy?ozb1{r=DHl<(^UNN4q9)<ziR$nMqJENT_!Z<c@TGvd2%rhYL3ld!I%R
z6X^<ql0Z=jn;?Ho7pG<y!o+A6-}%oXc2u`CE#wgdr$I6l6!$AZ72OounoHLfindFa
z_|xvWy}5Yh10)cWzK<t?uJ};WZHj20mv5nOd__^(WK6-h@B|16oU6lR<$vrE6{Z39
zcOHxax6kU9zks0+z)m@RDUiDGAYGV>{{<naHz6V+mVU!slq78KNEDej_Sizk-nRd#
zKL(WH+VUj?0fxf`4G_hIJ_~gZ>E}-ygqM|6%dM$#{D$vy(Q#B!bAx1@48gQ$Vo~Bd
z^5)V};G^iVW(yDy0_fkqiLnF5pC8(N-a@wl>%If+M*A(l>^5(+ZtLFxS+~HM@4!=8
zme^zoV)_vQK>uI$FK467NW7N=qd)R~idsffF?x3o^>aI_nIv)QC8CH9{-vYGKD@Db
zD434i64;?weJu?OO6!;=wBfZJy8iDqOB3vj@{<np4t~_kqU~@#2=feFCl`iO?3oS@
zWflXUXoOQR)rjp>d#vy;49yQeYjnz`#bUqn>$AhttF7Sza;<(O47PUA<lvuD%0faN
ze{2ZPauSzxzL%{9F_-7p2;`V-pN2LCUJp=XmIYonBWlT+i3j^Nv{u^pGQ?!8J5&!6
z!=~vZ1uQ0QPb6f0!_RYih)4o_?Z0018j7~7cSAHRH6`g*{tQp@l`DN?<LoUV8N&>!
zGCSL=#fwI@w}{gz;;=eMh^bM{lgWQ~%QqbleV4COC+vJ5atYT$cxa3psI66zu^ezd
z+zd^$x0l8#!VbLG?y`ZSlxF{yySl<`uo28}gw5|nYJ(oeNnCi4OWLtQ-y1F&@N`#~
znE03c8o0~{gvsWY(<`IYxH$}ZcTBwIGL%}LKiNkmCS02P+*z{e_~foYaDR~(LF=vU
zS8Sb63Qot>YHT_MmVWGN4P1skxxFG2Q+({E-CSf<0}YI}Yo7a&%7HUF2fE<c0qOs
z&b6R*90RRT9V;MOD<Hd0SBW6qF~OqE0T?VttTEDY9s6c@D8v*g@$U5b6uiC7^Ir<B
z&^+-EIHO^H2^S+)Q%@Kx2Kb1oUU9yzB?-^uAE)r-*r`K$zEa%`aZX7MV$IHeud&##
zRfe;!v^bD%F5JmiYuev<?1ZgE98+ln%&y2htDOA2dZ?}Z*8qR@QWLfAn&lqW18Md?
z_njzsGJ{f`nI2t~de+-{JRqd{_M~sKYJo<&G?$ir3V*GuCyyIhc7vu41z!*>%XaTA
zYRfQJGVMO5d2c;g)P%)+Q0MrwgL89x(u&@g7BPI2DE=8HGvh?KFk0GZ$rO@SO(b0&
zyNjU(JLiN6lY`TXlIKwxzXQ+fEtL7&vI6Sj%sIs-^3@3(tg&~{I|FWQ-COVeH-a7i
zi|=r3j!W;p^H(r)VkM+9T-@3pFR?t?eA?1@7PgG{lY`apo_!72$a}om_42<kFcbv7
z$%#jvZ<Pr>YEnDzK4iCB$IjO8#bwiYgpe}5O!%7paY1f^)tSX7>U|0{icI4oZdV}7
z5`(th^PY}$6EQq0%lhuKqXv(oy=76-uJMug3OSr?peavu5!w$9E1mQ@oQL|e?v#J<
zG+>s@Aw^N8Dkm6eC+m15gt#;>AAl(jLjDVHyL7p6fPS^Hz@KMdO05t5nt;_wpzga7
z9nkJLu=WcWp9b_&0Hh+I&ElAu1n&<-Es8H{F@89|=bxyr`!);BWSm52b4QHyh(qF9
zam;cSqWNq-OTz}0-*(hMq90No9`!ZJMS}}bnRhbvr&bEhm1I;^C^OA5jL>2iyn~=B
z!&x+K1Ng5wrfF>pSYVViV;g$t(V1%|P6c5XMr^f7f>lm~L^(z)d%qylkM0k-+Fn5O
z3IU*oavEw$q&#bFrr_(i&}xIH70NwXN{J7LtI4BP<hvoS%}FWlF+}FcP7CfgL|<RF
zNWt$Q=m^L?WX~pd+1w4tN;KS4j+iC5k>%>o4|1pRgh@WM%b9bbbzV4gs^-9^%?Vv$
z@jm%jxnO@tliA^U?Ol07w`Ej$kYncZPUb@~G+v0@TGkndJjFWIK32IoozzzP?B?lv
z0Cmto!p2}sb|T9!#PJ-BMZU=$XEANToMWp^T*_U?M~XrxlNn883njNYl&|ci1YUvi
zrUqK_q--yO$ail&J9z``tJ8c|b2IA{gg`6)@)33Ovr<wD^*>pLU{)i|I0(d=#?^Ro
z7wcE!=7Z;R<A>#IFz_uc=5q<n6VMo3Kg+GdV`xQX6o{bSp9b60A=`5w2W&sE1|R?o
zSqU=+T>3Wck>j&qc0y!~-X)~gaUy?3Imc_}G%%VYyU<)rVCuES!4u}S;pKXT^ik@_
z(j9l*#8n@pTvQq!M)PRrba?dNnGl*^eOG{e|5Zyf^)IzmKm!9kmjj^3D%Z-zTOd*e
zkRyGK=&>6zusc`xU5_r%?%1jJt2V8__2ISO;GK@W{s<5E-d0+J?xPsWq+cix5x!kl
z&@2t<?7LE12XZn-^F};u19L;h2qwln#)rW2gpX*oi{YzVG*IjO7WdnrydUq$D+M4}
z*uT5D+dy$?G=D4jO)$S31t=1JcIg!htW5N{Fw6bWHcEM3t<3khu*&_=DGykw%zs7(
zI{r^4ssC>-E$<e5;OXZZZxR3h*%U9mOvTOhFPko`{yUp&7&$8w|2v!fNH3jKh5lvJ
zf3(v7)$Aq!tw{GjN6w+kpeAxiV>kpk3>*ic<BB%Poq2Ud~DoI!sU~j{FhG+ng6os
zL|>4O(K^<lTwjf)o5B&gLADC^K`&HvzQ`m2!;1g+@h_b0wCJ>2;=|nkkFa;@uEY)6
zH#_OrHad1kJJ_*}j&0kvZQJT39ox2T+jjQ+-v6vw^Ul$nT&q4n9o$!~s^>XTzs5x+
zd^H_d_1L?GBR_i7Ty@u?eSzP@k-v|dlZsvHPWN4(;3^iLZxmu92e1m%4<X>rx5YQi
z+ehN5hhj^x&Rz#EH42cw<uLyW6ut<q{vTT^{*5Z)uvo?^pV<dORLzpbxzl7-M^csb
zUKRS22l<!h%eNEpsLSK!w`rPnVHvl2AfDF05_ZgWr1qinpxr>tcZ!G{Z6myvORj#n
z8%G@7wYP2M|6f;H2#&BCRL&vRVc)S%SyzlQs7Xfbs2oNV-;Dg+lWb4rK%HjlaEfWj
z!lmww=T?;eNw?~MT5UJE2*ppES<xYnz@yGaRMq)5UIvjrm6Jr@TFED1!^X^H-doD1
zxL#A?6k-A~I{Tk2{k(Y^7R~H(UXAf}${d$bCX&qw{<&YVXp4ewm`JK|>@f@5{e_h<
z^x_IT3;RMdE$<30S}_vWkmgmZtZ`2@Zbxz?1~e#qS7<4YknWYlwMBK<+Gj~Ty9rd;
zddy-tP_9LSh@y0B&VCq~{V!jFb(gLhLfsW`tRmQrBOOzRE@U+m8DiPMqN}uRN_hbB
zUKkOW56bl+@Mfdd1^;h-0#hRtb_{I|pC+BfrkgIM3;R!?0;!$<N1)<bP=(ty`>ckc
z?+E^=1!h+$=m|`dm$j*knNdTBXpak_7~9DYiD4h#)J1O~dV$rhub$FQa4C^p#J|+#
zsCT5_42t$D_PRG0`Az4y4e!tE*JZ?U|Fv5=E8ABn9l*k@Vn+ubU;bU7WDxDVC>(c?
zTpr=sWdnOoOmP-1KDAqMD0`4l*4~=d|A(j7GH!lPVj77+GYZGC4nTnVB|JW46+ZW^
zDONmeD%nVnYFr`ZEKO)#(aRq-;*c6x>l+J2ipV@@&Sj(OvWB*z@bnee4Qi8H7`yM%
zj<L4=cXH%cc!AalTGPJxS7qn=J$v@gg@YZ0Qnm8JuQ%|XCpKoxBUQ+~^CNWxwBvjY
zVqfNHfBM=P*aB%tYg2vxz4<=x=;7K^^LbO#`(8@(dp;s&5s5v4-iB^=E1(!4G{WfQ
z;pZ3R*)w{|U2q#aSV!LufA4|?F;B}_pvgMe4cKN$Qsjc^QFeDC$%s^cjmFp<Ipf4#
zHwar-w+d^wYoNvN(zQFSBRS%Y(5{Jj=x(K^=S675c?7*BC%sx?=z;HvEiAp#%83%%
zj`>Tqdh_5T<W1O}IU%(zeZ4A(Y<e~$An#4>zV*RK^dh2a_$z_`xPi_%r^`JzYw*ph
z9a8dLQu@YdrQx)z<F?CTlPBC$5+b4NOj-8vXGSunWti~L_^n<_sEy%OtvhYoKZllC
zhc;D`<qCTDTE{df6MZeEM;pZov@++<q<mxOIs~uR7i^?gPAGSU%rm52kyIfn0d|eW
zY}>yW&^0J%TCQvQF2%4_GMKSAo^u{JCthUyP!NGhk~>v|idP?B-~5|A+kAK3n2XlA
zpVAAn)f_(9RE@3Dh0Zj<-6W+EMu~Kl=u5PR@XZ0%eGf`McxkiN7CoaYl3;fX=MFIG
z-Htew(vLA2{kcrN54Nl^f~GGG+w2byj{~s@ZVru^i7P-pe)w~j>e{v@SPQ6BN4E3P
zp=O?eJm8|zPq7kq2RyhO>A+G8gLlUgy9oxjA3R1OEe&&Z^h^995VmjCJRr3Z1rK>M
zuorD2l!ajEI+g8RP)PYborefDod>fBh0b=#Pl{nGHZ$Xo=K@1=zii=*DSJX3)LR=g
zmu=(^_riw|UE&gEB5;g7;-B|`2ySI8uFw(et@5r{cXxCBL$L~^6Oe}cb#ty+_J8DX
zMSEZKORLh)>dCJ9yLzeT|L9@rU%wOB3#0C$Vv|wOb<+OK!##O6rGlJ|BG*lHn6T=;
z&5=bhYaH^AfPQD5r%o>NLoZy)m70FZy^jX!r_Ps^!utEVBat9PvE|J(dodq2fC%jT
zFC1_fyC3mwBLac9<>X_bGg$Fnr4A6twWR#w#ibB|%$KnW;91AK*07tX0NllhVy~M*
zEuka}Rqy@Wp$eW{IMbiu%7CSj6`#s?NHeHFRHE(Rf092~ug0<~6Cw^d?^3R$9iX1P
zq9ux~vIrBZhC^LT$1%dXhi1OObJ@p*l(^CGT7dN8w>N$b&NEP+F_gGlmPx-C(kDg>
zpeOt=k-Qcvagj8r7L^<eZF)rU_^(xe{MYFN@r^ZLUMYBzZokQu0$B-(V+|zTei-0Q
zHUHbSQZzBs&+0#j^7}D=Z;+dV$#(z-y<opM;@`%T@?C*~!iHBEZ=O6%ErWynPpnrR
zH5=GKipM-jWpisnG+f8DKaqo>feVt74TAQ17Ms7E6I*WfHrPw`L=|~m>1XuZwwGGn
zol9JBQg@mYat$|>{8#umGgs~B?-^T>(o8>*q_as)$JjabcUUANFceKg;*+voWwxAl
z2J$BKa&wV)uVtx}dTQu8+RYJny&Xrh%$Bi#=dvm@on~vK>gQB@WnEKNsFac(-Y?90
zywTx1Q?d^~(KgyS_8ymuF^!m{t}IE<EIl2}Q7bTXP_Tf4ec|AKFIehEu12;TWa&|0
zI!A@Ag`Ter!{v;qYJ*2t+TZ9OQ7OXLDuMOSO1eqZM`fIwl3!yU_8V`OrbOYdKGmZw
zkbeVdpiLF78VXdnPOw&>Oohv+QH~%u`1{JQaR{xY_;p;G)bMpUmG5xcs%&@}3zLyh
zMQ<JgqmGJy-5z~liXubIq8mr=K{wwR$D_I)i8o|LnU7D(2`=kSSoNjl`pSI@HcQt)
z?L;2zngmu$MHx}=IXPb<HvvqQo92h+=&5StzxP)iV;IvM<v>LBVEsXY?3#iU^}#9k
z;SzMz<Vp*tP$D6;$x_KxGNxm6P`@q^!$w~q#JFJYJrV-(f!)P6<_8GqY>h^D1V|CT
zh7UerjKx0_a#REG{$Tn9k^>yP2H7?pqi@9|rRui?PF>crfi7k9l2W#@ZOCO4056D^
zAt$$7{lx%+PBBkkbM5C!*mXrn{Q#Deb_H_oiPe3c2YAB9fxi3#Tq$77q!fGCw&DV{
z%Sbt-7fIi71I&GwU1m7`gd7H6ESb|k=f0VAhPg`~^yAx?aI5nKWac9XD-k1LKuOT}
zf>!^M=Lv9Np*=c($l5EbR>S5apCnT~p0frM#!Yc7KT!(*2@g(XeY^8gCaQtNAO2?$
zS0t{ov?GSPlBWwCTM&QbnVs>rz)%@lN|m<XIQJA!dofN;IX}N&-*<f?t;G(jFUg&V
zr-Zzj!f(i8nV5}N!31_xS`KMc>^fu#KUDzuOO~}{PZ=J4QTHtt#(ccJ)aEhVJ+TF{
z9)ymCR{igWnMiHZo<gvK1LxVBaj?A^-SyMfwPI?QEwb009uQ$Tv7bEgQJlR8F5=_7
zI{;lRZI~a5=%;fmR4~|;v1^}@V3Dwb(45UCk&>w2Y*_B93`B$=y&2jK7$1=yI}xwX
zA%N6X*Yz-Hm1b;~T)}hHec53fdAQ^Ne{j8Hn;)t!1sWp)<M?~SjzRfH{@}V-zyssK
zuiqNtNz$mE5TwPHKm^*QdUXof*Eegl=+r(;Tp9mzP9h{3qzMdMwmi{sVfyrunk*Td
zx;~9zIw~QXQp)ECPIc=7czYs{>c+{Z+8QX^aQlS(OYxZ>AAf6#ZROj{#-ykj<RJl)
z?;33SlD7dVZGaY^`Tw8NoB4y--Hk`uiPlh$YQcf)IxY<OR5!ZT3YumcQW)zG4jTO}
zqnfC8Z0^l|q4#2p(!yx|7b9?%4W?`fG3EX$9!@6x6H}iuV7AyH*<JUWF1OL|b)!%6
zn)I(CbdfaIfK3v`N<FMXaSWC1I+pg*w%7R5F<Zbslc$!`hIj;qcK<|rMvIplOQ|U@
z->`s(m=JFaN9XA{#0bR#^7;;Vq5phc4{=Wv6y(2TreMO#zJx}LhYmb(Ca(3bO=n0q
z`Kovu8?^nkz(thP)IB<J(mz_MK_ujbIU0dT$%o~1z{p!t59uoe($l1J<{Uc->!5|N
zoRp;-j=_*$geJXXEYdK$4Bx1f7M>jv(JXnuMM!(M8hPZ@!eU}MQYtO!Gkw(b%RdZu
zj9q+3-_?b?(s@~QUX%AideP7=_u*OZgc=pQ@YFBp^~OpXD*S7Xwz@H?;&V%!F>8v9
zki~NGE%{0f8)x)S8aZKSq5f2o73nKksSl<VwsFptC-rmSW%_}}QmhJxf1?=k>c7o0
zJ&ZZxR=n+Zfwm*PNOAricia$0m>RiiM<vubAp~bX4BhyZX<)8~Jx#$nTDR8aS5WWD
zmXEuk&dx}|m7%ZA135Zo_iYX(=gRxvDjeG)&@5z278(PJ=JBWUc`DLSG)gEcY9+%|
zg4t`@?jSXAp2Aq$?Fx9Mv;f4F_-%kH6LgZEw5B>2LTD=^?+q*y071S*_(tS-1qUG3
zMiAwdeSC8w)sQ9O1;TeR(L|QJJl~p1u%v1F1)n0f&C@@WttRz^fE=Z}y<TouV@Vh4
zej}yFJGSH4JM~S|OP=tkvhtR8L<M{2gU974^XSEPuVjgiZ}jGYd~k!fMLYuW?MtU{
z>|gpb1!vf92h*Pfvjp8#EZRSdJ`^pGqW1l%3VG?sIjfwrzH{yQc-fH<adZN@zN0E`
z7Opd09UG7y1#gCY)9jm0ZCo>K(a<iQ(@2&?J11`=PfWVAYYJ~U1u+IWq85&M>Xh)e
z7;Ulpx=w+EVWA!?`qL<5A`&|$o=`A{c<v{hcwk)%7}yO=WcH)kGB&vjMCA^ou<I7L
z@q@pj=Q|Z}5&lN=+5*hTWt$^ePNbfxahh7f4B8rMra~iAoBXq$5fpCc8m+zm6se}0
zT6+hWwq&r=Me7J?2k5w|r_k$f@L~1nUwT+K6M2UAv5z1PV9^>YgDXT+RuCMRUJ^;o
z^<7mdMD)!}N|jDGtI^+1OG_y3h_3pX#om)93N=wB2h|Ookc0n)24!0+(sFmDA_<3}
zn>!?8&6T6+<AFT15`Se54v)0siHI+cW?d#QfYlYM9?GWRM{O{{OR;0*#n=_c?&Ny4
z=!gR*vV^30tT14#kmlF6Y&rsU*zON7+lFe9g2jkk$jZB874)Jb&)hL0)GhW#UQJzv
zw^4Z#bTviV4br!xPSI+@SqS4kzHn>JZVvv8sG~D4BDCpgs^RbZDFxde7`MR&p+E^w
z=KiV(U{)`9q-4EBUfHjg5m9{iS5qz$uNaNl7AK@SM++{;jkV<up7ylDgltaY&vDnm
z@#dCkY344jiAC>}yzLclYGWY|9H4F7q)Jiv#SAHMT>cy7Sh1RP%os5t&}aWZ$kUR(
zHDkDlgrThffHUCGBU&uCl^HR>AB?)k2F;N?%yEAm(&{~~Bv0#Dy_g1H(DWG!PGkoO
z794bBgd{QY%9c_%0fL*RcP`}Ucc8WI2_ZM8h6lq+3GpD!mfR{yfRKr4Q=nn($StVq
zi*rq$+(ZLlVKQ<L8a*zjpNo!=k17&gXA@l;oq~GgL42B<<K2F3D;rzfK3>gkfhD<6
z-7Nnz!2k54FK`5NAMaAz+qJfk7sWr|vOGRiJT<1=|1^DICF9LdX7AHX6s+6(2w&<4
z#Mn|+BrIy-{zQNJvFlpQHTMsJF7e2rd#qv9v2Q}zromMR+fkP73M)75de*LIil7|P
z$g!H*I~^B#iXvvkl)C4{96!x!>@<s-LsCp11}@4=8BBIu=5LofHl0+F3bThak6;wT
z5n-_mc{T1N$1{3!BHv2t(b29wPcDqG$j2h|VR#s<qCKz>vgWRc_Xn~jUQUi^QGm8N
zw@#>Th4e4b@sQ%c=N#YG$HYZfgVSn>cn7pNe`9zy*KvYv4Zpv4%B(CYC5B*@Q)d)Y
z^$dDtq=yZSV*F~yD*dcImz(UwT)j6*mdYR-MhiF7PHSi_(=QJ<|I$2oBH7CHy_JPk
z#dxj``DbjZn!9F1n5&Tuv4xhIZjDb{1umG=W~az&0%F3etL#cR??twWs{n>_LPE_t
z!fKnR-_y$@#(=L?YgN9)#W&_0Sl5ko*3f9n)mqh(%7o+|ec(S8>YrbX7h?O=h~B<(
z8e@GIW{LbMsYQYTWXU@uQYxc=?X|_dQ#c~Za=E;%bBp_jm${eRGHl(>Tzstk{v9Q1
zw>L35#t8tXzbTynfXZWmaZ2HViJ$VWbbH6Uesjb#VixR5hM}iGf*Q_*gtmv@e#7Zo
z{1`dE=r-7Ez~bh^dQ}T+<yghpzuDAGmoFxL8o{5{M$aZh(S4?OA^5hR<?CuvQ+RDF
zqRe|^cp5zNO;K&t24snC_5SC9;G#2b$+hddhnYF28xq=S*|Bu4uugrg7Rdw?2nSnJ
zRxe5)VTwgn1=|L3G^{~xwoD_f;hIY&A&*(-rE(H(Tbk+Dy93h(mUd}Z$_dch-OzV+
zed+zIKX;WkTz6xaHAUQ6U;T$6?n8ziGNT$h$M>bDA)|t5&qX86q?m4naG{$YryM=N
zFoAjeE@5#$#V`VKZ=!dP9;8Y;iFX;65<P$WS!6^-aGFEbFG`0bN03hx*2QDi71@6;
z2{D2syWE@3r}k%9uG>Jq5RhFuCeI9G9pyY%kp!c9fvV-8IpeZH_U=#EfAOU1Ki}tC
zYfyFtASm4RLAU5DdM=`uxt&j&T8!Asg{|9dl}80YL~IhG@j+ZW{3^B&P1dUj6Gee`
z60jag0+FKBWJW^4$?*GexcYxpY2}>ib`;sXwFHi_TXoPYt?7)Vlc5^mE|Q3}khe6q
zckV6DG0~6_evCFG!iE3(>SIZ~kA@O&Poh&aJYlVM!o@gbu8nm{nHD%^hWRARN9+ub
zZlQBWJ!p6LcJ#IPc21>Au_%_|8x)DCyWk;&9m>=?tKyyF)AG~2{N@V(J+Q{)bAFH4
zaX@{yod8M9gbB;{TErpCRm1X*<9S_3z7uc4-m1wKx}Rk|VY8gX5%17XA$hV(gE0s^
zvBXs~AX0PA;V$8Z6QE@-_ZE$(GX8P4)TKqi!6BwvIyJ2Eny7$g&EZK=z1}y~EJJ}E
z2WuCY>N<Sj=3s0=iv9F3ls)V8w%*^VPWpXnKF*NxgG<Hr^9A7w){C0dmCAEyd<>6
zob~2>mTcz0c{H_$1|I1aD4h3h)cK~JLXl{C%U8rJ9=dY4c@JA9%PR%!q!sc`nAf!}
zamE`KwiSf*W13NP=a<Tl%Bhn8w;PvDoZ!a}Nj0Y26Fopszto$V9U2ZAbv@wwCnuc7
zYwZ!fzlG#xDBf;91_gM*KIAqvV?=qBXj)6Ao9B!!NoSu^-K!c4^-eNf;lSjJvgXg*
z{u{JFyYXnYuiN>q#zRxF7m@$VY~^m*e#r8JHh??W2vdjSdb@1orKol1b8uI5K0E=M
z+g|O^VySAA@;e;$eNWkacd%+)6zZZMVmb!hcm<8#a#Ri$Xtftpu5W6ORU%uSv*Xhl
z6o{>hl?ZXXmdqYXdsc<}`SD*K-TVtwzqTq$a6=E(gjKb8W1Oc>uAYlMU)YHB)tU`$
zxMNGuzTH8{oO8aip)BPf?$B`wuwclUA@>+dsqksq-MEJ+x3HI7#Ag4`cRb~X?np%R
zb>tTf5Q|ix=(LdYQKcQusHq;z0zXjJLkuzhA{8kKYC(D#-?;nrH2~>ky<_9!k33Ec
z)_6BP-^zUm@ha8VY0`AY73eRPb0N3R6NQix)fUocTA)OwoNnHe$7u3c-YLAke08YR
z8O7VmHsa~WIo=WobqZk!WVC__o!^oO2n9o960FOMFH%F-20}7Jaa5YZzO0=6@pVLj
zpA+)@Uya7;qoP=#!i(c|t7L$DOV2iv%#w0of3Bqzw5B3z<e+}yx65EZ?(%A?^Pj%k
zMf1VPf#g9-#t%}N{qB>)gJa&wG}qvx&Lr3^KlvmWXjdV5yzX4|PW<M5ii)g9d-s7D
z0iA57xgmy3l0I%FvL4o$a4iV^AU<z-?G2Ikuh|g1#lY^2QCEKH!n24!JO)9BOyDzr
zEXisT#O3e!w7LfBiH8>g+6GE4@0ZPGe3#r5*E7e|*o63Kgdj;j9-7|-g*h@qa=M%k
z{OXt^XiDKO-4Q;CpbX{%uJQhssqR9KwqBfOOffcJUSGyXmftfm(mXQHm=6;j_7L+V
zy(2f$6;C_TDuT%m`NdKJ*>+>fys5WNoGdF|a@&7qZlGnwGB!8_`;Im^unbWd?xYX-
zc#Sn?;tEP4{Id^k*qn_r(3O<6a{{u^c?#OIs)uz~o6*BK0mg^-E4N%;?&A=VhKRO+
zd^~)Cl)i{u#A*Y(N_C=(a-5elf41Ru**%ej!Si1kAa94P1G|M0fnB=q33@U9lYX+5
zo;|YTA=UGL)5k7Qgjo#nqdU4Q&qdw4l6x&_0<^9IOihyF`SuX5;G<QN#G)mMj_KVb
zv**B1hakNhjxgrJwN0jv?efI0Xq4D*fMR-ou1p?LYYIkgX{Hk{MKG@YQ{LQu?Bs1S
z*3)WglxPSElP5~|rqci*z;y}1YF;FoD4G&D8BRtzMljB>W4bjSQks%7{pLjFP3jbn
zptNa3AewIJx<m9TI&XCsQfaMbsNH(3_;;F>qU;!WGI@3|DahsLPMk6&=8&YSzLTyR
zB-RK9%R#*&=G~_HmX&=kXR+#ioVq2*QCnO~&l<<aOfAZDzwcOYGi}_UTFP6iuX-3s
zg=^9ocW=)mOTlv<3jqz?ZK4(>A__qVQw;oB!-b8eyzslb5%rekYX_EXrXUrHcmkS1
zSSW^j&%?n=IJm~_HF9-6M1*yOd+<W4Rls>%jhI9KKfw%2i46mIV5&7)O=V%67#4Ij
zs5oAuxg;(nlZ5L2ZRoQ8*L53MY;)5rvI%pFm-%Db8mG(T%IPuQUi4TvJ4!&>-o(L(
zZq69sbm9&sDtFbzr~qXuDlYO`ot%6pyoRC$xOw<Yrd+WQoT4*$U)BGJyVtOBo8chW
zOX|T~?F0r3g97Vybg{d3ny22q2T9k#dvC~{MFw~%*>n&vk6J2JKSxym3<b#cqiR;l
zIJP%#)(>AQN<#<+?_XnY((1o(MruzT5th~HpF7&{701)Jr~7f>mu_B9x?~y(w=ym&
zZ+uWBVT7HVr$`R_PU2!?F(Sn?7ivo$4&m&G*!sgBYr8M243<HBr=!mI?}gp6D*#yo
zI(amNmZ?tZW&8X5Kj-=TD`w;D1jyYQtj}PmptMpH_E(kUqtmZ2ln0Y4v?YlVmG~Ym
ziJ!8Zl(eHX`2r~XkJ(6j7fwWk(^BiLEUY8NbHt~|aTyClO(w-BEQ&g)0*9G~H`I-m
z9(mwRlegDUTYXWiEl3URo+h*$IM)ameCbBH>loD3`iM?!eBkb-w|h4F)@%~RIBf1W
z2un_GZ&i2*hEoZLPtIZyzVA`EmKS1B02Ar5V`m?StX4*mCbg2W5s8W&(K!}};+BGS
zl831B$mQ{!5MRI5Jr1~MwEFlT>ui@tfjeh%9IL}nncClJuQ6Wj+$;IhkgV;A&V001
zvsGY1?-Tq){JuZ+uX>sK`P#>S%+^Xdz{M~s9btVnS$G;Wfi5ad<-3<FiX04^ym@@;
zMd5#}tqLpR&^i*Zdb*&;3ei;(;hMG$TW>!E?{w`$iFn<oNuO2KBToDo`Ky|}J!M6k
zoa7~Lw&kGrIKqJ;wDMrd)D3%375DN#pVZ)bnpPM!do!Q8ms~VTdf`ykY7MEZ-;+H=
zhoJ3P<73ZyMlWV3byGy>NYSMmM2215_G3)bymLmBCDTTUt7Rl0FF1Ru$@%JH`E-EC
zZRdN5b*><P<M6zHAShw*z~%A*E_A0UIuQ8ejgPW{JXe_NUo6+Hy;>1{8>GKA<mlQK
z79N(1#@ETG=Q!hj<s-?E^*s}$E_Jp!Bz|HcIi?MmvEA_dNVX0*_c}?3Z%r9|SHq4I
z=C4vDKD-G~&$~e5Fx~H;2s5>5otTM?@oX8fPoH<u1*$LA%>?hMygF34VfuU%{8|5N
zo*v2v!e{L&|B@fZ8;NHbB@NHRT1ZpEwEQJ1(0s2?jx*sM+mMessaVd%x3K}_TerDW
zcceW_f~wSGM(|~dI`)mrdi!i!+1rkp{5Svj_W3E&Gm|C#;o6d^keVCP?_7_WlM%Hr
zr5a}Gk-_?+I7){AiMhWFF&%sVW%x>mIuhCN`>>@jsN4spq!l5$gcA%(i(n+7Keixe
zF|53t+3~Z09<v`k1@YC`o)mLn&TcoaEM-g+)o9~E=CiC*30Rr?*AZqxeay@9=5^K!
zM->?KfSsOUG1dqF;bXcOqo2ay@w;ho?GMm9hxC*|P(=%E;$bvwZ17+a#{AVoG&27!
z+2c6S8<}~tG#wGeD>SG$JngooVodrl?KT3b4ceg&VUL@)AwY*7U02PyTn<S}B+7Dd
zjLPv)X`mC9_N9ig=kj+UMBed>U3u{hq(O%q>8sZmDDBBKlNIxq$(z>11Zd^T)>ZB5
z(~mCaX^9K8^Ya)qp|Q_}sh<N^D+j4w241BEwp{V!LfKzM5$t+Im_vBeha7%#44V3(
zj}WJhD`1-BgIC32J!PW9F;CC1X8S`9C(VQu3%qVqGCl)mnvTjzgjp6~{T!5|)Vce*
zTHwC$Hbia8i~)ZX<7ywuJOX(?I~#;NS40z5*RK#&34C68$BxidzmmuhoK>Mx^_)pj
zc15(Ds*>l6(o)^7rlECX3Vscf$hBA;M$Gn*W~PWkFGGO+PvLMsxXHH^%{SYlxJj(V
zgsh)KYIT1q)w*@=xc(J6kgnPbrW=#(GYMl+(;zq2{vC^M1r^rR05d9qBql^e7A3%`
z&jndYb&sO|r+6?X{VF1FSL7dzV^Iw`tAOuSmOScRjY@j4Rn8pM?&;%t-+Ba5gp!SU
z@0stagq|>8gOkWG(}1D#jx~@YV*Y2^rd>xY?p!k{Qfbpf`c_t4gdG`$<E3FjkIQb7
zdbz^G%C0G`t!Abv!O^TA4os1Y;jiGv&Fb^@zh&%>q*z8qAST$%DQ1}>T}&O4SJ8W?
zkjG#|DWeP^<kHZx=)usOE>hJ$ZPnFS(dUgIX1Xwovl2e@U^?B!1Tqw7y<^LWPCm1L
zdSoezGS;M0+058q8*6K}i$hc~=B1s7SZ1$6EgDR7oB3vS(j?x;4UXIVz}ENiy-*Vo
zwTK^L#=?`9^V8}|ZB_t##DeuSZIcdu^3?8VB-nUhvgF^9Wg4st9ZYJ72%d|_d1oIf
z`8V8%?bGKvF=s5LrFVwLXSPogF(WB~Ld!-U-K32hdK}Iu1(7x#PJ4n8TbHvi+vpeL
zx-&g4iwK~@{dAMeF>^OV@{9ySOSaQ2J8~nB&w7J~9)am*8W9mdI_<5`siD6|k0UtO
z>2Wg6?DsN!v}Ay;Z_nuxGEmeueaWI3ZVTqo`$9)?STxZfjm<>2mhn3>hQz7om%hAU
zEe=x=@xqYFb0MqQyJHy>m7^&33wk0E7z+q)BH(xQc2VF!<Xrv?g!2=py7k;j3d;y~
z*1_%i#2yMF`71eT^R`Bgt{}+DgeX(#sq(1@-{XB}dR^0PVHxDg!NHZI3Hq>q-?>Cv
z`Maa}q*du_wPaWQUaiv8biPvgjyb+Um{Ek@KqAjL(EZEJyqD}N*kzr%&^x(TYtZwJ
z-?ntbYhm2d{qoswN`Ax^BC~I>qhSm}B1xJ2AeFF=zdkDiQztp6GnKRDv0pA^jKm_)
zPjrFVaL$BB)DEsAlR9{v09AytufCO|wQh|%<LQIRnDfinqC^-_bj0c4FljENio;?!
z38Ir#q`d{bWyH|I{iOH=iMiPf)PdABH1ulTKw$?U>C{#%C`*I<|BcAt0NwasqArOQ
ze!I#M4Z$3YxW9jOb#>XHM}ImIFkqoD9hWxL*c9w$H#3CXEwjU<+i;(kIsbN4r~6%(
zQo)5w<!u+3rIM{tXz$-b8LW!o1zzhvMR<x)<h|LAl%NtO<`j)(yb8`+vwenb7PvP*
zb*26@B`^|V1tNzrOWP>q6KJ>kP;7%SN-fyjUVW0#2|r8=bn%^xb<PIQAp}y=iHMVP
z`v--p0u{JOiGERpdL0h`9&A3Wu;?)Af19Q(cdfZO8axZXVjqMq!a4fKiK-wvIi1d&
z+`C^F=04;mDDr}`+{`i270zFlKbFI+84~OuP)(KhLB3A9Lneb%)r9RK<Cv;@6wwk?
z22)9r>$2k=`I!ALCm3IDICoJ$;%*xz*iO9Dk20s->F3^sK7fx5ZEs7W0}7cVLnlAv
zt2uBp|CaOY*{9fqmNeJz-43%7lyvB~vGst2?LTx6I=5`;xZVa;yLB{ug6!{m%p$BZ
zdQ#u4GC;O0ehl|3r2hew*5;cn6A<wNbo~=ua&u|c03h0Dt>EbWncgo6aY^ED2JxFQ
zLsxD?rsP&ss6@Z-{p-%Tm-!%Y;VWl%Y+Pz~x7Ecxe6GRP&7?^(g$5?H<+sj;PH(Io
z<jnrb!CAx6D^Tsa$~JulBH@NGhY*6hk9AV`o=4`LW{Ae4S*8c_(Ze!ks7lkQE>d&A
zR34!Q?SO=ai5`T~ef878s^w*+p=IR5&ToC=gZ0>d_i?0QuNIR&cZ!MqZl4ZLq0^l&
z>h_?_T%hNl*=rqt%-sRK47Nh?+W5`s(%szK1nbh!ex8t}3x9h)Uq6lSBc;-}OBAtR
z|F`JbTKx9Rzw!?AK;n@v4F1^Rn>!<pb1R%djJ7#-=W+pDXz^lMh(ONV;XR4r5)1wC
zjGh?r1O$$touJ`;<eF&}3E@<`>QlxJg5d>(Z^Ni9-g-Z*NI3NhTp3jHpm>fku2Le=
z^my}8>`4L@!Eze)a+BCZCpZaPu+}`3C9re^=+$2V@zH*W*~k$|n~FT(Kn{Y{Kt3DJ
z%Z$LT7{+hZCnYs>ftwOBeFB(+YiH{e^nu=_xB-X=Dn-;(6#|rkMK~BW1O1F|{2(+E
zGwm7wM04^Fn8^s`aa3~+fJKr{L*tE#yeMZelq>zuMH8XsQIz!#5(jJI!qXi5@x{&H
zp2wJU87?_Aj0B2IyXbD?;>BgM8R7BjqenkhqLLM<8WtEkV3ne8TBAclj8~zwp;7uy
zCnU&D_1~S}gVpL!_mW>6kxUw6833p+S&ad|kR}yYM|@P3TaMS2uI-9Fe-V>B1F;vm
zBhF^qDbo~l64##@V#jw~w~Z>vJ~QRzMnB=r&@OwNmvq4w@+n{NfZ?5Ekaw3Ds7SZ}
z(C+^jJ?!DD&<M4bVf7D@>6>HTay+tPf2u)?*UY@o29RiGBD*S7usoWf-uG~Uh`uI6
zat~@%z(pDX#E*rFM^mxRPkQ#p<2(E!G+{tfJFMhS4<R%*A$s3QyB7R!h3~6ZbL2nP
zAnTjXj5H?BFr3_+iAY(Tcc}Ba=BnTxUKSXy=)hhyqL76<2+l|V-ZrhLfxzh#3qTx&
z{!C9xx3K;YYUJ4yJ%f#ie_T@48|%(yTB^2Q#6SVqj%n!!;1|lS&AfWlYt6WI?eoW@
zDurVo9e>!1uR0<{4I-QEgE|GNLP@CRVUugjKtvYJ?PHKK-jcRQienV3h1e_IJd6GG
z4cZztZ_B7X1~R9k2eFd7%IZ?V@k!s+V)2JsQ(-2}FACgx8Xe+GmgA{o-f|6Cr$(+`
zX7%NuQVr`mh%udD30J++m)q#wGTn$xN=8lZVi967)J<pw`f3COS3I`C>@${YN7Aam
zof4upCH<BU35fWzCh85B9=s4nl<u*>?3%krzeL<N`jkPd8UWN!d&O`$IN}t*Y%mSL
zJ6%AIEg!Wey4@X`^Sqzo+1&`d#^UjGP`Y0y#CGZhYoemeCbTTaQxNYr`Z0LUoMrXH
z|KoGvJq~uxf;WbQfj3ouYn;QwLdvYS;navH*BxDAuQxZu>_^li{Hdp0Dk8t0b{&bS
zz?FB=pMs5G`6))Zq&Pc;>YyxyZMUVDYO>cf#h~USjd9TwMwH)az7_!>8}lN4@Nq<?
zv0F(d#n=~fz3M@cLJfK3R68k3Tkz+dw;IFXpOlyxxxUo|V~tY95;dK^v5Mx%^l}xa
zfqD!XDSCbSz*8s$o_`R{@wAq6zJ1`WR44oObo)>h-*v<Rgz?UJE_ht>s!Zfs&rCpL
z<Kt=`y<4no^Rxvu$YAFUpD!M%AgYDrclw%{`%cs`!Bs{YMOlCD=omB_x<VRsE_c!+
zAw01+-B80gV(gn~FMT|gWD69X&<2BZUf>FM%$+F9aMaOpP)>oawx;K!R!**-v6XSC
zQeHgBfa*|q9BrTe(s$>qcjH~6DsJ#U@C3BCg-jkP?Cat;sBT0no-jy3C{y=K!)I|k
zR`=Eljn)LFRaI#+LWleYlt6ViwoV=nE?8o;VC=PS@7OnY^}9PAKkb<PNvI)!Jo{(k
zcyI{&a#fNo9XuurDUkf7_+F#G;bi-6o%0tx!8@ACR!eJ^gWV6$cQ*?tA6m;zZthUy
z6C{Vle(-f*)7e^*jpRrP?Hyw_`F$|L19Hw#+urnPUhZ01zAh`cX&y4+u$A^$YkTBr
zmFz_Lbz3&7Cu{q+!Rx4HTNE~kHb2+4-VlxW!Z_-4Q0Dy&cNyGOvt1rutgwk}Ky^0B
z?rJ5(yEN&ie<*R6*6ew3jVO@tlAgEQJD~Yv?91(hsWWd<|Hv8EYQgLRPu;!pd_oUt
zUo+s`u)5R@B6D2xw!kT0orN)+Jx?2Q+|C!CDFFYmZyD{)N(V<r{kHMS4~b{|Sl><N
z?JIQP4x00DLD_UcNvroFAyRU^SfP5H4;aZedQ*B*6HWZ?8O&FHV1b7iOO-A~SgSUI
z_LRPK9AIkOp?y0kv@9m7KDpxt-Q@c4iTniO!R!M855J|yP5IOaR_kAD=rUgAx2}&Z
zuRw41-+6D++8qJQw)su?=7jGR&p)fb1g!XHxd#JGYFIF{!vflVy{;V+K+!DF8L*Nv
zq5&r<w$1C7Nf-`GkFmq?E==IE1r*E_GI1=XN`mrmMAFF*d}W9#GHLbTuZ1FL6~*3A
zv*I0pHNEWMr#@L7f|zTUXc#Va?m@>7po^_*F-p+QxYQJgp9ge9?=bbbGU@lfu@mrS
zCKY<xciAmruV4GA>OB68eYpQLcU^l6?a%Gg8U1EYh<aAwlEDHUhkI8`gTnjBT)bs+
zL>>G>A>*4J$(<UEj@rDa5OP8C(5p!ToT`wtUvFhzCv=6L2uN6AzdSf+3)Uwm>uz53
zSJ^sFMHf#ouvZ(@2nX&5IMVDj#*_@Ik-b-2lFF#yJHMr?+k~Iy1iyT$P_i6zHBSP>
zS(LrtV%48zAYkF4(Ey(@rKGMkz2qoDj{zKxgU#Re+2J3%nsOg;DWL1r=c4<snRipM
zZ!N8@s-vE;WYBpdanq|>l;8i07xHhCsl8w)G4@DC+6&CGC--;^wC_z+Ej?P+;0!dw
z3oB_*eA>(2MM4_wFeq$^z`)jN73GBx<X63+abvs>s#*C2{Tu<grM8-WTOIfOZ&Z^1
z*tOrB+tmkTc%Xyfn8cb{_!bmGA;qSgh#u?s7QsA%eV!pg`8P#?j@DX1|MFpan)9gq
zUj#JmHUi_bBX)pHipxL8Q`}&*#vW|V5H|Vlenp&IO)$0jV#@K@Ic7S+`vIWpiJupk
zvr*SxBw4KUatITDJTOl&?q;COeC+#9mo<?4gjDPoRSZ+Z&#k-Lmf1|@UUiij2c(Se
z5AM!mU||~?d2|%&`xXH%yf~5`pHH5qU#54JBPYAT<4DirI6p+uWOtnYMtw|%TR1i6
z0@6r(;RpPS#$Nwuv=RL&94NGtkCmSZy4saAtE?Ss0Iu^S#r&(Tw#~40%(X@zeV6r8
z%NAgPa(Hk0WEj*h`&WLHyg03!YhPoV2$lfq^WId)khx$PjT1DgJB!(;cSO(?JsZPl
zUA6GsP$!_W+4U(LD(xBHS7p*O+H3Ul;g8uHY!2HywS;od+Av<QRuE@i4mfbyRMP=J
zb5h5g%5ds1Z+@l;a#-n&h2qS+KRlco+=M6L_kN;KZTbVJ)hFf+t5asNG`#BObY2|t
zC5ekIa{P^5#ZP%F5wIs$UPSUI%iz~cYQCUXQN~VDBTPOX(W$H_`i=-SJV_-5F+9K2
z{$^;)D=W#k4v)yzDX=BuDh+KTlXpKT*rTlNL;AsUtcrK>+g+<$({QfhabiCO8%51J
z9py(<u5s-Ttxi}Fb2BGz>G%AA@)_pGucG$_WTirI>mza7!3A-i_Bg&;%m!phB@dNb
z9KftVZe()xl9Hn(ajqnmrPxofjA0FwE?ZNw`Z>YFyMGibG7#d43-;086vr!I-?J-`
zeQrW)o2J8qrdW+e_XUDbW^uzfQDI;B`V(SgU3i*5O&Z71VXFx2pZ+*O2R{MsZ0_$m
zaO$E~5%(hEH_9Pg!YpBlL>mi$SgdORgiuGbIb4a;!)E%Yk;Lfw(ACxSMy2YjgL+8z
zmU;Wr`W%6F65<ws@O+|}8sY2Af$%jh<Rtn7%?lfWMqEW*Gc7)P3i6TD)|PkDtmCN9
zb#b`ww|Tv){8Yt0cpgwKk=-(jbLxio5&<t`;wos2*J()7kV=LrGSbF2S2wRk<ZZ2@
z6WU^e=Z4SA!(}Ho)|iS`&1H!YM3I)0!Ekc<rM*6<=&f-by+83m@=I^x(%V6i{G^dZ
zVZWbfTq)8E<goC`Hdoo{(#b!*qzYeWGpcdUFm>M?pS7sO?64tKuV7c7)O?jBq5P(t
zA~oekiH&$86N*h{8ac_VfrYJ$_qW~Ek^5Xv{=bl*H}_)#fMS)BTT@rs@U=>Ro0IvL
zY&HKUdfUb?R<uz?;O>NauvgV`Dj31`w~Ft`kQezvU2VL;9rZ?$NPxU9*;jG@wU8n+
zo|%grTGK&rBTDX^C18S>4wZ~X!(o>vPd~+gaxUTx6z=&(M-tBnzfStx(TT&;?C|Vx
zTmA(@tXY@|-*d0n98mLvO$onpVll<ws_+;~N=E^}$dg3SOt;p|;#i)b0B7`->F@KC
zo4rzI-XI%Ezf31&VEfTBbcK*BaG{VP@!%*TGpnr6P!|md;zuVJhZaN0dU&8eg#dlG
z@S4rs@W4nPgC+&jS#6O6M=PW`7)*kHn<On55^Tkkk;?fqd`0<OXFrtd(r}(?C=~&O
zkElAto07sRsni?cyeuqISw?c_QE*3eX{MZxp?s&%+6l1TV1_&0VRx+Pr+$w9Ne@M2
zyre|<4!cuiE1&6-sUu~KMG~Ud`9~|{Bv6V*y79+$64;<yB#^RKOX}*k@o&8-{GFRD
zT6~BM;NRTW;aD(3FAn>MJ&Yca9v45-0%(??S{Crh8`{@N+~f2tMcrAan@2=1D#@SS
zd(6GHqoMKTD_knjBe8ABoB>8?gV~DM{0<ui*2~^SAx!P2Sp0w&E$E$sAJu0`!@%&k
z8}A$HrIk;UsNh*R|Gif^O(;pz;=}rLc9hl(K*aDxml+$vy;c}jyP%9ayL-c;-YQqy
z+G*ZW=JRem?*Qa(ZDe2r0_eC@9E59yT1^~U`mvVB11Nu#aM^kvFpg7naj@86dX&_K
zix}3GNM0Z4-_g`lihyVj=@@K(<3}vgN4L01JNvY(&Zqfv<&<iuM)9QceCuGyruG|r
z@@YO|-z111&2Pgn;l|s}QGL-r*O-}S*FVi%+2S9rRQe5}ziwOj#Zh9VnQw{l=QTP`
zZw}!w$nQU<M%pw6-rP<U)<v^>pgD7PP|EQ8dXQ0z?xWQ=7cy%!C4_Lf_C!d&4Uq+m
zh<rPdd8)Pjc4>&g!HxuxaRyyo8#eG5gdnO99b#^4pL;+CCW3>5%LEnN@JR3Ln9ZVR
zsapfF%eG%1@<im;EQ~ej0F%U53tc^(+uYoK9Zu~l+&deh@U@H6tlTj?-or@mwUi~$
z>1Q{8e-={)7$hjU=g6rbQUo_lot2uWSBDC_TJz&g-#ky>&CP93zxCbCZR89bG<0p5
ze!DPdA1`ovG9uiBTo2-x(>Mht>e%TKbc)$D)<?f9B-RSrZC@>>0GMg>_%~w<@2)z(
z80EykOn&E`QCrDri$+~en<725cr^2$KrJb$yZ&Vq_2CvlN*@bNr_oug-GLLjYqQ}K
z4Sd;8eZ)bEm!nH9NIO;iW-&TJPufkj3-8uC-epN%T$007lymrEuw<k6*VJfImGgz8
zO80&G?YKS=Q0(NNw<`jRf^R`m9=t83aA~KK6dza&9U7n#zYVi^x30&=v#5>VR#yaH
zqBf6=nujDN8QG?U7j^4Bw-CApojpYJjbYDO<0}fo?{QBXzC8=52+-;ps%~`O@#<@q
zxd@cLYpcdrY7|Shz-U6}bgyX@Q7NSPFlT*3??_VXKAI0+=J^YST#*kO?u~*z7StxP
z{y%HD5@5cFSTo2o+inR%KmB)Ve!rUn_i$dx0E$G}qvwcAe^_haZnL<JDem9~v&4|}
z)iH`R^y7OExxBI8j<x1m$Z5uK`kNI!*h>VJr_XT|WBGLPlD`fxiImk8U>gjrRTsCy
zdF8l;*4Wd7iWOJ$M&NMLIf}?D_gsQXn?%Tzrmg2`Eflfon!5nt3I)$T$As4c*9QXZ
z({7CVccVX(P3RIcV#0n%Ylj}*1`}~tW4YqTc2>ojqRGnH#Tpz&azA_Q@^3jarPAYV
ze46Ou_qkGzY?P6zCSnMa{1w;dqB;!)&I8dqKM(W?>g)u0&XLW6!m|sDze6#=zwV}t
zdB*D|=G7o)%L>>sh2L3%aZE&N!&Rrp7LJaFc4<JpH*0XQ(Yd^%eoaUoN-g@YE4Z2w
zn@>z8buzkugCk=eS$J~Fn(qRL#;^eT74v1WdSVe7iYv>gPL+M?!7%`RD^k)fIK#M1
z1B>l2)oO%SE18KZWs2QmX0<J=^~}9w436T6xEUT|LSVJUdfgsauMJ05FIWUlt85+%
z0VndkV%vf-=l6V_b<A)shjntM=>pA0`Ud)tXv=XHQ6-8Nd#!kwbjB2H;DW!F9jr$%
zWgQ9FfV26qGXu1=G#Km<thZ4moRTl}9>oR0+$bMhs3*~+m4#BX!piDef+t*4!No8W
zU3%7RUuN`<cn_xkW(T&|hxbDT`RMJQt6_eeQsAaMh4{g;Y18ke9Y<Q}SN{t_&9VhT
zG2Th%8qF7332<4COuJ?{GX}z>kU&-&-|DT|vYLfm3SvV;?<1b~Q9FK_!hB_rQ5hXk
zVWQlCT<282;Dti!_p1_WJGdA(ks#uU8((UB=`TJ*%e3<D03x*VHujj3_GIo-OimI6
zHfK{0E08+k+0=rftaZi{oo&JW9P-t*N=Jsq*k!g%#E#(x22B(%w^;8aM-1kRv@gGz
z*Z)}!JbZBqwR(HY$=(r1o^Gxo<NoFt@#7hAM@?|i;nzb+-+yJ?nqGg;#0NPTRmXxr
z{&#xyQCeYs6$3qsdwS=$j4rQfT%Xc}-z9)gX$#{d9YAy@Rq>c$WQOa=0<b9Olo}tl
z&c?W%qwkI!V{Ddv*BU+x2`(joik^VBQwq@32*fop5nWQ|Z@>9NNW_Pp()fBCisAWO
zsrR7m|AAA!-Rb!2lW4H`Q^3;^U5>KV4%*1yS;QEEXbvioT_3x}nDeS5KtSGDVbTFL
zR5o4C%w0bFiES))s<m?#%Nn=DTx_V|(l?~9ru=d{IJ}Rhh*KGGGf!4wJ><|^8*~Nb
zZABZ!qAcO0Bito@bidgCIA1fZB232in`5INE_RU-bJqG#5s7IT!C581z=Ub-E<(Ys
zU_5QLzyg44h_LvlFd#ZOzy^-Y-H9dTE#H9B9H+^Qr|&bh^>EEE=w-?Bt#Kgn2SE#;
z^#Jld@3%FMlhv^nf3)JOP>T#pkH<)w1tBJRkoqcDQ$n#HPq#IXWmXGwGA*yUK%HH4
zP>Jog57OD72l$|brXie+(HIs|-yD8?P{0q;44qmIl*dzy2cdPicE~j0hk@c{C49sv
z@j@^y8VX`5Ugd&{d<3EV8WbFW)05WoaOJfmPI~&1;%cFTjLl;HzT8xoFr|A@ToMjM
z1&_6g6sO0J1WF_h;@RD^E8Oj(7Na{PT1bZvE}7Wf#a`k!b*TK7m)a2jb=0CgaanWY
zK?ZdfdblvQSHYyLURb8<iAVlSAtF=5gd|>XS1VhA2QlSkrc1w}BK9z1+ui!)joffp
zN}ne1|2bnC*$+lc-JheUZ4RG%nS@#5Fceu&s39mn|BBz^J1KpG#kmR1?M_0md82ct
z<iMWxCtpmlw((B~F%bpPb3)BwT`d{tVd$4+pK<4tkv7h5NpD&mTM2-i8a9+?K|Z0!
zHJaaPQ%f?RCrwjw)u75Jzoze+Wzf=lk0wa%0%ZBMqn7nC<Pw3krXD^_m|tR#V%UtD
zU~w-T{+Is8C0?o>bqM};LrWqSZAluqI+aJC3iPHJ1%chX`m6aZ9|=Nz3|HHk2AWSq
zHmj&q8|&`an7EdN{XV^ub3BordjpP)O5oKhR<ws@Z8f+VM};ZbororlRl^t`MM1=X
zwKgQ-Dh0Z1UyO%w*zZVmh29&LX}#g(apmHrJ}}=@OR3`2Hn(rk?`S`^Z$BrWsWH&%
zmik%Mf37V20Af4PWBvaB;A%gwGe*AO0uA$sXK%yLg@ZqF&-iZ9W)E20_9vgG?1Q^c
zs^#I!N3ZsQ=BAC4<0loIi=W27AAXJQk>|W-(#j*Y=jvi|c+npiS>d8obHB%ljPseK
zkFVf`E0hwGWX6%Y8On-t8c`9zAWAXyqoGTxoydT~eIIk-oP{f8VS|6EM0N(5^$>ov
zs35{wd(u(G<0%k3S`JYQk(3~#mfgAo$Z-{<035&cQ3^^r5jg6?duU06B*tV;GF!bo
zL&0=c4AvExZMOnCeJ5<zJI^-8wXZo4v~4s_mmN|1yE=8KI}2Tg7}4RV7Bg$j?F+l^
z+1FCb?I29a%ulBkfBp3=y9AP_o~_mqh(@h+30<oDu>LijW{;WWYyQqJR~1;ahLP`1
zbfK%Ld$+o}_4jeuU6gsXlB&dNDa@tv<L#cB4A#SJFkjB#w*Ol$?!KqZ-N6=>Ccr)1
z{<Mf6J*p28{c8Ni(>_H#*+t~XS?7S8dn@DnPrhl7$i@@Sj(?nwJex_U0#aRL0+Ksx
z2Bcqbm7UF}YFs0)y5Z>`QtedN`OJ_MMLRvI_cY}I`GxF(z~EPEY3$_0D-RMrM_UA0
z|8r4W=HT`_bzV{H$l-^$X!<_6(D0ksvF6j!kNCFpF>)Dr0%_>|*T=}^pb2{0YRIZ4
z|0b;3W$<OqUQKv)E3gJl0lDxCC-6en$Jc#g1%Ae~f1K8`dW5*3$c4-8hV>bqO>FQw
z1WElL=L)b=;hfuh0wQ_6DDX#Z);Y6q7^8iX_QtLWKR6rXu4hBS|Ncr^!f;O5Zt{^C
z?bx>>&{7PR{g=&=K%Tleo?~ctyS4e8txp2{Ezw%SP@E{61%*6$H8D$KttO<h4s<vN
zZo?cz|J5Ohm1Oyex3dc;%Swkjz1y5l^rYP1RXw7focPNQS7aR5J15Eigh1B=KnI@r
zNpzyg$vw;eNGrT%#?>tL@dxYVVcd@;aDs<azv9bnqz<He_OEK`c?X#hq!FbR<=+IO
z{=eZA<$q8A6yYK4G3^WHWqm^J2FdrdiS_2{524dbEs9nJh6jj4z!jqS)>qCSn!B{~
z4c9?UnS(1%i?KXaxA_QQ2HIh6U?oBQc#8Nh_X(>2sA1YDHi<7gDvM==c<2+%y7dFS
zhyO3C-XT~QVCmA{w$HY0+qU_hZJce}wr$(CZQHhO_q`q6|Nr&miJa7+YO*RbBi2g3
z0djB)FZGi2hsHFyScID3k){!j`{?W?fX-+By&b{rR`}`NoWA>Q{(c?Y@HNF~J^8i#
z>HVL$^#U(y_Z?)oi`;tT40SuM#0RCHAY|D!Rv>@z+Hsye)lOA^#lGP^(X+mTc!3|3
z6O3~#WqL&$v9*`G*9hptd;4v#1)6yKO`a*4{4JDJeEjVN{?*e1ULqEx1Nr~4^^4}$
z-{mu|<#*-(qk#e3(y<$EHy|8X*fUd<`A`Dd$-(CGeG5C{3l;;r=mC=4p@o#j{2Lo{
z`8&U@tx^8|8+!DcO;&^Tn{i+JKT&WNTgrUNHA5`EEcp6*49z%Mrv!O2v#*9B1uzMh
zt_#o>$mzz+4-5c~IAffkKJsCJev-OvhlvAaWP8l|5EMm;18>-Ya~Nw^Gxkp?qGFaT
zxRMH{84uD`P0YgHHbwpR=z=$eqr+gPa0w02-^6S|b{2W+v5u-b1L<RFwULZiEsz9k
z+JeOG2Aje+C^58!F+2owMQGAi9t#ltLc>4>-I~0S9r)H%Rn>Of3#@WC|BeQJdAcy+
zZ}ID?yRp0X2o_}W-ZC0M8IPiz&NJ&nWf3un$$;cT$E5M!iHHw1MBj*e?h_7IDldgF
zNp;{Nh8`kHdeIjmoyjKg>bSAv@aws;N9E?GxsMllKq1h(iQVc2K_f93Il1h&NyO54
z0}jGBkiKo$o$-R6r+&htR!2FduSyy75OQY!m|YU%kDOQg_>PDWmRyyd{cC+Cu`{>(
zK}fs)C!d``UPE5re>1&s53iGmSn!ipC2yS8aEZn2imP(ToI!tYse?u`9{#YzJ3P`6
zBYafwy=Tz4EpIAC?$#eB%oxuqR4~Fk{rVtE=ZN|wk3h+3@AU_9upBi8)zSa+=ltOz
z6p>(qpdeD{4gWpzSm{5FxqwFu!I1Xd6z!k92#hQ6!F#j_wvv=NW~bp8E)!?zi=h>e
znREDt#k4wk%$^D_CqkU5IN0KPWh+Kw6WlAJL0W3sJZn0L&5-fXa$g7)96rbz?|}K7
zT)IVt`iOwpY)yXeaWHgb`BQw!RmjOvI>>cRp`;gNr<l#?4iL1EOyY2<qx?E=mc=%
zWI}j9e*)}9Rhc|Z5QP4<hT{Q!_T>C><96`AT_4syJ6%RmOkO)z*3~RE1VX7oGSpLB
z^VJ0xlsVER&KpV84~#Z}EOmq)IcaF%-G(fXWhkbqRQrfYw4hu6eNw7Zau9=`7HfS}
zD0Ivv1k83Q5|c%+d1jgLQnK&>+zm2vbq{EDi038~Hz7>Br{FRw4|Nl4HG-Ai>Gxld
zxyXf}Y#1<i;7NOF*76Mgwxh4zKG&9SNU!y>+27E`-xNCgYQ&o7{$~ER70A_(Jxv1b
z%fQH|{s6MSU91f?cQi+aP$^vdS)2)@;xieTZ+@6>pMY0%+$$Fw>sim*Vz&&#)@YHp
z<7nD_CB)doG<b0%1Va2?<X0@fA3NbVQ|_ga?;ooSQ!)fPlTIx0LK@QSC}s@QLiSo<
zN-cfkn0TA#4mS+eLKC{Q>yT<~n=0p?T}IHC9W1EWXC4kxZNp(wu(a>BmAb<}$clU_
z4mt7r+W+uaa=938TCn>V)CG%lLxKZ27(W+O_7WST{FeS%rD2)}un74yZ8gF{;v0do
zI^Ku;2`wv*(`t@yF3B|sRDtE#vYSI1n|>VJ0Y03Z1RHM>ETUnd;^aFdB4(x!AK;#0
zn&~s7e9*q8+z$Gap5`fUOMYUs(X2lM*f~=iHf@EDt7A8S%K^YEb)Y$Et|57YfB_O*
z+I8=C#;0yt$SH*6{Veb@tiT6LbPq=vLBAqbGuW$4BgTEE)(D~q@NU&Ylcyv7Q@Vmg
z(}&2)64;S?U?=(}W3tGstI{%Yr)}u-peY04?Gn@F=u?fVUV?x--O3%~7C|w4)$<01
z)5X7_haDZ_cYmv8>%Vfh{Jwk{3huq`b5}buE1kbicXfY7E`0+^!RureJ4^g%nt!!6
zoo;>EJO7Uv><fx#GhbzMOX<qX(nb!mwL<k(c`{#f!>IsZJrFKFdCdCu3Jn{x?2$#n
zw=r#bmlplfc5N`Hne)QUBs(hk=2FM0^O*m!NcC_}OSv>}@A-X3Br-^EDWaOW@2@&0
zfAwkB9Cb3*o_4%c|L&aNCUiUQBF~kn76KiEIiLp*SCy90|I^Vav)Sj5_i?NF)W}2j
zqApf?eL4QsXJ!{WUwf{iD`5$6(d4>ani}TX;Jkwut878QK>y+1{0qsOWfAFNg`Ou!
z;m2-j%5U6&IZiYWl>hL<E<~MT%~xxCPdi}J7m?!EJ%v*Os%fTbFWo+euhzIv6Qb(>
z!xuvw@N^pDecSZAVqt&38MDmn7>Wx`?bJQ(G=u6=U%$P4ioILvE+vRJknhaMx2w_y
z4jhncqE&q2IVaxh=wKFo9aO8nK<QSiy~if{(UI&I{Y0qzstKn5z@v>RFD{G%(ClWa
zfx|a-mfOw|D+jKgPCvah!N5BQtp{Q*YQ-SseLO@q>EO#cRtA+}!)x^xNY40u*Dm7x
z>NR`m+PhOqQSBHz4`^hD*%Dn-aj>hF_?6qZ$*LAtaspDmpFlCV%h-5fQ(ApbI`3cB
zMg|X4PaI3FpH!aG&`*BZ*_4NaW*w_rlZ2h0uATRt9DZ93&a}dZ7`^=PZ#C6?+l=}w
zvb&_L5RO`xRaohy!H{AgtwW{$&j`O<xWO%xnNkA*l+F=FxA>mOZ@-O*O%C|*IkMAn
z>DM1tgDTKQG1ORl6d?I3r3gDn5wI>!53=a!4R24Mjm`Bx94{VQ(PE^}k#Y(XA7{)#
zi@6k_r!~CtCpSAjx6k|Un~8@i6O=78?-$8p32H)Re!9;H2)j4A_`Lxzk#8h@1PU0f
z(v~?lSy{b&0wW%8Y@7l$%9mGQG+=1bH!m}u>gWPAsJjB3+?<~8fR&HWG96<$Q_oXY
zptmZ;uhGTt-hcLARf=A>r->xAF}Y|Fy<0p*<NdQR!?7{3<(xhM%AeF#NLJOaDu}6i
zpp&)<MLmcA9_V`g_##6eou!s^L~z)D-^T*P?_xy8;kXX=&+`W~E$D1g)4O%7ZVnsk
zXq+b{@&2$_S3dl+jg6Vz#mB#WLMRt*?Haf&BzToad_kmayPo)vmV%d+rnL8V?zRf*
z{>y;i<L3Q^O=0Kly==HBnFSpRCrIQU9;3gKwnc{ZK_ZF!7mdy;a`(ED^4K<7EVA-B
zo*TtHq_u4Unu!q`DGtWggydmm;lY(XI~bqGQLrG}>Ms+%_d=wss3ai{t@USVW9-rE
zLDroR?}Q6m$>aPrtYgqznJ`>pN@BK%>yZP+T7~JmFtZpcdKkx)x|x_nFmQM@`DR@w
zP(5Tch7_YHq|lpyx#m++xYU#8HO!#YJ})+m%M4aOnuQ<|2sE$sEs2v~6<BMPt`pJm
zJ(}99(yC$8eiqpT-2$gqJDoSkBrH{xVMo@1Et=p!mt}n$j5}X&apu8^IrmQ-Sjd)f
z<~_63$+|qc*WE5z*h?L?lT2dwZ~NyN9?dRSXw5%U!d?j?lCw$m06`|H3V9`&H3Nmy
z3|iXXTqdc8tdJ|-o^Zw0j17Zjm3vNnaE^PVMzs7L@_KBk=!*a7bFlM?c6Lg~o#XFt
z%4xTsZjit!!DDSzX}7A+culL}mu{__Js!#+Nx>T!{xpV1Hwk_g7SbBuc3)HpB@S%w
zZ8`VD$Ewn=j-87O=2M~hM9*s3chDi;UnzL66jAbfZ!^}ZTAEnKoOFPG1(5#UYh-Uj
z;!}y3r6S4wEq=K1;~AJ?QKI9?fotF`of^=na>VylG>F-Q70;Pbwba>i%kb`+30Xv~
zv1bz9@efJ6l$(%f?(SUf@$sJEEDJ0s-02fLsDB)nI@{ob$l#s!cv*9xqL)MTl&4`O
z&h#FuQR|>19~rwMc3n%^GSarkSm=Co7TYLcsl_1|9nsx6E+GXc83?CFeV(~^(-%Nz
zn!Sp{*QQV->9Ylx)O}J<pij;5t|xzo-MG`b1Oo)aO}4tPXfJ{pCwmU!`#3rw6Itm0
zPKq(K`iY+&eVm!zjQ+}?WL0+~>)ya0eM>GVMzeYk>ro(A&gn%%Dh-0dq00=KM_e5I
zcUsK01%h&(IMho2wLUt>C`59*LKfDrxt<3k)I{7G=3_R1T-_8Oa1|uTigN-LmeFUK
ziS*{e4m~XT0V;z(+K$l%h54^5;d|8K(}R8)+x{TM;RYoNg+ysU3Hl7B<@1vq$e>h
zT4$_Ec%mGkwIF0#Pkdfat_+lT13s3rsFclKp1hy13x8g%)#;R#j#mUPRD#BQEh_Ha
z-bP3}>!2G&Z_c5r$i*%yBN(x*;0XbnAa*XmAn5fb>4eonuUQRYZSpNb1amx-?e&VZ
zQ~=*>{T>YJIdBf2_RM<9vGxh(pp=t13dLdmC>&~VGqzO9B3CNV-XwRg0tiI1bzYq1
z%_Ng8{@p096+9rWUBa5Ob<vBO#$fWc58s9-Ymd$6aDG=J_QqiV(~-zquaWMp^C@bC
z7@VMH{-x5u0l1({nbf*d&eHHNfrRzOKFrMOvJ{&7i8R~<_Nm3Rs=*2UlXNOObP98A
z0Lm=*2@>|4+c8LyniopmvA#K#x^*o??SR{u{h@FVZYT!&n5R()h*m;PbP@Jq3PB0-
z2D>OlzUs0=25VPBC#J8}93yGt{*Wfx5h?PD)&3ltcPekszEX7BH2*p#NYo214i^#I
z_}g-XPDI}oC?S0Diz;Jl6B*ae3yycHGD@UFSo?iEob&NYQWvd4;g-!T{<|Kg;>nU1
zy(P+Q_32|KuZm5nf8(D?!%YuU*&uJ)Lh()_*h@KbX)-%pLPTJ<bsPjTP4c@P;1Ys?
z3B2F;@^2NrqBe%7;Bl4JwXGs{VT8#frm{w6;n#oM+twaT-wwRrNb-;{i^I3Y(7b-b
z7o&-30}5LV4HS+15T_!AQx#|Y*9r@d_98rBl63??>%(;SS!FhT>V+Q|;?F8raDNzw
zdm5!j2ddO`C&IE`{^5L)F23l4O_tLaqs$o|vo1(eSu_u~bgco6&UWF>pm4aIz*p$g
zr{N@x_%-;mGZs+ctFC=Zmg&%&s<c?45vd0E+m?jGsd(Bs{()x{r>x7IGDk727&K7k
zRBYmtDohE&<eWV6z#td#o0j7eOaw&HJYX?^G&B@b6s*FB!2yr-pn7&#-@Yl<fa(Sv
zXk&Lt9<+<qgTZT8IGm&+Vb{jr1`S-Ga0fa_Lh!!G!K=W>39YuTZPMc3hK-xVMBI!l
zQ){{_+FN)vsf|vN`JW&J0Z7D$%Q>O|{E1;(6k=yOWor|J_X^`Ns|N7jC>VAW=EL!Y
z!_h+hEQ4agK3}J_SiK|(Q24HR^+B~e_A1xn7%V7+p!pq<%(S&f3nCN6A-vpS&5TRq
zoKl=6j<{sh(hVz}G{$(`7WyJZWcY_$qavUr>3DJ2!-@~%Md~4REO3;3Kw=Ch95+LY
z{og(w?liNmiMx~n{n3q?RhjsZu^>(hu*J}|o;Ts+#Wv>BZ*zZ7T9k=j#=0PEEZ<2L
zu&4K0-HNO<ax@1@^2q@9DF%KVH9N5Ja%h4w&#|Ma(GtI(VMsRRn@&lAy(URVoG0v4
zF?t6r!CF+rr%xYO9OrC)x~;vkwHSW&w^PJeAVxXJ(&@J<d?zw*1sw*1+d0u=G=$=K
z)cb@WJ5w0#r`uC)&yw3<%|X;MJD;kX^9>GrEJ*p)sNPvo{GO1B{rjt3ft9z5%uAo$
zd78{U@$ybWyuzkdJ&iC7N697PEQ`araD_G$1Fn&73u!Eio}}}39>s^r=5i=^&u5#&
zN@({V(r<=Xd2O<#Ti9Jxd5^SD9n<Qhc=}~v3<yvJ#(^2gJvtJD;lhvS%U_f`ef83-
zPkXUetTY(eK9pa%y5A#hvFK$AzlC)lK!3;xu@mm=qa%%q1u!L!)|}xhM|9O(*emvQ
zQ025qfb#lpNdzRguwyCExTL|PsQq<p77X8lk%3uOpbm-QX@H+*HIik(Z`oKt!dF&D
zGCYBgeJ6LR1R!4sW!8#k6%~6uyj^A>iWx^oeM5q?`&`6u<T5;>vCj^<I)aC>_@r|W
zM&ua|W3Ld#G??cc8S{s_X^sH-H26_trD=EXB18C1a|ju&@&-D<+3qpSsUa(3d6T1#
zQp|+-CZ;fL&fUBR1t@-Rgu-;1tE;$cOnNCwm@*L8x))S*+UlSDBVEKsV23=F#YK=#
zR#q8C`s?rESpB5u$yVg4X5rqK@yRVxfQj82l9t(4319t7lv1o6sM0{y1a9EysrvqQ
zsU+gjEyfX@94i!PG@i}{Kb`HV?Z1IsrI(-TFd2ChGDPa^Y;=g@LgzNV7!=_Ut#L%D
ztsQ2I*P5O{8aeS8Y@7D`1MxZUSBiNA$?_XmS~c;2noWStR>5q}*7c^Tp0d$zsws^@
zx(KjU`@HiV9{lX3{hA%?!iKpjlket;2)mCy%&wqJl9<x2)gO}Sg)7NZ*30EusCf!&
z=iusvY3^H#3iGs0>Y_7CsH^qIynZDowh~b}VD9&_H6Z2pB&7}JN+J0ZXlZ??j`1VC
zG$<$*{Cbc}1yfVcs~wDd0JeSTpbnHFh*uy6yR|ogme2gc3=jI)2{|75F>XK_nOeU7
zaQEwS?<^GyJB3JQkz1us8;7Hbn+=IJqRb1Ii!r+%MfU=3pg^r=<Iu!lX<@S(9c;^Z
zIzO{@CWUp}+7Ki|{t~kB534?^OA%oDkDEUFL~~B&q(jVNzDM=xjXoDg=(09=WV^5>
zS0Wvs$Zk`(vC9bupNzxQOhsmn8jC=sNkEEWNXk)q-n2moU>TL}G~{5$C1jWbtLWx-
zJslTsxaXeT@b08B6>-MHG_+rCsTV=Q-eFPsWQTa3p&{PIL0jrd6;I!>44GoM$R@FB
zD`4=D03k-Rf7}3(Kd8xL!?XFx!nGUi%g$wmhigp1@T!T=KYKZrVxLi<x?ZKw*2wln
zYjf@R)=kZ%q$p*plL^@d|5#hgITERYcG8gmECFO&ogHrI=j@o}EcQ<f5zrGFT4Oyn
zHntS1pBA~=dAx}~3*FWQZR`gEHn?+CNfS$Vc9Ef)8W=US(E-H$=GJf``<0kW|Fw!S
zLJbj-rd2r=eJ!cK-cyW7=rdGZle6L;k2`bOzv#@AI5;i{;dN}tWrWrT6LLCr`qMm<
zIO`l<Pu4-r>q2j8I>bAw)x%7)a~MYJA)HJ@0BJgGso2Is<bM+s*6<+z7T&XN0I%2N
z+f{1#U=0#dlNR3L4wtF9F*Qj(9s3v*s1xLZCKfrq!ls0UIdJqz^GqJDZk;&1bC`=d
zAF|~QAVHUuFGRQa!~92?&&nr=-9NcygNj|-fAaHqNd7=$1==nawUV%Ygjn}DMptCd
zt1T&FNpsus{;gO4dy=-k)50h7wP-Ee^nJ#UK4+#h>k}vf+gymO`Bn$ar&zISD%0JV
z9T+%l(zv17_;5C2NN-XG2*aCxEvI6E9>>8hj|Kc)%`pk7n(aF%X9BeEGSyxIp-;&r
zvPNO8Q&;T-U;LA&GIm}%bIs7osCywBL2YKfJ|fZV)q?9^@)NAOSPBqCnAwE7YqMq!
zv6OxMm>1(F;0X*6Ho9LKqw4c#Wi(M28HER#^2Dcwh?SXTZ|Gc=<h@2ev~n56ylO-9
zrwPkQW`a0zu;nhE1h=Q35Q7<NKo*GkEAkyi%mdQi5g|?CO!gIR^3ypS!IC=PQDNQI
z(2=tp5@7v(ipj;<56*$rYY<3<h6vM^XqK_FkkIt&;B^2TI-_t@kt$gBnw2{zwa9bj
z`^&TEQ5G;=uI_z(`ZzEFSI}T~RecP`Q3zU#mb6NPT5DaqL0!=DZmWA+3%3Hm80=o&
zlC=IO|3CvsI*xSlmzZJb3T}hNFs4FqpiCZ_%c_<O{-G@A14ZHn1F#8BeRt-Jilc>
z?i)-vd=PTw#>t1h5&=DfuApr0@bMnMM5W8KNrrDOWaJu*G|?g>uAN0mmnU@et^ozM
zlNwYP6&HEf8i?ba@=&|XsZkx*a&v=t!OP)iJ?butEgwf^m&ZI6rE;JuY$}|1DHj$b
zUz*Z%4MMYJ4K-7-@tF8r!YfeZwJ=;FFEWPLBI(b7Me|ITuRDWrm(tf@f<Q%qoXO6B
zYIHX$+em5i?}pmxnTN{(w|u9q0Se>Chdi@`BH-Ffe<HN)&Ykx#bc|`h$q$M_5H}A(
z1LpdZcStpuF-4D=h?5Pd+i+_5i@gug#%c9#&R|`&@cmg>EU~hq3b2u%v|PuFVswAb
zmq?;<$gQe@DVK;>dOEP|>xL#t2D>zO@!&`Mpa2N!MWDD?7m=Z#Sa=+0KBOXGngk(<
z5Ob;G_jt5?DuqF7)W0)g4mm-^!0DTbp^2s**<r&jlQq20(c@>*wd7wG)^dTA1Z<Xb
zPJ?&Op?6Ay>O%2ewnP;td(4_~H+u(`lk%dZnP0zGZ+*JPdaCF;<9UHddkx|_KeiHs
zrk&P(N^Kj6v~wZbL%vNKY$;3&I)ceHtT~DJsN)pQAU@{i{0=*abjcvhC4A-L+%YHq
z8h5Ll`o6<I_*RhBIQp%;>s@>?IRB(_|H@?l7SH@Dr~DSPmO~2w%u$oDT5$jeu&0P(
zjGWE`U@7SEsM&KU{D$Y+?TMuN58?~Y!}el5tTpb?6S`7g3@kTt4m=lu>dXP9pe(AQ
zHFEJxYFkNkB&Rg)PJ&f)&ez1ZlP)$3%1N8pP&~<<JXi3`{}yo}T4U%iIK2@e)|aXe
zcpv52sLtm=bYg|ox+bCE-0Ib~0sK?0GWMEyq}uTYLD;$4Sx6Q1v`spwm`Zyq2L*cZ
z#u^~nNn$u8!2xKDlNwhs<z2!Rvt!_LF8;>QIs0XMlZT`4K%3W|^uLiGv6);jJWTKy
zJ#pI3+-v`Jw#W7RZ$!D9>3o~u&#j-)`PeT`WhPDY*YE$EQXaQQzn<3L@)ypZ)bTT
z$>nMCKvCOH`%rWmY6?qY&nG|YXRT6WejRK&`RB?uou|fiZ?)?;G|;ZWi$TlK(UH&f
z*LCP;_kVjE51?Pv_;Y@T>UZz_>J5Gn+`P!kOKo&VKC~wNxc=q#!$J7oRoALL{~bK{
z%_e)H_Fc93KYr=P6T5jUKPwx%nxCHpmEU41W71)frloz5P0g9lDlr@<%ZL0E7xn~d
z<35#$X8AqS<IJ8f9;T4tFD`xVjE)g_xmVdQGl(&<GdnlgIUxQL+bTluiHnDihuMcB
zY97U9Pw)Me-L0XJwFoo2H{V^2t*tWdEPo>_vhcp9RgA04CT8wwIFnv>Ze9^)8C`EZ
zk_S#pYwNGXl{B0#+&?W4V-|H_K0RMP!2awaiDm`|atiSAf9%sO3;fq!D^R84v8Zh3
ze20IEs+jm1onFKCb-7>3C88tv^5vuK)$n$&?)AX#_Il<VIBSnjeK2?SHD*TtzWzh{
zz6$S};OYi1o8T#qh7od^PIqQ!eSZ@JQlirq*hDagKpu;#GnmH13C^+m@xl7`>+5N6
z_|j3k{6_r#&E@q4G3Ld^IlJ}LmoGmeSjO3Ii3hk%ULMgy@mTHF>-4yP9#Q>TYCNcq
z_!S{$X7|U<&P!4YipE>7%>kzAaIsb31wrq&9|bon`!J}=DI!1U0P<vS<_Cs%V>XGa
zbwU=u_ib?p!315GPTTaX$Tj2=%q^h3<nP}~!Di2*ehnQOTVL1MIPY6C$dZjK>%tG|
z{T*Sit$3B*mm-)l9>1oj5zceVU5r&O&+cB|I!7S1otLnz9zMReeX{CRkA07BH}8=4
zh}Fs)DcVkD?U&NPL6NMQ$HP{^Mt_V^EG-7@+`+@cKWSHvy6?{ibgQ1h)tO^mO6vYh
z7uPceh$X`nExJ*0jH#iaqXPW^K7Nn}m?Ep%*4StElQP#1867kmYxS1JUSYC`qkPwm
zk;I8X59cnK&i&*5NKo=moM9X<Ujk~8?$yv!ND4s<SO1@V1Eny?QNMAwB>r<FmKzwC
zfGXkrH-gvk#j!YtHbMCn(+<<%8xyG(;|MWA^(veuBXWF~Bzap$?8KX2b$0A4{B=2y
zU&B6(ctZ_car8<)YU(VCEb`@JPJ2MD0%EY(1KF;7L|F>2pb_90S#yo(1M?p&;XSFm
zgTu5I+tXAtGpOym;H<OR?8`=zvV05p*QQ1B63#M77i)=5H^*lv@r!JQa)sUloEnL%
zt+`s#HP?+9%}%JU*f|1oEiRZJD9~7x?92BtLu}V9hPy=zMz`&IaqmBwX)Sr<=N%|V
zn`uj%3Yb=k#+8?UH+Xd#Q%q$il5-Bt)UH((s81CWZ@gIo?<oh<Bs|J%Vm3Y{AJ}ib
zM?a;hFw>*4X)N^VzbJV}Kd#-K2W!5yeS;f6pTN(*J!CZuz!x%pA1HY}U}gWO?DK?<
zV|wI|RKBz!ltt^=lIK)WL>b>AA6N#A&w@-Ye)jO#A-MJk)+K?kr}67)$+(OPYfJ~Q
z6}(xf&cVq7r|vi;1Vz7r7+--==8em+_-Ps&z2Zx1kFTW1g)mj60BbKIz(zA3L52W&
znkDXjjBpDL8~1f1n{^~KQ5M@Zn2ImTG5cFGN~)}RN@Y|Q_vF|;5o{Z!OpNtfi2XJX
zf0*gSkId;xXcqH8jp#6cOvinzaQ@sg_XhpmPK<br{I)hK{qLQJacW!2kHvZI|3rn9
zFIP`*t!y9@mJjY29wbF?k#>XRGo|wkXEZ}G3%sldO0bGL;s_ggCTX3VU(h7fGt0xC
z9Az@<f(v%Eg@5Cp0!|047v%MHIQlZwUAOP{LY4%(SeJ=g_BeOw&@d=6XtoxXG&y5Y
zpB<PH?1-%W17Vy-uzoPo1oTNWm%uX=DV)MC(1-8vK@W6VpHbv61$QIPOef%JkyC6>
zH>DcTYY);hFh+0US1wb`Y*{-Oeg3AWh}Jt+Xj<({jL3$FgImxj)<cXnjjD@2VU%*
z3AO&ZpMzy5^zc$Ty3YvJ*o2%jAF@L~VXI9^2$MThA}P-?6n&fyYLN{TFp$>AU}n;`
z|Gf=Y4w27^fO}}nm+bkm!W1a|W`vXYYn`HyOE)O8Yl-|r_xgQ<31FG<{ths7=ep&Z
zs(V)irg(y+mKuCJAgDuod%cOzo=~Y}rnA~FpeE7;lXa?-w7}y$;WB}lt2WKLd1QKE
zEl9Kc9*ay~ti+0E->@0`J1d?g5-90|^6e<DCZ`3vrz;UZfQMB9KaXE-@np}8tDqLb
zm3}ps2gIwW<#+Z@aMSF<~c<`%0eF=M#)bu?xzUAz}xio`|s8%H6TEtH|%#++O)7U
z;r>^%{Pw?5U9Kr@=s%YGz5f$3!M|=>BXdWntdnpPy~imYe9HEWFO1{Rd$AoPOVl7(
z7umM9$V`+Ju*5i7i^@hu2V`*}Is1ML#4y`aVF~jngw4nSB;czfpF%`@3j<NhX#34=
z_2`j8*D8f1EG_^&OXRX{)BDF<-=Ft&hp(*_f;mFfOcxFVfoubau3IAt$^(_-BOTuO
zYkf|Sw{rjXTN#IG;Pv}?Q6Fsfz-?yTyz;lV2j+ep-_P<cJ+@o}Ns2}_WIOCZafMNl
z-AWCQccV$W=S3iPFj+gRV7kh#o^&<9y1FFT5QToRsXpOn%YoG5F-r@W0+wGv-ywO(
z*j7taxLv}fY$UFy7j`(_R2NXHV*Q91m|Zd5fqY8G4yFT)>$Tl3qsULx%)g`A)?zl6
z(eBx3rq(5A`iu7%{L*i)DB$z<biKCi(OBaf+8A+wCY(?~p3yEucB}$-l1oeCpIx}a
zbc3u9D3u3&bBZRcZeLy0ZappXw8usDE$|_~^-vOwHwt7&e67iF_>gXmM##G`cW-j)
z8TbL<xIvjb;-pk0WM?LZL?AgU3JMEN2~jx_Lg|ANdlA<6*&J|%492sC;|yE!f_aS0
zP=%Qmwu7ZlTArXm<e3Df5iL?CIU3xbq%shBQ<k<hW^EKZLA+afX#Bw@-zyjHow+-e
zl5ntKs0Ck2X1TT!k^p)i%{x*eZ^R_r_X?}(kPKT-kxsdKW#}3BLTIJu`<AOs325&-
zq-Erd;fNp!)+_i@N-FggjG!_2IWV&n+5$mYnGJ@RSj1Nk39%Fjc4e$|vO1xi{kQ3B
z<Uhn`&#o_mKT1TY1+zz!*=%B^$d~wDE&q}2S05nV^G#dO+t2R8)!I2wAKTX-ili3I
znKEB`Lw-{G5%-BtIc_L+hIj8|zc_4ltMhf1t*C4Yt(8UJN(5%7=#S2>sTnd_ll$}`
zKZgEM0D{s4VHl<5L5MUI+@u9(sy7v!{h{xVg>j61|H}v{olxuQ>8HX@PIhhqtULY$
z-fh-xcBJrYtkVLUk|bJ~&VoJ@;)2QDs6Mg}1D^(*(M0WVVB=vig~Cd&-UYo)GN}>z
zX-NEB9C9_8q!sLZOR*rcN`k4BUGOqeX+&aJsS>FQTIpfE>4$<q4~RX}zxBVYQjOVI
zeH5>L`Yu^^cbF;E-auJD@QSh~7pj%=i2C5*20Wc|f+;qIy7%bI<XBE=2e}COVvdy`
z8dt_N%);?`<OkxF16!AF3*u~++_Nt~4<5H>rn6H7fR$QVh9|OB-4htii;N-h*AIf`
zGY}`L4`oNv3xGzFlMSz=4l1W@%Aq#*XoKf`Ohv~x)K9kC8jkFCwf4l|dGfj3P5zv&
zb5cD2!e`GutW0!Nl)6#;1AGLy_ed|@?WGs+*w{rXiS~HVP!=QXWT_!|6?9%b4CsCP
zH8JduHL^4o8sypC4wAEGfaA20D-Ex3!3!7w#DwmumnS!*&hH-#<2B$V2`K{4kq;~E
z3R80kA0hAZ(m_?(GN_Uo+blB)R5#3cf+`*_L!pk`P8!UT3Ajz4l_OfhSK&JEDA&ov
zIh}=P+A@|_-|Jv@aX>y)aLq`F*lZ;(`Rrsw+I11c)Dz=C)cv+Wzr__L@Z?qlmijmw
z$N09|H?f#9)K8ird?8u+Uu7EzdL253Gh`V&eJidR^vx~?OCq_D-)ZDs2ON^^kG5@U
zK;xbYhL)`pKOmhx9h(KOH;NzTOc|tB!@B4A4lX&0g*uRu>XR_kt;~_e`GQ#^aamKA
z3#h|pal&7VH!@cmsthMp)Z&bZ%j&+Ia+q7!T*a=xgN_P*{P+bQ?lA&~ISS$>E>qdR
zDx=5blYniAM*}GN?YFAhnfXV6W_x+#=Tzw0_lm5B&d&liZv&*P1*qcxmJ#4_@mAWI
z5C6F}m^(Ig6%h7uUMYCSBV{t5SlH)(9=qhgFSTzb>ZeiwLL|XoAv&0y6x-nl%%e#)
zI)UuhsnzC6);rlF$sb3ojxSr&1|q4%-f(EIau7{*`u87a*faw$>f`Ta=JqsO>aX7B
zR@M7&==ARatvwy^5{lsegz5uuUgfQs9pl6+P@FN$Hdq99-4NWVj!$9n`?y}nINLDQ
zSQi(ND(vFDlVy(^<j<U2dg-a47R#}H1mfQ8dfP9(v}YjjRTHV!krB82==VLNsc;=s
zYuxMBrbEA~w-re{zRvtdt83A9Pa^Eucg0j(g&WaNbSAab{gL=)q`9mCc8K7g5G6@b
z44)V1*3-m~yCjjWU!;c8S7+9&y=AVJr(j{2jjsZVg1j~uLkZigzNoR3@zA=?)?H6;
zw;y!JYkfc*#jY)y3HJsU|GMXAfon@?V-_m;=^l(Jq=W$OO*h}xr~Pdm?IrT++xan*
zDyQf7!IzV5tO|h!@Y865GPS~&9cCyd31}MRi&^O#qRJN^ZlOSoU9yBS^#Ht`R=`y{
z{nIduOGoSP3si{1L|`lE=O=K6PE;u<?>LFGKBk>IV{023X%FWR$lWc(d;k1qWA{$E
zbhnB*(6XSJR9Sm9;#phKYJm|-#XzsGT~p+`3Mq<5+#pLxBoh-01gBe+FV@x>-p|tK
zB^Jf*#7@ykDf}ee-?-KCvK<=Al}}(-h0{X|dyFM+@{A3hLb`AJLgn|y#KS_C8~s8p
z_5Ek1mdNA-r9TN9Y=FvTPSA)Ew3Uj#Zk?W9q#+Hu>X*y+H~l|))_v?1%Z#U%jdjwt
ztLaq~Xc3vyzIy6DuD5o#Ub8nuRe-^(qk@S#*Vrqck7Sy39r%iB($bCt?#xR08La%m
zE_{jsZ^a(MUT98+Mg?$%4m4VZ!Vv@PDws<NVCYBIzDwrGLOskYQdXlL`)bhEEA;xm
z`Sg5s5OVVt37bm4sw068An(F~xTKKLv|*5EG&VvJ`<j)JjdL(2;YHp7?t3k`2*M#r
z{oKL~2}Sg5P3wsvOzSCX+2!CIj*qOEgo#8%IDo;)W5*Ebr=E$hBb<MN_wYXD{3|KK
zCGO(U$G4764Pq!I{-@xqGB^Bo@8Yi>GuANGfgxhNA&$@F^?_12Xl{*Qkdvc1FplyL
zHq?|Z^$x%%5e2K#uuCQ38vxP3>VET>E+3C4lTBem1t1G0>4hx8TeoWm^IK;FGa6BG
z^GQ-M*KtcQ$<m>$`)AnM<~z)Mf`%Hh81VeJqXaCWxjvP_eD+ABS#QSy!6vcukh6+p
z9sE&RPHb|e;SuH^=LJTtN_?|0^qPVtFKW35I8W=?jlMD~Eql)wI;dS@)($}3+)XeY
z4~uY!!4@A+Kwh1TjL|gM4F1+c<qLkwbpkpr4m#piH0gj>vB1M+h<)Aq{)%znY+kSL
zuU7LfuR5KB11BazVWr*LAN$#->^$}?RCKon6+Ky(=!u7^Y1y-WN_j&e7>0jovG-uh
zqyaDmN$}f`Yz)Zc03b;aDWS!UpF2lZ&rXSTlM!(<u&7-M5fbeKW6<FXoM?$wjzbM$
zq<+F`1e<~%XU>R3oC|O)_ebP>Aq`0A{SfFBZ3D3sV2OJ*%7bn)C13Vpb@UV13)v+W
zVj;}%B=4ux!GZa;t2OtFp1)D#xkxmxAn<ZDNFKNk+<CTARBYs+?nSW?0Dd3ZhAFIh
z%sFU<e3Upbe+#$2eE&eT25^sC2y@tzJYjFax~1FKA?xtc`{jsVQeNq1)E8rj^y#A$
zz344ZZCi|0Y485ND$nqM!kNP4*UYz`qYzWDLnrRXFx3=rkxSd8{?!0EUh<tuI&RWo
z_k3Y(i&ocVswUjEmE(X`ag9h>m#RS89v=14Wd0kns3}w;ockxdicI@iW=jeQiA_m+
z(Jtj6#V33h>Tlf_ydE!uhEekz4_*3c_F>^;ckZm1BV)I~Jl$auMLK>%qYAH?hD9{`
zt6_7s7FYCuKg>z{UfR**Y0?r4uHa%u1*Wl|VvhNtHyYNGy$dTFMFCdeTtr;==2ay*
zO3{Wj$Fv#pNn{IhSHDP28q6b8nEEuAbTveS4<R}-r@#)MOFd5RIdp@awtL(^f}BNO
zDaLJ>g=^$_qxul*eiZ1)>LLEVcLW83+pQJnG)y6zGOP*=LxLgh`C~ray?BrXWbF;1
zTjuB?f1KEz*k?Ru$We2H%hX1yUDU|8abjGC<X8x6hSgyaOWw78@{)$IEIZ`-eMb8j
zZR2iRD{CDY!1y{3&!9bVaR+#!*yw4r$<M^4;AYnySr=Bi0Vx)m6X}2mqCbm=!V82`
zW`zh(r0#*}&;>{da@aBa&NKEm*?0YzE9t}{D8dxYQlA%_(CUY5bQQ!Ia{<NEG(io?
z09qH!bVXq`K*OB2WMr2WVx23>_CVDr?gw*sN<YMe17i-HHBbdfA{^mCOx8GbCTKn_
zGr$@u_yg+(Ie6b{^DXK5*ljtxu3k`jy+~A*DgOb(!(m&?M}j%8g^MddJnFta+mzZo
z4v}ixCw9@2gP9#IKYKcV9UC>R)cn~E3vqEQ2*B^}pM7&%RaI3>G{<3(fnkSTiGNjb
z3G)~ow7nd6$2466vmU5-sV7S6VZE3~5M3-JPtM#8CFT2<J;$`)<cR8%y^y=J--6Z#
z&Amm5FFaMVc0eGKS~{TQP6+x>7k6-SGf#^qOfI9+S_oRG?u-XlaL6MdFBS<}QS^i&
zpx1)cQ8;JJqyJxEIC}Bf&QDEy8a%8ML8)KgOM!}@wz)Bu&u4I7R2L_I4>dZ(w3<qO
z;%oH){jXNITu<vK>+iq!hQ(sLasAzhb^Ac1so}Uwk7e}-I3tI)4x+s=ST{@0gof#9
zqJ^ab*>*}p1=n)M#O!@y^N}88>+u;c?REXEXn7@}oOzE&AwW<)N#U6vhzT63tt{|m
z#<O|OE=!|*yF+5c%6<-b06ed-i+!e6{r8w7U-k6%o@#RU!EpPcpDZ>JvJR$7`!^6G
zcNB=_==%|iz{d$NjE*LJ3W%lbe?Hpf7Z`F0i@nlO=YSaHtES6xxpR^MA3fOw=Ie)N
z(h}*QHM%s?Us0_4ZIhrP>cCP0TROJ*J_EJZbP*VzX?I;Fvk<Yx&u<q_Ojc1dIC&lq
zuXRR)6$2IrpjYmD4A7J4Qu|a!%%1+cV&wAAYJWShs7?f7OHBGu!;=LXp&Xou0mu&M
z8W>-D+r}J%$wD}E9{f=};EyK7W32hc7o!w*PCN#`#PhitvbGZ!_Q4qPQ97GeWm4o-
zzrmb}ilw1EjihgroJCPZqACu8{1i(Gw#fQ2F9c_hoGa|4sVwGy4W~0v-o?-1?THJt
z(bWYEKg=zCfH0PI*wNwfskUj(@b$k2g$}6V19ROO1CmhJ!g50$1QvuberV#r!Izfc
za<T|?gzNdo#pP5D&oEn1GRHR36rXsm6FD%-yGNel?s<AxYOq0WIiOl$rZSX`z(^r<
ziukgiR4HYWFnh%K>x(ua>kVHfUjG8vOmeI4DsN9<ERuHsLRiis#x5ZLM76d>HtQFR
z9wk*h3;SaeqN#7jsIxbV(b(rMuDzRW+6>}#v`^9}ok$WIQUxhus{bjSZV9Bj1Ul-}
zgM-LKNp(!(Wk<&pjDP;wwDO!!xw>iX33G^27C5D@I4U?*)fvl!aqf#zNg9q-Oad23
zCuYGMQXyP5d{C|y+*X5q=K_^`u(GrpBn_`kg3jJf%6&T!vQ^s2v5G@pTae|)oCYt0
zdFAP1C~1Lu)ETp`=bx0Zte57p4-nQ>Tg52wbRobhFb!bgsQ%+<iEv7<I-xGIe(*g{
zIk?p&#`nFlG|p<~OS!-rZ8L&ti8nqZ8AtXY-vz31u;G*l8Z8cv8mvPqSMZ>31lmEr
zMSr&=!Y-{i?m|b1G50xKA`K(WV$^upp=dOrE>|!=pD80E;E;FhkF)=dz;gh56Mz>d
zT03NY%Ge=mJ3^>g=u5hDrxP7UQd9(eh~r0#P1VXU;Ilz*GNFmgBXo_~5jCRYXL^4H
z!uOdiV`&1mh9<Lv-B~@=ek(Y1u~>Jd<#TqLWCdNYr@Da}Hx%lS!v>r)FHX=#$7Qvg
zZwTp!^WnzXhX90l{x1V>Y;+LVnI?uBOYvXCJ{IGMUy>v|{qR-Ea6SyBJF8ba*{s!#
zO&*mRIx)}}+~D2N8A5nJmbi@D{Q#r;DZ&Uni0qv5mLC*u;$gIvIN%+V;wnmSfP-nV
z)ANXp_*l#w&6ZqhW9pnuD~`lVUkIb5R(oli?_q9!iY6mCEA^xd9>uiA>c%;vl!Q$K
z%W@bdA<E^<=H97#eU=t=Mx1iF?pUx5X<fokLJ-4PCak;m409zDCeYmw=fK@&h#1t
zUG8RHlPtzoL0IE}>>J4e7UCJxo|eIUuO`7$Ypce;tH3GpVFuQxRrv*M-uZsJ8ct8J
zAV5FO@E%U2X!QD$kt>3~0EA<#cbLy)8qc(KGenc5<{z4}V0+5OJGK^=n#P|tUNyQ6
z6&$|dA>&sPbr^EN$Cq^^r@HaLPRHAW3FDueV%rd~Jb2akx*}D}Y{aOxrVdcXh7vGw
zq=Q-TFge}RP>H3=CiZs>)ev|epVf?K(!@Nxy>`~yaW0NpVs;D*)_WS=sw7z{@|0F$
z>aI_!3gmS`vw!)@f@>`(Re#Z6)E=zazm50ZGmE){DM}hKQIwxnHq<~lt|T|?8$c;g
zA)Fr4`FN%^+^MR*+#B@CpM&{=Dcqd;;3^s;QUC6bHT6x3vNFyPKAzp<1}C>+IQixn
z+qRz7I8RQ=ItT7A?!z)s&Geh=d`-#%FySB38pV<=6jL%}%E-U?{E{?R&b7Pe=yk|#
z>6$r|u+p}kG^Lm0J>$w?2lX*2&T(7Aq}1xCRAWW<hZ@RWH3-LG8R8|)%C+Q6kX$;<
zo3v`(HqELNBL;q9*&;$*qfc-;fjtsq+$qX7z+K(M9|)uG;Kf%`{3GdLXEpt|Te`Q?
z@Zi5WeD3|MQ@{uCuee*<K+%z#QwjMSCM5spENZ|0<85nBr|bt#+?rGV2u+z;6h)J1
zz41B_WAz|_A5AO^Qg<Md+Chc9?m=Q~ku?sYnk?hfBb*3htSb;(4lE2tRgw@TV-h=m
zqs7KzPG#tiL*Zw<ZgL@?JCAHbzAoA9!JSF4B#0+3AIzJ#^N9JR>_c$!(mKgtyHORP
z^FPd&A}tc*#g`1}5L{9#Gg9(Qa)HW~u7|**tttHBxqx74La2YKMs7s@9<s)gVRY^9
zJOgk!HCo3ABNc?G40d+;D`dp1a^ZKvn-!-QUw||F=?$^Obe7z-Ps1#TkLEvh0R5qu
zY}hcQv<#%fUXJTTRf2U>9Xx^JT}f0<DpuK1*H~<hScrzw03dNBUi@AAnK?3cDNc{%
zWm0Z{=F&q;?VLi6ug*5#Pl-Og(C(3{L4dIFgviYAGH%~$88F>N38%uaeor0VPxp1!
zxNbz{>k2YN4~*Ij80?fUIppU><zKtdbNPeRHe=6q3%EP5a7K2z7;>g$jdKDL;-Ld7
zD#oGW`!H5J!k<##a%CDZIN)w*5f+cDdl3XjaYYfk1=(MtK=GQy9C;gq0m$>KT}@zM
zp&6-YJX8iXA#;DiF?sUjBV~i9HX^@M_dg~%t$O69Q>?MUNlMW_Sq+ZS(Js(btl?>E
zn*NFv4M@vnVDf$_J_73#{g!I(i=N9vGvQ<>xTtB4?iv?-dQP%2G*VzEnr&_!%j^4A
z(wch<_>T5*KZiK^=DR~k&zcv+$<3VS%T8jUlD-<nQE|^j8>RCwua?>~o|<aEsO%^}
zh-)-t@?Mg51=No7U_&P=@=+)&Q4>Z-4b8YSi3(cBQy^BudCaSC@$BeL^%IixpQi)^
zWJdl(cI{7|QllBW#B|Y;<<Ki)VJIx}W&uarsgb(LUspJKVsEKg%tmwUN#A67IVitk
zOO0MO!S+^uA(qgcrjGr#?2rGpbZDOXdiH%Grjyk)3M{P?TwEbKzl3ssie!K1&-^H6
zd<&U!GA01)uMw)>U|k_DrVC*@KMZdb4zX&d3>`iWK0R7fL0-CMxf>yHj%sf~AME15
z;RYgZE{F(i52#yl+qAnN#V`biYX()+DP~Ngfz#by-+X|qM;1Q<71B<t2ipLNS^Nb~
zYJiF=W%t7*5pN1=3tp9-_p<`q$D2FO6UdY&IiJg%lcExNKXY+XV;aa~r03D}Gb!B8
z6;CvfG;j~uNRo5D#3}3%5EPP4i;bM)bEgT|8YIM@L&(?Nu&YxLTV<{$U?cmUx4Zdc
zo|u~6tIkR?Mm93hf!!}8Fn8jAT<O!{>aezXUj*Q6wV8T4(1U_8yrH%?b&&0k${{9~
z`qa}@m=6sR3tm~VE30YwDu)@BACo6JDup&9Ux;gjQJyU^ERBc`-q%>hH}pkvy_u@s
zs?)bgpLwpKOHTid+tu;`r@X%G=Pk5;yY<C6^rSz%m9PJcVOr=)BI{F*t^FjXv%GDc
zDYI`OFeY&SyVhtug(MFnK9qcm<MGAZm$>MX{aY{m<7yD^fN|a{`=h6$qql8$<V&eD
z`HPtKyRCkp`B7W^`~M)}^Zr?qR5}d8bAYoi5#h%fdvTe6@;FFWxTWupTllpP@ah2T
z7fSfvdyuJ+Udcl|gh-P9ze%Z|L5}npm58k=7v9mH*Nbc5y>*GlVsjT&rICOFlPCb^
zW5DX^r;LJIuOv%UkZA5uDw-}PVu#6r_y&&G5==*SGMnB&<|nA^`lHfC*gn>#kLW|I
zItYxBrahq*kVTVeY!%&9r(4JQOKvBl&3E*~!NP7cLH(-ck{Iimbmbq*Zey|63GB;<
z5dw3SVRP0;N~p0p=Lj~q{b|!+qPfM}(^<oBG9N=tN3REj`PG)_p&$CI@_QFGa~nlk
z<9EG_%&`rzcVSIcBAS~9wSVGMYMv~;odYGw-BkwomGEK3)@m8eMB|P4`)EXw@#aY;
zbP`R~ejy%Q>Y6Y)O@Be8)VS0`x4^GV8=H=v|2jrAH%envJKi7UF*3Ah&a3D{7?`<(
z-m)dvi*Pw<W|lr{cQ+Qgq^e1_$sdD`tz}krB#)LxPejMVcou}PBA|Irns$JpO-?wq
z&V4IlCL$~DEb#UTOlX?%qVOgQFk?1Y7(z90Te)dR5zDvp9EL@Fs6z{gehvQnvJ1W2
zBnrdMLgZ@tc_NqHQEC~SoTMlMU3Zv*UTu*rEDgC!_)MX0P-CmWYm9xLY~7U>DX$rI
zh1{W~Gk2u1Ib3hN-V6WJDSE)n{>-V~ju8bwk9A700w2h+jLe_l%jBgym+Ch+Qd4Ge
z`{6~}3cr@6u#z$dt>Phd!0vCLJTk>)nY3*qWs^_W8V3|SWZppF7%ED(N+Xb)PHdD-
zoLnr2)f?>T-jG|VI)M6R&NEK3|8prz<|t%!H}$9M=d~)E>Ieg~^2*M4)O#6KX^F>|
zH13%iVT^m#Sz(kuy^k>sV48lZVYr_JWOfsIAq8P^BR$C9CE+9iXYk4_5#h)Ua)cpu
zV=}La)2-k0)^Qi<dm||K)!{mnd0VSeTkCa~LpFXPA^P{FcE#^1F4s{eCykt{RBA+i
zRkM$#yO^*lyH~!)@Q^^*K+8C_^10kX=5Rf@Gfv$1)O#y-#{*v#NrsQhy6&h-3Q=_%
zlbM^CBeHK2OBF`e9it=8#H8l%YW(_?cbb7I9AQ%ZklKQv9zR8+2rk5BAEoiVibUw?
z6hT85yidRWJ{?x-GsdV>kY$jQ{0Q6eb1jXDxZZfbo?73@&%exASw~iSAK)$|T^5R-
z2|*?ou7-x5+`Jxgg=UT|41$!5ZLF@ztXDZRPxXj27S#%e$pqWy*zviwM{Rll2$`->
zWa~Rzb`(QItDNdcPhZn%)15Jqawq^|(aIB(eTi#`G(Hjl*q(2~HErtSzWU>jOD+ee
zf@LPxgd#ef*tpWj7`V`hh;b4I1)+q;m4Pkj5sxHDTOwO@7ubwIK9?8)l7kSd+RgMz
zA)vfYXFa^x?PuxWn)H02m;=L8sYVNTbb<^u!5o~k<#xXtmkdPyF%XhuhgC*HPjowV
zZp+l{naCQf=9I?lR4`euXnuJ{V<+7LBO2ql<n)RS@=nWyvaa`y&h(;IjWr!dd;oOX
zR2<qyp>K3gr#YLh*dvt&KnhNriL$@|GNb)1;el)HSmLW&ONJ5)gl?U1+6hIZ-LW(%
z%VJ>_C`ZRqj5S*rz6_pc$0&ez!z$h{NP0iVQzcEh^wGZfF$p(cp-KgrdV<i!r3L5+
zG$tlip=e^(<`TMPxvpYfa;wE5^R?-nuYYxl-NGW`NEi9>r0wOjT~-E-r^>T+?T`zj
z13utJ%hf4ub3_ozVQ3|R_+_;M<zn&Q(hDNO?5q6lON>h<T4{|!-}fi5KM#L3j8>}t
z=LxU@4>vyp=uBoV@&5u_K%~D5)d4Va22@3|i^562*YxtTU<$^v<*zzC>;WIq8i(a
zk9qGbQYY}!?kbP8y&heJLkw6pK+COYL>3ZBj!WlQ`f_@x7|neFC7Va4wsHwp$cZP|
z?kvSa1+u1XxYHjs9%bj1*DSG<+}ZSdoZ&iT*1()xc1EO|3bRKz^4aG=6q8%;Ao_TG
zDF9bI-0DCK0yomOLN}~Z=J0D{%&VA!Tm+A{AGv5kFi@bKD7K>|XF~qOFyP`0k=D@!
zU@isSH!hTdJZwfp9{xX^(&_l)f*JbQ6<{S@JUKZ%{&~v?)HX0r2y3s4URJr6=fM@4
z+Kc!MCEfgRy4Qox`TAi(jACPwRO#e!@9b>89zyEjDH`Zh>0s~Y^Sz_9gBOQ;nS|UD
zxlLrI0HaBKF^pazA7&{%qqvy^o1~`LGVG~vxSY%`SQ2$)_k1K@39$#H`;6z`S$_XX
zT;}*+GzWiq-8;NP*ch?N03__(B2K`YE9`z8tPp)d7N$v{2gqv6zz0Q~NdeF&r+a66
zN9S9fn3DsSA@T*bEbSWr&90IsJAx`ynjk^3rm{xczAa!Z8V$;>b~58Fz?$!EtjT8K
zqeUDIvglKspf8Vv9*tElc%r1uM~mJJYoCgyRK}&PHl3^(6jM1Hv8*cRlm}1K+N`n^
z+?aFVt@*l@hoox3uCy=*Lw5H1Meg$Q0S-NF74Yp<f|lYx`$!`#Tm}OP8%V>~EJwsU
zAfFfE%FpH^#QK!UOi(~EJZe*8A}ZmJ#U)Ygd=AN3KM%v?wPm6yl{QnC@ia+QoJXRp
z(w1JSIYqj7yxLu2glwTb=#*wl#D8L!J8HU3?hmYTpwOLia>r9{2__iok0*(d<z1^y
zh1VM!?D69JSUiHXZ?SqF`!}2nvQo3IHT*_uq!hp9bTWuS{nD^#asJ7cX-yqbrL8V?
zOT%??k^Ci=s5WqIg|+J1TuxdsZpFK7v|@v3h>5(VJ={PwX{f!~sbxBzr5NU`KORB%
zP=n01QR@@-Qx#DdgCGWrfB*P#5zRxVQSk928dPJGy|bNz196+LYVpP7SjY2GuQIqJ
zFhy;+QUMFMVC6f@*`;{{p-7q$S!^;+v|b4xTokci>wQDU_`9Kt!dl<>Nf}{Rf&^q0
zSZf9TJlBQ73sEX5`Cyb+g?SO1WZWq1d&xc?%5LpjI4!MUXta#;WCwqep}=<3+79BY
zIBk%|vMCrp9{9?ema8@IV`~+vc4bvSF#NRi=)V9|c|pcVJpusZyc?C~TwUwZl_|J6
znR8C0J&6)ReVdN&zoA>ZT!p5<?Bqi@F;N?8mfWq;g&J0)FHqjTl0fyq2Xq4PkT7`t
z`-*T?&Hq!g245on)8^Wy8~<r#ZLRkId{F;Sdx>ig(AopE_5iItKx+@s+5@!q0QGu+
z3ZG7OZX!7zh^WolAd@gB;vYozUXNmIA2tn|q9!77UeX8D;_3%y$IAzMJHeADD{D*L
zr*Q7B0yi2_Wa!{Yd+q)_L(#uvdWoTb%=2Fcf<g<#^YDuOM*&dySs2FW_0hqPd#7i;
zL-GM(?~mL-PbVjb2Rps<gX1GAz1AT?;sOr0&4Ow>$4BRTKc7>TC%Vd$f+{Dc2S4^G
z;8N#fUsXwhO<I{JdZ@;_gjeOG6)s*p)-JWHC!kwUQTYCF#g(E-dT}6ihg^8IsIaMO
z<l<{Z#mPXFimey6da%o-Hns|5TAl5j9-N#Xi&1SB6~_Cc(<6x`PqqpJL&;*_7q)ox
z8kiSYWC3efSoqb8z1`it-M1&ZKXUPlqT+_>I+yMj1AGWL{o~$SHE)BW;xbzh743Fh
zB-$vf%DbQ|!-yfm_9npDi6C>8kl5Dq^0wL}Fw70QS>8hLh{qdtGPPG%%Bu^+3A<b6
zWsi;-&~T+fVY;Z};;R*kt7`?WvR0vrxYXqe>lG^C1ud7}s8E`(rn&r+Dg#!xvs{NZ
z&cdgKJIeo)@nm)RUPR-~&GkI_pTuR9|2NmVE9;veLU-3Xo83PI>$Uv<8|!~){ZEch
zR)IShF$;1B4N$TEXQR8mk%|Akw$`cjKM$w>@hq`d1GHLefNBj;tpTbvK(z*_)&TwY
zYJiX}%EkbDLEcKy;9xYFrARfxo2|Su$FnrQ(BARB5j@O!S=73bjVJRizegO=0sJ|m
zOqadE0Nr}BMNX#Uble{gbGpDH+(P#GYYqVXa%`lOwo4_jfq+Kewgp8x6Ti!fi^Oib
zfC@>PQ|4%VHtS#ahF4?wd3}@9B!zHSx$ivhcE-aY^GAv2?aMH^eua9$%$q~n%ti0L
z-T4a-$5&UVNj!wZd|31zJ~5t~S>hwDMUo8LXJ?0)NE^1}R&D{$T6*5^hZq{|HmBJ6
zbe5#A!cpjfMJ*4iG23_sUcfeM?}#rifwm2?mcLRGA{6kaw7?FqI-|6-m>I246jj@u
zX7)wzY;R+2%l~rc^l;y|F|(z-+t4mXUUomPcfPl$&s!FdD=P0;?2__=G+{X_;3+u`
zZ(i-L7nG>hBOq%$DtKRE&al|YFwAY{d>Zy6v1wF_1`>OMJ|PI6qf-1)G7K`xbTn8I
z@TrzCITPA8kRX_`Wq$p1e<$dEzxq8f<|>SZK61qIHX1L5)2ra{;Ov}^^4ABuhsbE%
zgy>`1z?gz(=e=`m<PwO__yY%vxk0W208<rF1fsg}C`qGm(4^MRk59Po(6o8{E*Q;j
zE`YlO7-W`UU^k+q9UP?0j5wpF;YMu6KgP6l{p)ZPC&Ulz^iFzkw2u1}@?8&pWo_;I
z7S76t2n3ao8#+5bJd?x2gPCIw!kG-u=|o4SlX~+sxc>b*^J8d8_xk*$YDiA^=IYvg
z$IP^)VkbeyIVE05i5E{1yMiI2S8=)n!a!>G?fEI^JV3i7jh&4}<So!n&FxFLMDPS5
zhL<N+*xlRjy*@mDdvbh=I@4}fWY(bI64cK7aX-A64UxyDcu&%~-)c&@?Y=$!Gc>aJ
z`1sF@TV+M}PEjMexCEc&g-`c-yMLvogT<`{D>;L&JZ=drjh~|NqT;kkpg4Y3D1N$k
zPN3swh2ofE0*m8kh2rG@3dQlW3NWnZU&PM}#sBj9fQsX1h2rN7%XuYGh`vB+{H#*^
zjCA6Q_*tR&NspA`i}+cgc<+Qk$IlAIkxF54{H#!%=oBH?VI^RY8X++7S%|-Mq4sBK
zLR(OXcTcyZl-xF+q$xw<{`f)~g-WIwf`4afm1M|=pio>@w6HCHqmMm*Yb;t)kdIO&
z#2Iq$BtGBV%tkQ<dC?)AqUn3n#95D%`AX49ZsRdB7HFgmEP<~Thc61^QEpRX;w}o>
za@jVl;hFQ$$YQV6SSmx2Ib!Y}7Ic=d0eAzbDhk&~Fq<fRqoQFlG3(<Y?VT^iLY+R|
z1&lUJ8sC6`6y8i)s1rk1GR~qS;X;Al;(#cbj?Cdh;Uj#V%NVh;btkGb`9`8{0S`Id
zyi~+lQzBwyT!V}_3U4Bz^Ata%Q9i@UfwQqO`0ApueQ3+58L}nI9^NOGw!DdQDNwX*
zqZKT5$ub!Jy9AC29#BXxPxR;qn*dpxSYIs3U(9k1Som*;>DD%YjjUycp=i!Gl`Y{E
zAS!<Lq+(0|xznlx(Mf2}Vbh>mp4Fv#4Ez{pH@Jz1L)d<(;<ICjr$*g_<>TT?cUaN<
zNC~keDxK?oQwhX<VuPnAHPjkM?h!|1B~px#n^!uW$=rSs%p!t%X24)~bps_*)K&yH
ztgg_f|8>-V7o44eU>i+SmYB)yW6Cj!<Rs#>q*~1REf6=qu8fe)v(pMp#S9Gop)g){
zpWd`+c~me>JDg^X`oDkFH<Lxk3=gp==Rop%*(EJi0LOF3J;SbFfMn0SL2>BSwx#&6
z-Lw`&paRT~IGYF&AUDvMkPui&!-7e}@CD0UngpTv<Qkdx&TcoFVpnVz_n+IrsGRVV
z)N)(YYKX2q6r_2FNEQCZ^V}x^_;e1C6Py+SoUB;TG~-i#tL|k4a0dLjjhP_bw+*1z
zTmJe1uAGD34fVDu+if#t5ct4jY6UJKR|MMVi1s7MOHvd^MENyXJU={p-(744i_q8m
z)y3eGT`fs)#`Z>dzdVq)a7jZo()1O7okV=|@d{mqJ-}cw^ma!ze{vnZiyCeLiTp{D
z@3}34>*d|r`2y=1+d7C=<W|uSO*o>JHWG`S5NKp0%7n0rRy@1g68mw61=ayB7_Ve6
zvD~bl+uKH*xTTzn9;3ualyEc;x`eGlumLmQNyx@7WL6>Nl!}{9!<H?h$fl;c0a4(F
zhig7iCr#oUHG*AuJtBJwnYSpQQz}A>rVnUEC9NBjo`?OfnN=4lh52?KFZ@In-zdEv
z588&df!;ym;}M$4V8E~Nl059&(jgz<x{R-8biBzcPj3WG9;kWN<ee(83a3Jc(rBV<
z@$sLFVr^t@!yAj@<b~1VJrIWzm1-F$5(e2wn`#+`Q2v4ionSD@bNQfQwd2Gjox#M7
zQsypPc1b4|&@mFudccyXYn+y1LZ(c<@KR+GCdr5KbYL@yst&L`)JUkiwyuU+v{b?2
z@ec<_t>EOa2OokrJ?*_(dbPXW3Vt{{>zy3fIkf}~oG{sL8zbEH<4HKQ8BYPHuLe+Q
zv~dA+mVBP@(MDoO3BK*}h``CLkIBl>rjC(T=dsSDM-t2owjwz#V*<nzJ+0z_gt!zZ
zef)?zKxDWy_25@yag5``xyuEXR94Jh^folC3P>x&SC4iSd)waz^2Q!FeLor?>x`;q
zv?|+~-cC{-H(QuK1kwv%{5I+QqzzPyRm**<%op<#qQ#b)$nr`O_(ktHv`IjKrLFTG
z-^s&W6>HqEjscEmQh1jKku=X6&H6R*0dlxUFp7e3N9x}MSh;J5MP!ie6ara>2-a#J
zNhmE32qM?xn`r53iYd5h+wzTfk_k<=NE>#sO%d0U1IpyS69PR{xYaC=jmjV@B%{0i
zV${kDIL0s$^|)V{$WY*OV3Jhqe!&vv6n6uxYRp=qYSp4&Ib?^~kf47vu!s*oZs6^g
z8MEYOAOl1_3Lg1i=&9N=QtH7AER&A8`Gy&UOvLsiZZ==Y5Wym?1HEi9C>Y}LpPy!?
zO_1$>j*)r^I^U#~!~J{|U5&|x0r~O?Ncp!7A|ih|KJi2*BizCBVhQAF5AHxGO)pva
z_rC+KhkQ>?#p;ph=3|gEgQ&zX=hkwpf{3EEg7SK~jCWRpQO1b7|K?@PGXdwSG3vS1
zToWfTC;1giIQ}ZJ0}WPa%eNKiayh#j?5r`xXeRCk`yI+b!5dM>YF~VPwnPE37IQ`^
zt9LP+CE4D!$&Wf}^gGQhebNC986r;-6}2)&AZXJ@D3QR1-I3KA{cbS?Qv(7Kd24Dx
zAP1MXSZ(q?XiU5L>nVz$!)QUF%i%1!&ci~!u}1^YhPE#PrsX-xA!W>lCg%WVD=-$P
zB0dQ{fMrQ(XZtBe7MRAp=@j_KSn?=xeFCv!*x(X60reyDHh`n!Dk`1NmGC#3^zW)g
z@$`lbzsqrLsbg}M!N`{Ahnx1NFc!tAj3&2j0pzaT(OAm<oY>be63jw(Ivb&+`_KS-
zEPrKe11sN$lbg(tt{piV`7}eoCV?LKw<Y$F5NZu0fS}46kxs)=aubUy6Dh(9Kd(MW
zl~eY3zzU;BshnDF3Vq0^0aP<Ck<B5S*Y$6MOAuW^5KDxET0395CfG6TpfS9B$r?*P
zO4PGlQ5XcI#%Bw@$R#OQjnpLyBIB8%G0DrT=G#cN66Trg?RoHQK#l~@3sa9!0`xwo
zY*LDn;)4y(sqxJOJ%_jlx4|@=UfDUI)X-#v=*Oa0@w+I7onh_MX^FP^tJ7$+pb_rP
zYQe4EvOzLhv-~oGo54B^&3eY%CPp0TM~y|4rLtME1{Y8Z>b7~#8VrUrt$a&&9eoUe
zJ&13@A*{HFmL1AFvaA%tE&2o*qj_Im@WF*I+^Q$kw+Q)&^_x)Zm-tOK=SUyZFqO-R
z6aaE%eWO?k<RMAn>VR_xgB<lT`Y4We=#O$HaYorS_iQh3fa`ZIn>n{N7y&#YpER<~
zcC!@biH@)d%}Fo?N6%xW394R2jLeWJ%|iNdRu0XcFSed|8}-EAd_#F&M{}HZxGjQE
zwm?V6CAz9E=AC-;oMX=E1&0<ax&XR1uz-pUdyW?agZFOD347eTwYFH3nvKPUWe@4
zD{jy;T46j0QMGy<ghCaIJw8KL`Z*SUwoG5~Vq83ggG4?Q)nn5XL8z?J)e<SlfYjbM
z4}IJXNwxK0=iQt~8!e?i+7>eVkP18wV9UT?Sx)5Ju(7}L^++K=gi_Dss2CuIgx*L<
zWC1TE#e2{nIXYCgMR@n&-W3ZBjz@fw;PeoIIy>Z6SQX&pSw^Fek?`urfKitD2e;_6
z)OGvz-A)&o7UkWtTBroD<swWf?+v^zW)0_(N_z>ir_e_{FZDzF1V>wWQQvG6SsR)i
z>F8p;`V}D(*A=1*y6<hU!kuaw7Jh2|`x*j!_WbFEQ($Z+DS3<2l{Y#Zu?Mrs&S&mA
zF|vX3JwjeU#4hmPCf(ucC`pG&`Q@bOhYA<0-9rvt?E(^501C8G&)Wp|Hd94gqw)>f
zXV{9^%mI3bIJYweG%`0Ven!C0)xT@AyzOdNOm_TnVH%}tp1=dMP_7xweVM@A!Te<o
z)0YZnFNG#AHWSO%VWd<|7V;J6NrBS5x}v<AlDnE>+oQ}*iWJ#%D6gyPofis}%y+gL
zs><h@`69HZ>kW&ZiBz(nL+D^So@9(hD5(?(j^$)o5P{zLyi8J|4GB3nRxlv((Keo<
z1@oeZML4o0fgj3>q^)xBG$@6%cq(K4qZHNjXHh+(Dcv7WZ^E=<`c%&-k3f<M#qtym
zQ}jw<84d`#S$cf=0)?RDbGxUhmi-n$QG{WX+&hzmL|~g^Jd<3(fEoMvy_f(W3Sy=b
z(mZgjQe?Ckd-8ad>@+v<d2D^*WKwSriLqXErJPA$0m|kf3t2sDH$&oJLtu4<^erL_
z*oea+2+UeH$YNcfcVS5SVhm)<RnuE|eu+Z(<!nkpq+}ccVK4^(TwtJ|WM)L~$?$#i
z;;MNf0ua+X<1D#dKN!wZZxwJ>96H!aHiQO@)ys3g3a9T9KSkg;5g3PPaK{NJ6D(}v
z&NEFO>kl^5)LZFHyHmI;Zc4756ifn(rGv%0D4MtikKr(WANg%TN8|CN;<=9g`S&^x
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�CT_9_A^Q
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

diff --git a/alien/origins/jlhttp-1_1.zip b/alien/origins/jlhttp-1_1.zip
new file mode 100644
index 0000000000000000000000000000000000000000..d420ccbfbbbd1d956f02e2af28eb25f53075cf10
GIT binary patch
literal 70190
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

diff --git a/alien/src/fmsutil/FMSConnection.java b/alien/src/fmsutil/FMSConnection.java
new file mode 100644
--- /dev/null
+++ b/alien/src/fmsutil/FMSConnection.java
@@ -0,0 +1,76 @@
+/* Subclass the GNU inetlib NNTPClass to support FMS XGETTRUST.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fmsutil;
+
+import java.io.IOException;
+
+import gnu.inet.nntp.NNTPConnection;
+import gnu.inet.nntp.StatusResponse;
+
+class FMSConnection extends NNTPConnection {
+    public final static int MESSAGE = 1;
+    public final static int TRUSTLIST = 2;
+    public final static int PEERMESSAGE = 3;
+    public final static int PEERTRUSTLIST = 4;
+
+    public static String trustKindToString(int constant) {
+        switch (constant) {
+        case MESSAGE: return "MESSAGE";
+        case TRUSTLIST: return "TRUSTLIST";
+        case PEERMESSAGE: return "PEERMESSAGE";
+        case PEERTRUSTLIST: return "PEERTRUSTLIST";
+        default:
+            throw new IllegalArgumentException("Invalid trust constant: " + constant);
+        }
+    }
+
+    public FMSConnection(String host, int port) throws IOException {
+        super(host, port);
+    }
+
+    // Hmmmm... would be better to raise NNTPExceptions here.
+    // Returns -1 for 'null' trust.
+    public int xgettrust(int kind, String fmsId) throws IOException {
+        send(String.format("XGETTRUST %s %s", trustKindToString(kind), fmsId));
+        String reply = read();
+        StatusResponse response = parseResponse(reply, false);
+        if (response.getStatus() < 200 || response.getStatus() > 299) {
+            throw new IOException("XGETTRUST NNTP request failed: " + reply);
+        }
+        String fields[] = reply.split(" ");
+        if (fields.length != 2) {
+            throw new IOException("Couldn't parse reply: " + reply);
+        }
+        if (fields[1].equals("null")) {
+            return -1;
+        } else {
+            try {
+                return Integer.parseInt(fields[1]);
+            } catch (NumberFormatException nfe) {
+                throw new IOException("Couldn't parse reply: " + reply);
+            }
+        }
+    }
+}
diff --git a/alien/src/fmsutil/FMSUtil.java b/alien/src/fmsutil/FMSUtil.java
new file mode 100644
--- /dev/null
+++ b/alien/src/fmsutil/FMSUtil.java
@@ -0,0 +1,302 @@
+/* A helper class to read and write archive change notifications to FMS.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fmsutil;
+
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.logging.ConsoleHandler;
+
+import java.util.Map;
+
+import gnu.inet.nntp.ArticleResponse;
+import gnu.inet.nntp.GroupResponse;
+import gnu.inet.nntp.Overview;
+import gnu.inet.nntp.OverviewIterator;
+import gnu.inet.nntp.Range;
+
+public class FMSUtil {
+    private final static String UTF8 = "utf8";
+
+    // Because I Say So name resolution record.
+    public final static class BISSRecord {
+        public final String mFmsId;
+        public final String mDate;
+        public final String mKey;
+        final int[] mTrust;
+        public BISSRecord(String fmsId, String date,
+                          String key,
+                          int[] trust) {
+            mFmsId = fmsId;
+            mDate = date;
+            mKey = key;
+            if (trust.length != 4) {
+                throw new IllegalArgumentException("trust.length != 4");
+            }
+            mTrust = trust;
+        }
+        public int msgTrust() { return mTrust[0]; }
+        public int trustListTrust() { return mTrust[1]; }
+        public int peerMsgTrust() { return mTrust[2]; }
+        public int peerTrustListTrust() { return mTrust[3]; }
+
+        public String toString() {
+            return String.format("{mFmsId=%s, mDate=%s, mTrust=%s, mKey=%s}",
+                                 mFmsId,
+                                 mDate,
+                                 String.format("{%d, %d, %d, %d}",
+                                               mTrust[0], mTrust[1],
+                                               mTrust[2], mTrust[3]),
+                                 mKey);
+        }
+    }
+
+    // Set true to data written to / read from the nntp server to stdout.
+    public static boolean sNNTPDebugging = true;
+
+    public final static String SUBJECT_PREFIX = "BISS|";
+
+    private static boolean isValidKey(String key) {
+        return key.startsWith("SSK@"); // DCI: do much better.
+    }
+
+    // Read the first non-header line of an NNTP article.
+    private static String getFirstLine(FMSConnection connection, int articleId) throws IOException {
+        System.err.println("Trying to read article: " + articleId);
+        ArticleResponse response = connection.article(articleId);
+
+        // MUST be on the first line of the message body.
+        LineNumberReader reader =
+            new LineNumberReader(new InputStreamReader(response.in, UTF8));
+        String line = null;
+        String firstLine = null;
+        boolean sawHeaderSeparator = false;
+        for (;;) {
+            line = reader.readLine();
+            if (line == null) {
+                return null;
+            }
+            if (line.trim().equals("") && firstLine == null) {
+                sawHeaderSeparator = true;
+                continue;
+            }
+            if (!sawHeaderSeparator) {
+                continue;
+            }
+            firstLine = line;
+            break;
+        }
+        System.err.println("GOT LINE: " + firstLine);
+        while (reader.readLine() != null) { /* Must consume all data */ }
+        return firstLine;
+    }
+
+    private static int[] readTrusts(FMSConnection connection, String fmsId)
+        throws IOException {
+
+        int[] values = new int[4];
+        final int kinds[] = new int[] {FMSConnection.MESSAGE,
+                                       FMSConnection.TRUSTLIST,
+                                       FMSConnection.PEERMESSAGE,
+                                       FMSConnection.PEERTRUSTLIST};
+        int index = 0;
+        for (int kind : kinds) {
+            values[index] = connection.xgettrust(kinds[index], fmsId);
+            index++;
+        }
+        return values;
+    }
+
+    private static int[] getTrusts(FMSConnection connection, Map<String, int[]> cache, String fmsId)
+        throws IOException {
+        int[] values = cache.get(fmsId);
+        if (values == null) {
+            values = readTrusts(connection, fmsId);
+            cache.put(fmsId, values);
+        }
+        return values;
+    }
+
+    final static class XOverInfo {
+        final int mNumber;
+        final String mFmsId;
+        final String mDate;
+        public XOverInfo(int number, String fmsId, String date) {
+            mNumber = number;
+            mFmsId = fmsId;
+            mDate = date;
+        }
+    }
+
+    // LATER: Cleanup. Seems like there's too much code here, give how little this does.
+
+    // LATER: other kinds of records. STAKE. SANCTION
+    // Requires user because it looks up trust.
+    public static List<BISSRecord> getBISSRecords(String host, int port, String user,
+                                                  String group, String nameToResolve,
+                                                  int maxArticles)
+        throws IOException {
+
+        FMSConnection connection = makeConnection(host, port, user);
+
+        Map<String, int[]> trustCache = new HashMap<String, int[]> ();
+        try {
+            GroupResponse groupInfo = connection.group(group);
+
+            long  highArticleNumber = groupInfo.last;
+            long lowArticleNumber = groupInfo.first - maxArticles;
+
+            if (lowArticleNumber < groupInfo.first) {
+                lowArticleNumber = groupInfo.first;
+            }
+
+            if (lowArticleNumber < 0) {
+                lowArticleNumber = 0;
+            }
+
+            final long first = lowArticleNumber;
+            final long last = highArticleNumber;
+
+            OverviewIterator overviews = connection.xover(new Range() {
+                    // Am I missing something? This seems wacky.
+                    public  boolean contains(int num) {
+                        return num >= first && num <= last;
+                    }
+                    public String toString() {
+                        return String.format("%d-%d", first, last);
+                    }
+                });
+
+            // MUST fully read xover returned data before new commands?
+            List<XOverInfo> xoverInfos = new ArrayList<XOverInfo>();
+            while(overviews.hasNext()) {
+                Overview overview = (Overview)(overviews.next());
+                for (int index = 0; index < 6; index++) {
+                    System.err.println(String.format("%d: [%s]", index, overview.getHeader(index).toString()));
+                }
+                if (((String)overview.getHeader(4)).length() > 0) {
+                    continue; // Skip replies.
+                }
+                if (!((String)(overview.getHeader(0))).equals(SUBJECT_PREFIX + nameToResolve)) {
+                    // Skip articles that don't match BISS|<name>.
+                    continue;
+                }
+                xoverInfos.add(new XOverInfo(overview.getArticleNumber(),
+                                             (String)overview.getHeader(1),
+                                             (String)overview.getHeader(2)));
+            }
+
+            List<BISSRecord> records = new ArrayList<BISSRecord>();
+
+            // Iterate over list in reverse so most recent records are at start of list.
+            ListIterator<XOverInfo> iter = xoverInfos.listIterator(xoverInfos.size());
+            while (iter.hasPrevious()) {
+                XOverInfo info = iter.previous();
+                String bissLine = getFirstLine(connection, info.mNumber);
+                if (bissLine == null) {
+                    continue;
+                }
+
+                if (!bissLine.startsWith(nameToResolve + "|") ||
+                    bissLine.length() <= nameToResolve.length() + 1) {
+                    continue; // Expected a BISS name -> key mapping entry. Give up.
+                }
+
+                String key = bissLine.substring(nameToResolve.length() + 1);
+
+                if (!isValidKey(key)) {
+                    continue;
+                }
+
+                int trusts[] = getTrusts(connection, trustCache, info.mFmsId);
+
+                records.add(new BISSRecord(info.mFmsId, info.mDate, key, trusts));
+            }
+
+            return records;
+        } finally {
+            try {
+                connection.quit();
+            } catch (IOException ioe) {
+                // Shouldn't happen.
+                System.err.println("connection.quit() raised: " + ioe.getMessage());
+            }
+        }
+    }
+
+    private static FMSConnection makeConnection(String host, int port, String user)
+        throws IOException {
+
+        if (sNNTPDebugging) {
+            Logger.getLogger("gnu.inet.nntp").setLevel(FMSConnection.NNTP_TRACE);
+            ConsoleHandler handler = new ConsoleHandler();
+            handler.setLevel(Level.FINEST);
+            Logger.getLogger("gnu.inet.nntp").addHandler(handler);
+        }
+
+        FMSConnection connection = new FMSConnection(host, port);
+
+        if (!connection.authinfo(user, "")) {
+            throw new IOException("Couldn't authenticate as: " + user);
+        }
+        return connection;
+    }
+
+    private final static String MSG_TEMPLATE =
+        "From: %s\n" +
+        "Newsgroups: %s\n" +
+        "Subject: %s\n" +
+        "\n" +
+        "%s";
+
+    public static void sendBISSMsg(String host, int port, String user, String group,
+                                   String name, String value) throws IOException {
+        FMSConnection connection = makeConnection(host, port, user);
+        try {
+            OutputStream out = connection.post();
+            try {
+                String msg = String.format(MSG_TEMPLATE, user, group, SUBJECT_PREFIX + name,
+                                           String.format("%s|%s", name, value));
+                byte[] rawBytes = msg.getBytes(UTF8);
+                System.err.println("Writing post bytes: " + rawBytes.length);
+                out.write(rawBytes);
+                out.flush();
+            } finally {
+                out.close();
+            }
+        } finally {
+            connection.quit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/alien/src/gnu/inet/nntp/ActiveTime.java b/alien/src/gnu/inet/nntp/ActiveTime.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/ActiveTime.java
@@ -0,0 +1,87 @@
+/*
+ * ActiveTime.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.util.Date;
+
+/**
+ * An item in an NNTP newsgroup active time listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class ActiveTime
+{
+
+  String group;
+  Date time;
+  String email;
+
+  ActiveTime(String group, Date time, String email)
+  {
+    this.group = group;
+    this.time = time;
+    this.email = email;
+  }
+  
+  /**
+   * The name of the newsgroup.
+   */
+  public String getGroup()
+  {
+    return group;
+  }
+
+  /**
+   * The date the newsgroup was added to the hierarchy.
+   */
+  public Date getTime()
+  {
+    return time;
+  }
+
+  /**
+   * The email address of the creator of the newsgroup.
+   */
+  public String getEmail()
+  {
+    return email;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/ActiveTimesIterator.java b/alien/src/gnu/inet/nntp/ActiveTimesIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/ActiveTimesIterator.java
@@ -0,0 +1,115 @@
+/*
+ * ActiveTimesIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over an NNTP LIST ACTIVE.TIMES listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class ActiveTimesIterator
+  extends LineIterator
+{
+
+  ActiveTimesIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+  
+  /**
+   * Returns the next group active time.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextGroup();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+
+  /**
+   * Returns the next group active time.
+   */
+  public ActiveTime nextGroup()
+    throws IOException
+  {
+    String line = nextLine();
+
+    // Parse line
+    try
+      {
+        int start = 0, end;
+        end = line.indexOf(' ', start);
+        String group = line.substring(start, end);
+        start = end + 1;
+        end = line.indexOf(' ', start);
+        Date time = connection.parseDate(line.substring(start, end));
+        start = end + 1;
+        String email = line.substring(start);
+
+        return new ActiveTime(group, time, email);
+      }
+    catch (ParseException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid active time line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+    catch (StringIndexOutOfBoundsException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid active time line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/ArticleNumberIterator.java b/alien/src/gnu/inet/nntp/ArticleNumberIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/ArticleNumberIterator.java
@@ -0,0 +1,96 @@
+/*
+ * ArticleNumberIterator.java
+ * Copyright (C) 2003 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over a listing of article numbers.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class ArticleNumberIterator
+  extends LineIterator
+{
+
+  ArticleNumberIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+
+  /**
+   * Returns the next article number.
+   */
+  public Object next()
+  {
+    try
+      {
+        return new Integer(nextArticleNumber());
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+
+  /**
+   * Returns the next article number.
+   */
+  public int nextArticleNumber()
+    throws IOException
+  {
+    String line = nextLine();
+
+    try
+      {
+        return Integer.parseInt(line.trim());
+      }
+    catch (NumberFormatException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid article number: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/ArticleResponse.java b/alien/src/gnu/inet/nntp/ArticleResponse.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/ArticleResponse.java
@@ -0,0 +1,84 @@
+/*
+ * ArticleResponse.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.InputStream;
+
+/**
+ * An NNTP article status response.
+ * This represents the status response associated with NNTP status codes
+ * 220-223, including an article number and a message-id.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class ArticleResponse
+  extends StatusResponse
+{
+
+  /*
+   * The article number.
+   */
+  public int articleNumber;
+
+  /*
+   * The message-id.
+   */
+  public String messageId;
+
+  /**
+   * If the status code for this response is one of:
+   * <ul>
+   * <li>ARTICLE_FOLLOWS
+   * <li>HEAD_FOLLOWS
+   * <li>BODY_FOLLOWS
+   * </ul>
+   * then this stream can be used to retrieve the byte content of the article
+   * retrieved. Otherwise, it will be null. If it is non-null, the stream
+   * must be read in its entirety before further methods can be invoked on
+   * the NNTPConnection.
+   */
+  public InputStream in;
+
+  protected ArticleResponse(short status, String message)
+  {
+    super(status, message);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/ArticleStream.java b/alien/src/gnu/inet/nntp/ArticleStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/ArticleStream.java
@@ -0,0 +1,166 @@
+/*
+ * ArticleStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.BufferedInputStream;
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * A stream that can be told to read to the end of its data.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class ArticleStream
+  extends FilterInputStream
+  implements PendingData
+{
+
+  private static final int LF = 0x0a;
+  private static final int DOT = 0x2e;
+
+  boolean eol;
+  boolean eof;
+
+  ArticleStream(InputStream in)
+  {
+    super(in.markSupported() ? in : new BufferedInputStream(in));
+    eol = true;
+    eof = false;
+  }
+
+  public int read()
+    throws IOException
+  {
+    if (eof)
+      {
+        return -1;
+      }
+    int c = in.read();
+    // Check for LF
+    if (c == LF)
+      {
+        eol = true;
+      }
+    else if (eol)
+      {
+        if (c == DOT)
+          {
+            in.mark(1);
+            int d = in.read();
+            if (d == DOT)
+              {
+                // Not resetting here means that 2 dots are collapsed into 1
+              }
+            else if (d == LF)
+              {
+                // Check for LF
+                eof = true;
+                return -1;
+              }
+            else
+              {
+                in.reset();
+              }
+          }
+        eol = false;
+      }
+    return c;
+  }
+
+  public int read(byte[] b)
+    throws IOException
+  {
+    return read(b, 0, b.length);
+  }
+
+  public int read(byte[] b, int off, int len)
+    throws IOException
+  {
+    if (eof)
+      {
+        return -1;
+      }
+    int l = in.read(b, off, len);
+    if (l > 0)
+      {
+        if (eol)
+          {
+            if (b[off] == DOT && l > 1)
+              {
+                if (b[off + 1] == DOT)
+                  {
+                    // Truncate b
+                    System.arraycopy(b, off + 1, b, off, l - off);
+                    l--;
+                  }
+                else if (b[off + 1] == LF)
+                  {
+                    // EOF
+                    eof = true;
+                    return -1;
+                  }
+              }
+          }
+        eol = (b[(off + l) - 1] == LF);
+      }
+    return l;
+  }
+
+  /**
+   * Read to the end of the article data.
+   */
+  public void readToEOF()
+    throws IOException
+  {
+    if (available() == 0)
+      {
+        return;
+      }
+    byte[] buf = new byte[4096];
+    int ret = 0;
+    while (ret != -1)
+      {
+        ret = read(buf);
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/FileNewsrc.java b/alien/src/gnu/inet/nntp/FileNewsrc.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/FileNewsrc.java
@@ -0,0 +1,610 @@
+/*
+ * FileNewsrc.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.BufferedReader;
+import java.io.BufferedOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A .newsrc configuration on a filesystem.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class FileNewsrc
+  implements Newsrc
+{
+
+  private static final String NEWSRC_ENCODING = "US-ASCII";
+
+  protected File file;
+
+  protected List subs = null;
+  protected List groups = null;
+  protected Map lines = null;
+  protected boolean dirty;
+  protected boolean debug;
+
+  /**
+   * Constructor.
+   * @param file the disk file
+   * @param debug for debugging information on stderr
+   */
+  public FileNewsrc(File file, boolean debug)
+  {
+    this.file = file;
+    this.debug = debug;
+  }
+
+  public void close()
+  {
+    if (!dirty)
+      {
+        return;
+      }
+    save();
+  }
+  
+  /**
+   * Load the file.
+   */
+  void load()
+  {
+    long fs = file.length();
+    long max = (long) Integer.MAX_VALUE;
+    int bs = (int) (fs > max ? max : fs);
+    
+    groups = new LinkedList();
+    lines = new HashMap(bs / 20);
+    subs = new LinkedList();
+    
+    // Load
+    try
+      {
+        long t1 = System.currentTimeMillis();
+        if (debug)
+          {
+            System.err.println("DEBUG: nntp: newsrc loading " +
+                               file.getPath());
+          }
+        
+        FileInputStream fr = new FileInputStream(file);
+        InputStreamReader ir = new InputStreamReader(fr, NEWSRC_ENCODING);
+        BufferedReader reader = new BufferedReader(ir, bs);
+        String line = reader.readLine();
+        while (line != null)
+          {
+            int cp = line.indexOf(':');
+            if (cp > -1)
+              {
+                // Subscribed newsgroup
+                String name = line.substring(0, cp);
+                groups.add(name);
+                subs.add(name);
+                cp++;
+                if (cp < line.length())
+                  {
+                    String tail = line.substring(cp).trim();
+                    if (tail.length() > 0)
+                      {
+                        lines.put(name, tail);
+                      }
+                  }
+              }
+            else
+              {
+                int pp = line.indexOf('!');
+                if (pp > -1)
+                  {
+                    // Unsubscribed newsgroup
+                    String name = line.substring(0, pp);
+                    groups.add(name);
+                    pp++;
+                    if (pp < line.length())
+                      {
+                        String tail = line.substring(pp).trim();
+                        if (tail.length() > 0)
+                          {
+                            lines.put(name, tail);
+                          }
+                      }
+                  }
+                // else ignore - comments etc will not be saved!
+              }
+            line = reader.readLine();
+          }
+        reader.close();
+        long t2 = System.currentTimeMillis();
+        if (debug)
+          {
+            System.err.println("DEBUG: nntp: newsrc load: " +
+                               groups.size() + " groups in " +
+                               (t2 - t1) + "ms");
+          }
+      }
+    catch (FileNotFoundException e)
+      {
+      }
+    catch (IOException e)
+      {
+        System.err.println("WARNING: nntp: unable to read newsrc file");
+        if (debug)
+          {
+            e.printStackTrace(System.err);
+          }
+      }
+    catch (SecurityException e)
+      {
+        System.err.println("WARNING: nntp: " +
+                           "no read permission on newsrc file");
+      }
+    dirty = false;
+  }
+  
+  /**
+   * Save the file.
+   */
+  void save()
+  {
+    try
+      {
+        long t1 = System.currentTimeMillis();
+        if (debug)
+          {
+            System.err.println("DEBUG: nntp: newsrc saving " +
+                               file.getPath());
+          }
+
+        int bs = (groups.size() * 20);    // guess an average line length
+        FileOutputStream fw = new FileOutputStream(file);
+        BufferedOutputStream writer = new BufferedOutputStream(fw, bs);
+        for (Iterator i = groups.iterator(); i.hasNext();)
+          {
+            String group = (String) i.next();
+            StringBuffer buffer = new StringBuffer(group);
+            if (subs.contains(group))
+              {
+                buffer.append(':');
+              }
+            else
+              {
+                buffer.append('!');
+              }
+            Object r = lines.get(group);
+            if (r instanceof String)
+              {
+                buffer.append((String) r);
+              }
+            else
+              {
+                RangeList ranges = (RangeList) r;
+                if (ranges != null)
+                  {
+                    buffer.append(ranges.toString());
+                  }
+              }
+            buffer.append('\n');
+
+            byte[] bytes = buffer.toString().getBytes(NEWSRC_ENCODING);
+            writer.write(bytes);
+          }
+        writer.flush();
+        writer.close();
+
+        long t2 = System.currentTimeMillis();
+        if (debug)
+          {
+            System.err.println("DEBUG: nntp: newsrc save: " +
+                               groups.size() + " groups in " +
+                               (t2 - t1) + "ms");
+          }
+      }
+    catch (IOException e)
+      {
+        System.err.println("WARNING: nntp: unable to save newsrc file");
+        if (debug)
+          {
+            e.printStackTrace(System.err);
+          }
+      }
+    dirty = false;
+  }
+
+  /**
+   * Returns an iterator over the names of the currently subscribed
+   * newsgroups.
+   */
+  public Iterator list()
+  {
+    if (subs == null)
+      {
+        load();
+      }
+    return subs.iterator();
+  }
+
+  public boolean isSubscribed(String newsgroup)
+  {
+    if (subs == null)
+      {
+        load();
+      }
+    return (subs.contains(newsgroup));
+  }
+
+  public void setSubscribed(String newsgroup, boolean flag)
+  {
+    if (subs == null)
+      {
+        load();
+      }
+    if (flag && !groups.contains(newsgroup))
+      {
+        groups.add(newsgroup);
+      }
+    boolean subscribed = subs.contains(newsgroup);
+    if (flag && !subscribed)
+      {
+        subs.add(newsgroup);
+        dirty = true;
+      }
+    else if (!flag && subscribed)
+      {
+        subs.remove(newsgroup);
+        dirty = true;
+      }
+  }
+
+  public boolean isSeen(String newsgroup, int article)
+  {
+    if (subs == null)
+      {
+        load();
+      }
+    Object value = lines.get(newsgroup);
+    if (value instanceof String)
+      {
+        value = new RangeList((String) value);
+      }
+    RangeList ranges = (RangeList) value;
+    if (ranges != null)
+      {
+        return ranges.isSeen(article);
+      }
+    return false;
+  }
+
+  public void setSeen(String newsgroup, int article, boolean flag)
+  {
+    if (subs == null)
+      {
+        load();
+      }
+    Object value = lines.get(newsgroup);
+    if (value instanceof String)
+      {
+        value = new RangeList((String) value);
+      }
+    RangeList ranges = (RangeList) value;
+    if (ranges == null)
+      {
+        ranges = new RangeList();
+        lines.put(newsgroup, ranges);
+        dirty = true;
+      }
+    if (ranges.isSeen(article) != flag)
+      {
+        ranges.setSeen(article, flag);
+        dirty = true;
+      }
+  }
+
+  /**
+   * A RangeList holds a series of ranges that are ordered and
+   * non-overlapping.
+   */
+  static class RangeList
+  {
+
+    List seen;
+
+    RangeList()
+    {
+      seen = new ArrayList();
+    }
+
+    RangeList(String line)
+    {
+      this();
+      try
+        {
+          // Parse the line at comma delimiters.
+          int start = 0;
+          int end = line.indexOf(',');
+          while (end > start)
+            {
+              String token = line.substring(start, end);
+              addToken(token);
+              start = end + 1;
+              end = line.indexOf(',', start);
+            }
+          addToken(line.substring(start));
+        }
+      catch (NumberFormatException e)
+        {
+          System.err.println("ERROR: nntp: bad newsrc format: " + line);
+        }
+    }
+
+    /*
+     * Used during initial parse.
+     */
+    private void addToken(String token) throws NumberFormatException
+    {
+      int hp = token.indexOf('-');
+      if (hp > -1)
+        {
+          // Range
+          String fs = token.substring(0, hp);
+          String ts = token.substring(hp + 1);
+          int from = Integer.parseInt(fs);
+          int to = Integer.parseInt(ts);
+          if (from > -1 && to > -1)
+            {
+              insert(from, to);
+            }
+        }
+      else
+        {
+          // Single number
+          int number = Integer.parseInt(token);
+          if (number > -1)
+            {
+              insert(number);
+            }
+        }
+    }
+
+    /**
+     * Indicates whether the specified article is seen.
+     */
+    public boolean isSeen(int num)
+    {
+      int len = seen.size();
+      Range[] r = new Range[len];
+      seen.toArray(r);
+      for (int i = 0; i < len; i++)
+        {
+          if (r[i].contains(num))
+            {
+              return true;
+            }
+        }
+      return false;
+    }
+
+    /**
+     * Sets whether the specified article is seen.
+     */
+    public void setSeen(int num, boolean flag)
+    {
+      if (flag)
+        {
+          insert(num);
+        }
+      else
+        {
+          remove(num);
+        }
+    }
+
+    /*
+     * Find the index within seen to insert the specified article.
+     * The range object at the returned index may already contain num.
+     */
+    int indexOf(int num)
+    {
+      int len = seen.size();
+      Range[] r = new Range[len];
+      seen.toArray(r);
+      for (int i = 0; i < len; i++)
+        {
+          if (r[i].contains(num))
+            {
+              return i;
+            }
+          if (r[i].from > num)
+            {
+              return i;
+            }
+          if (r[i].to == num - 1)
+            {
+              return i;
+            }
+        }
+      return len;
+    }
+
+    void insert(int start, int end)
+    {
+      Range range = new Range(start, end);
+      int i1 = indexOf(range.from);
+      // range is at end
+      if (i1 == seen.size())
+        {
+          seen.add(range);
+          return;
+        }
+      Range r1 = (Range) seen.get(i1);
+      // range is before r1
+      if (range.to < r1.from)
+        {
+          seen.add(i1, range);
+          return;
+        }
+      // range is a subset of r1
+      if (r1.from <= range.from && r1.to >= range.to)
+        {
+          return;
+        }
+      // range is a superset of r1
+      int i2 = indexOf(range.to);
+      Range r2 = (Range) seen.get(i2);
+      System.err.println("r2 " + r2 + " i2 " + i2);
+      // remove all ranges between
+      for (int i = i2; i >= i1; i--)
+        {
+          seen.remove(i);
+        }
+      // merge
+      int f = (range.from < r1.from) ? range.from : r1.from;
+      int t = (range.to > r2.to) ? range.to : r2.to;
+      range = new Range(f, t);
+      seen.add(i1, range);
+    }
+
+    void insert(int num)
+    {
+      insert(num, num);
+    }
+
+    void remove(int num)
+    {
+      int i = indexOf(num);
+      Range r = (Range) seen.get(i);
+      seen.remove(i);
+      // num == r
+      if ((r.from == r.to) &&(r.to == num))
+        {
+          return;
+        }
+      // split r
+      if (r.to > num)
+        {
+          Range r2 = new Range(num + 1, r.to);
+          seen.add(i, r2);
+        }
+      if (r.from < num)
+        {
+          Range r2 = new Range(r.from, num - 1);
+          seen.add(i, r2);
+        }
+    }
+
+    public String toString()
+    {
+      StringBuffer buf = new StringBuffer();
+      int len = seen.size();
+      for (int i = 0; i < len; i++)
+        {
+          Range range = (Range) seen.get(i);
+          if (i > 0)
+            {
+              buf.append(',');
+            }
+          buf.append(range.toString());
+        }
+      return buf.toString();
+    }
+
+  }
+
+  /**
+   * A range is either a single integer or a range between two integers.
+   */
+  static class Range
+  {
+    int from;
+    int to;
+
+    public Range(int i)
+    {
+      from = to = i;
+    }
+
+    public Range(int f, int t)
+    {
+      if (f > t)
+        {
+          from = t;
+          to = f;
+        }
+      else
+        {
+          from = f;
+          to = t;
+        }
+    }
+
+    public boolean contains(int num)
+    {
+      return (num >= from && num <= to);
+    }
+
+    public String toString()
+    {
+      if (from != to)
+        {
+          return new StringBuffer()
+            .append(from)
+            .append('-')
+            .append(to)
+            .toString();
+        }
+      else
+        {
+          return Integer.toString(from);
+        }
+    }
+
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/Group.java b/alien/src/gnu/inet/nntp/Group.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/Group.java
@@ -0,0 +1,95 @@
+/*
+ * Group.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * An item in an NNTP newsgroup listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class Group
+{
+
+  String name;
+  int last;
+  int first;
+  boolean canPost;
+
+  Group(String name, int last, int first, boolean canPost)
+  {
+    this.name = name;
+    this.last = last;
+    this.first = first;
+    this.canPost = canPost;
+  }
+  
+  /**
+   * The name of the newsgroup.
+   */
+  public String getName()
+  {
+    return name;
+  }
+
+  /**
+   * The number of the last known article currently in the newsgroup.
+   */
+  public int getLast()
+  {
+    return last;
+  }
+
+  /**
+   * The number of the first article currently in the newsgroup.
+   */
+  public int getFirst()
+  {
+    return first;
+  }
+
+  /**
+   * True if posting to this newsgroup is allowed.
+   */
+  public boolean isCanPost()
+  {
+    return canPost;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/GroupIterator.java b/alien/src/gnu/inet/nntp/GroupIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/GroupIterator.java
@@ -0,0 +1,114 @@
+/*
+ * GroupIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over an NNTP newsgroup listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class GroupIterator
+  extends LineIterator
+{
+
+  static final String CAN_POST = "y";
+
+  GroupIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+
+  /**
+   * Returns the next group.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextGroup();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+
+  /**
+   * Returns the next group.
+   */
+  public Group nextGroup()
+    throws IOException
+  {
+    String line = nextLine();
+
+    // Parse line
+    try
+      {
+        int start = 0, end;
+        end = line.indexOf(' ', start);
+        if (end == -1)
+          return new Group(line, -1, -1, false);
+
+        String name = line.substring(start, end);
+        start = end + 1;
+        end = line.indexOf(' ', start);
+        int last = Integer.parseInt(line.substring(start, end));
+        start = end + 1;
+        end = line.indexOf(' ', start);
+        int first = Integer.parseInt(line.substring(start, end));
+        start = end + 1;
+        boolean canPost = CAN_POST.equals(line.substring(start));
+
+        return new Group(name, last, first, canPost);
+      }
+    catch (StringIndexOutOfBoundsException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid group line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/GroupResponse.java b/alien/src/gnu/inet/nntp/GroupResponse.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/GroupResponse.java
@@ -0,0 +1,78 @@
+/*
+ * GroupResponse.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * An NNTP group status response.
+ * This represents the status response with NNTP code 211, for newsgroup
+ * selection.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class GroupResponse
+  extends StatusResponse
+{
+
+  /*
+   * The estimated number of articles in the group.
+   */
+  public int count;
+
+  /*
+   * The first article number in the group.
+   */
+  public int first;
+
+  /*
+   * The last article number in the group.
+   */
+  public int last;
+
+  /*
+   * The newsgroup name.
+   */
+  public String group;
+
+  GroupResponse(short status, String message)
+  {
+    super(status, message);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/HeaderEntry.java b/alien/src/gnu/inet/nntp/HeaderEntry.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/HeaderEntry.java
@@ -0,0 +1,76 @@
+/*
+ * HeaderEntry.java
+ * Copyright (C) 2002 The free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * An item in an NNTP newsgroup single-header listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class HeaderEntry
+{
+
+  String articleId;
+  String header;
+
+  HeaderEntry(String articleId, String header)
+  {
+    this.articleId = articleId;
+    this.header = header;
+  }
+
+  /**
+   * The article ID. This is either an article number, if a number or range
+   * was used in the XHDR command, or a Message-ID.
+   */
+  public String getArticleId()
+  {
+    return articleId;
+  }
+
+  /**
+   * The requested header value.
+   */
+  public String getHeader()
+  {
+    return header;
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/nntp/HeaderIterator.java b/alien/src/gnu/inet/nntp/HeaderIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/HeaderIterator.java
@@ -0,0 +1,103 @@
+/*
+ * HeaderIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over an NNTP XHDR listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class HeaderIterator
+  extends LineIterator
+{
+
+  HeaderIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+  
+  /**
+   * Returns the next header entry.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextHeaderEntry();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+  
+  /**
+   * Returns the next header entry.
+   */
+  public HeaderEntry nextHeaderEntry()
+    throws IOException
+  {
+    String line = nextLine();
+
+    try
+      {
+        // Parse line
+        int start = 0, end;
+        end = line.indexOf(' ', start);
+        String articleId = line.substring(start, end);
+        start = end + 1;
+        String header = line.substring(start);
+        
+        return new HeaderEntry(articleId, header);
+      }
+    catch (StringIndexOutOfBoundsException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid header line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/LineIterator.java b/alien/src/gnu/inet/nntp/LineIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/LineIterator.java
@@ -0,0 +1,152 @@
+/*
+ * LineIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over an NNTP multi-line response.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class LineIterator
+  implements Iterator, PendingData
+{
+
+  static final String DOT = ".";
+
+  boolean doneRead = false;
+  NNTPConnection connection;
+  String current;
+
+  LineIterator(NNTPConnection connection)
+  {
+    this.connection = connection;
+  }
+
+  void doRead()
+    throws IOException
+  {
+    if (doneRead)
+      {
+        return;
+      }
+    String line = connection.read();
+    if (DOT.equals(line))
+      {
+        current = null;
+      }
+    else
+      {
+        current = line;
+      }
+    doneRead = true;
+  }
+
+  /**
+   * Indicates whether there are more lines to be read.
+   */
+  public boolean hasNext()
+  {
+    try
+      {
+        doRead();
+      }
+    catch (IOException e)
+      {
+        return false;
+      }
+    return(current != null);
+  }
+
+  /**
+   * Returns the next line.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextLine();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+
+  /**
+   * Returns the next line.
+   */
+  public String nextLine()
+    throws IOException
+  {
+    doRead();
+    if (current == null)
+      {
+        throw new NoSuchElementException();
+      }
+    doneRead = false;
+    return current;
+  }
+
+  /**
+   * This iterator is read-only.
+   */
+  public void remove()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * Read to the end of this iterator.
+   */
+  public void readToEOF()
+    throws IOException
+  {
+    do
+      {
+        doRead();
+      }
+    while (current != null);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/NNTPConnection.java b/alien/src/gnu/inet/nntp/NNTPConnection.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/NNTPConnection.java
@@ -0,0 +1,1572 @@
+/*
+ * NNTPConnection.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import gnu.inet.util.CRLFInputStream;
+import gnu.inet.util.CRLFOutputStream;
+import gnu.inet.util.EmptyX509TrustManager;
+import gnu.inet.util.LineInputStream;
+import gnu.inet.util.SaslCallbackHandler;
+import gnu.inet.util.SaslInputStream;
+import gnu.inet.util.SaslOutputStream;
+import gnu.inet.util.TraceLevel;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+
+/**
+ * An NNTP client.
+ * This object is used to establish and manage a connection to an NNTP
+ * server.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ * @author <a href='mailto:jan.michalica@centire.com'>Jan Michalica</a>
+ */
+public class NNTPConnection
+  implements NNTPConstants
+{
+
+  /**
+   * The network trace level.
+   */
+  public static final Level NNTP_TRACE = new TraceLevel("nntp");
+
+  /**
+   * The default NNTP port.
+   */
+  public static final int DEFAULT_PORT = 119;
+  
+  /**
+   * The default NNTPS port
+   */
+  public static final int DEFAULT_SSL_PORT = 563;
+
+  /**
+   * The logger used for NNTP protocol traces.
+   */
+  public final Logger logger = Logger.getLogger("gnu.inet.nntp");
+
+  /**
+   * The hostname of the host we are connected to.
+   */
+  protected String hostname;
+
+  /**
+   * The port on the host we are connected to.
+   */
+  protected int port;
+
+  /**
+   * The socket used for network communication.
+   */
+  protected Socket socket;
+
+  /**
+   * The socket input stream.
+   */
+  protected LineInputStream in;
+
+  /**
+   * The socket output stream.
+   */
+  protected CRLFOutputStream out;
+
+  /**
+   * Whether the host permits posting of articles.
+   */
+  protected boolean canPost;
+
+  /**
+   * The greeting issued by the host when we connected.
+   */
+  protected String welcome;
+
+  /**
+   * Pending data, if any.
+   */
+  protected PendingData pendingData;
+
+  private static final String DOT = ".";
+  private static final String US_ASCII = "US-ASCII";
+
+
+  /**
+   * Creates a new connection object.
+   * @param hostname the hostname or IP address of the news server
+   */
+  public NNTPConnection(String hostname)
+    throws IOException
+  {
+    this(hostname, -1, 0, 0, false, null, true);
+  }
+
+  /**
+   * Creates a new connection object.
+   * @param hostname the hostname or IP address of the news server
+   * @param port the port to connect to
+   */
+  public NNTPConnection(String hostname, int port)
+    throws IOException
+  {
+    this(hostname, port, 0, 0, false, null, true);
+  }
+  
+  /**
+   * Creates a new secure connection using the specified trust manager.
+   * @param host the name of the host to connect to
+   * @param port the port to connect to, or -1 for the default
+   * @param tm a trust manager used to check SSL certificates, or null to
+   * use the default
+   */
+  public NNTPConnection(String host, int port, TrustManager tm)
+    throws IOException
+  {
+    this(host, port, 0, 0, true, tm, true);
+  }
+  
+  /**
+   * Creates a new connection object.
+   * @param hostname the hostname or IP address of the news server
+   * @param port the port to connect to
+   * @param connectionTimeout the socket connection timeout
+   * @param timeout the read timeout on the socket
+   */
+  public NNTPConnection(String hostname, int port,
+                        int connectionTimeout, int timeout)
+    throws IOException
+  {
+    this(hostname, port, connectionTimeout, timeout, false, null, true);
+  }
+
+  /**
+   * Creates a new connection object.
+   * @param hostname the hostname or IP address of the news server
+   * @param port the port to connect to
+   * @param connectionTimeout the socket connection timeout
+   * @param timeout the read timeout on the socket
+   * @param init initialise the connection
+   */
+  public NNTPConnection(String hostname, int port,
+                        int connectionTimeout, int timeout,
+                        boolean secure, TrustManager tm,
+                        boolean init)
+    throws IOException
+  {
+    if (port < 0)
+      {
+    	port = secure ? DEFAULT_SSL_PORT : DEFAULT_PORT;
+      }
+    
+    this.hostname = hostname;
+    this.port = port;
+    
+    // Set up the socket and streams
+    try
+      {
+        socket = new Socket();
+        InetSocketAddress address = new InetSocketAddress(hostname, port);
+        if (connectionTimeout > 0)
+          {
+            socket.connect(address, connectionTimeout);
+          }
+        else
+          {
+            socket.connect(address);
+          }
+        if (timeout > 0)
+          {
+            socket.setSoTimeout(timeout);
+          }
+        if (secure)
+          {
+		    SSLSocketFactory factory = getSSLSocketFactory(tm);
+		    SSLSocket ss =
+			  (SSLSocket) factory.createSocket(socket, hostname, port, true);
+		    String[] protocols = { "TLSv1", "SSLv3" };
+		    ss.setEnabledProtocols(protocols);
+		    ss.setUseClientMode(true);
+		    ss.startHandshake();
+		    socket = ss;
+	      }
+      }
+    catch (GeneralSecurityException e)
+      {
+    	throw (IOException) new IOException().initCause(e);
+      }
+    InputStream is = socket.getInputStream();
+    is = new CRLFInputStream(is);
+    in = new LineInputStream(is);
+    OutputStream os = socket.getOutputStream();
+    os = new BufferedOutputStream(os);
+    out = new CRLFOutputStream(os);
+    
+    if (init)
+      init();
+  }
+
+  /**
+   * Returns a configured SSLSocketFactory to use in creating new SSL
+   * sockets.
+   * @param tm an optional trust manager to use
+   */
+  protected SSLSocketFactory getSSLSocketFactory(TrustManager tm)
+    throws GeneralSecurityException
+  {
+    if (tm == null)
+      {
+        tm = new EmptyX509TrustManager();
+      }
+    SSLContext context = SSLContext.getInstance("TLS");
+    TrustManager[] trust = new TrustManager[] { tm };
+    context.init(null, trust, null);
+    return context.getSocketFactory();
+  }
+  
+  /**
+   * Initialises the connection.
+   * Unless the init parameter was provided with the value false,
+   * do not call this method. Otherwise call it only once after e.g.
+   * configuring logging.
+   */
+  public void init()
+    throws IOException
+  {
+    // Read the welcome message(RFC977:2.4.3)
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case POSTING_ALLOWED:
+        canPost = true;
+      case NO_POSTING_ALLOWED:
+        welcome = response.getMessage();
+        break;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  /**
+   * Negotiate TLS over the current connection.
+   * This depends on many features, such as the JSSE classes being in the
+   * classpath. Returns true if successful, false otherwise.
+   */
+  public boolean starttls()
+    throws IOException
+  {
+    return starttls(new EmptyX509TrustManager());
+  }
+  
+  /**
+   * This command performs a TLS negotiation.
+   * See RFC 4642 for details.
+   * @param tm the custom trust manager to use
+   * @return true if successful, false otherwise
+   */
+  public boolean starttls(TrustManager tm)
+  	throws IOException
+  {
+	  try
+	    {
+		  SSLSocketFactory factory = getSSLSocketFactory(tm);
+		  
+		  send(STARTTLS);
+		  StatusResponse response = parseResponse(read());
+		  
+		  switch (response.status)
+		    {
+		  case TLS_INIT_ERROR:
+		  case PERMISSION_DENIED: // e.g. TLS is already active
+		  case COMMAND_NOT_RECOGNIZED: // i.e. server does not implement/allow STARTTLS
+		  /*
+		   * Although RFC 4642 forbids it,
+		   * INN returns this when STARTTLS has already succeeded.
+		   */
+		  case ENCRYPTION_OR_AUTH_REQUIRED:
+		  	  return false;
+		  case CONTINUE_TLS_NEGOTIATION:
+			  break;
+		  default:
+			  throw new NNTPException(response);
+		    }
+		  
+		  SSLSocket ss =
+	          (SSLSocket) factory.createSocket(socket, hostname, port, true);
+	      String[] protocols = { "TLSv1", "SSLv3" };
+	      ss.setEnabledProtocols(protocols);
+	      ss.setUseClientMode(true);
+	      ss.startHandshake();
+		  
+	      // Set up streams
+	      InputStream in = socket.getInputStream();
+	      in = new CRLFInputStream(in);
+	      this.in = new LineInputStream(in);
+	      OutputStream out = socket.getOutputStream();
+	      out = new BufferedOutputStream(out);
+	      this.out = new CRLFOutputStream(out);
+	      
+		  return true;
+		  
+	    }
+	  catch (GeneralSecurityException e)
+	    {
+		  e.printStackTrace();
+		  return false;
+	    }
+  }
+  
+  
+  /**
+   * Return the welcome message sent by the server in reply to the initial
+   * connection.
+   * This message sometimes contains disclaimers or help information that
+   * may be relevant to the user.
+   */
+  public String getWelcome()
+  {
+    return welcome;
+  }
+  
+  /*
+   * Returns an NNTP-format date string.
+   * This is only required when clients use the NEWGROUPS or NEWNEWS
+   * methods, therefore rarely: we don't cache any of the variables here.
+   */
+  String formatDate(Date date)
+  {
+    DateFormat df = new SimpleDateFormat("yyMMdd HHmmss 'GMT'");
+    Calendar cal = new GregorianCalendar();
+    TimeZone gmt = TimeZone.getTimeZone("GMT");
+    cal.setTimeZone(gmt);
+    df.setCalendar(cal);
+    cal.setTime(date);
+    return df.format(date);
+  }
+
+  /*
+   * Parse the specfied NNTP date text.
+   */
+  Date parseDate(String text)
+    throws ParseException
+  {
+    DateFormat df = new SimpleDateFormat("yyMMdd HHmmss 'GMT'");
+    return df.parse(text);
+  }
+
+  // RFC977:3.1 The ARTICLE, BODY, HEAD, and STAT commands
+
+  /**
+   * Send an article retrieval request to the server.
+   * @param articleNumber the article number of the article to retrieve
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article header and
+   * body, separated by an empty line
+   */
+  public ArticleResponse article(int articleNumber)
+    throws IOException
+  {
+    return articleImpl(ARTICLE, Integer.toString(articleNumber));
+  }
+  
+  /**
+   * Send an article retrieval request to the server.
+   * @param messageId the message-id of the article to retrieve
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article header and
+   * body, separated by an empty line
+   */
+  public ArticleResponse article(String messageId)
+    throws IOException
+  {
+    return articleImpl(ARTICLE, messageId);
+  }
+
+  /**
+   * Send an article head retrieval request to the server.
+   * @param articleNumber the article number of the article to head
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article header
+   */
+  public ArticleResponse head(int articleNumber)
+    throws IOException
+  {
+    return articleImpl(HEAD, Integer.toString(articleNumber));
+  }
+
+  /**
+   * Send an article head retrieval request to the server.
+   * @param messageId the message-id of the article to head
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article header
+   */
+  public ArticleResponse head(String messageId)
+    throws IOException
+  {
+    return articleImpl(HEAD, messageId);
+  }
+
+  /**
+   * Send an article body retrieval request to the server.
+   * @param articleNumber the article number of the article to body
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article body
+   */
+  public ArticleResponse body(int articleNumber)
+    throws IOException
+  {
+    return articleImpl(BODY, Integer.toString(articleNumber));
+  }
+
+  /**
+   * Send an article body retrieval request to the server.
+   * @param messageId the message-id of the article to body
+   * @return an article response consisting of the article number and
+   * message-id, and an iterator over the lines of the article body
+   */
+  public ArticleResponse body(String messageId)
+    throws IOException
+  {
+    return articleImpl(BODY, messageId);
+  }
+
+  /**
+   * Send an article status request to the server.
+   * @param articleNumber the article number of the article to stat
+   * @return an article response consisting of the article number and
+   * message-id
+   */
+  public ArticleResponse stat(int articleNumber)
+    throws IOException
+  {
+    return articleImpl(STAT, Integer.toString(articleNumber));
+  }
+
+  /**
+   * Send an article status request to the server.
+   * @param messageId the message-id of the article to stat
+   * @return an article response consisting of the article number and
+   * message-id
+   */
+  public ArticleResponse stat(String messageId)
+    throws IOException
+  {
+    return articleImpl(STAT, messageId);
+  }
+
+  /**
+   * Performs an ARTICLE, BODY, HEAD, or STAT command.
+   * @param command one of the above commands
+   * @param messageId the article-number or message-id in string form
+   */
+  protected ArticleResponse articleImpl(String command, String messageId)
+    throws IOException
+  {
+    if (messageId != null)
+      {
+        StringBuffer line = new StringBuffer(command);
+        line.append(' ');
+        line.append(messageId);
+        send(line.toString());
+      }
+    else
+      {
+        send(command);
+      }
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case ARTICLE_FOLLOWS:
+      case HEAD_FOLLOWS:
+      case BODY_FOLLOWS:
+        ArticleResponse aresponse = (ArticleResponse) response;
+        ArticleStream astream = new ArticleStream(in);
+        pendingData = astream;
+        aresponse.in = astream;
+        return aresponse;
+      case ARTICLE_RETRIEVED:
+        return (ArticleResponse) response;
+      default:
+        // NO_GROUP_SELECTED
+        // NO_ARTICLE_SELECTED
+        // NO_SUCH_ARTICLE_NUMBER
+        // NO_SUCH_ARTICLE
+        // NO_PREVIOUS_ARTICLE
+        // NO_NEXT_ARTICLE
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC977:3.2 The GROUP command
+
+  /**
+   * Send a group selection command to the server.
+   * Returns a group status response.
+   * @param name the name of the group to select
+   */
+  public GroupResponse group(String name)
+    throws IOException
+  {
+    send(GROUP + ' ' + name);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case GROUP_SELECTED:
+        return (GroupResponse) response;
+      default:
+        // NO_SUCH_GROUP
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC977:3.3 The HELP command
+
+  /**
+   * Requests a help listing.
+   * @return an iterator over a collection of help lines.
+   */
+  public LineIterator help()
+    throws IOException
+  {
+    send(HELP);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case HELP_TEXT:
+        LineIterator li = new LineIterator(this);
+        pendingData = li;
+        return li;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC977:3.4 The IHAVE command
+
+  /**
+   * Sends an ihave command indicating that the client has an article with
+   * the specified message-id.
+   * @param messageId the article message-id
+   * @return a PostStream if the server wants the specified article, null
+   * otherwise
+   */
+  public PostStream ihave(String messageId)
+    throws IOException
+  {
+    send(IHAVE + ' ' + messageId);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case SEND_TRANSFER_ARTICLE:
+        return new PostStream(this, false);
+      case ARTICLE_NOT_WANTED:
+        return null;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC(77:3.5 The LAST command
+
+  /**
+   * Sends a previous article positioning command to the server.
+   * @return the article number/message-id pair associated with the new
+   * article
+   */
+  public ArticleResponse last()
+    throws IOException
+  {
+    return articleImpl(LAST, null);
+  }
+
+  // RFC977:3.6 The LIST command
+
+  /**
+   * Send a group listing command to the server.
+   * Returns a GroupIterator. This must be read fully before other commands
+   * are issued.
+   */
+  public GroupIterator list()
+    throws IOException
+  {
+    return listImpl(LIST);
+  }
+  
+  GroupIterator listImpl(String command)
+    throws IOException
+  {
+    send(command);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case LIST_FOLLOWS:
+        GroupIterator gi = new GroupIterator(this);
+        pendingData = gi;
+        return gi;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC977:3.7 The NEWGROUPS command
+
+  /**
+   * Returns an iterator over the list of new groups on the server since the
+   * specified date.
+   * NB this method suffers from a minor millenium bug.
+   * 
+   * @param since the date from which to list new groups
+   * @param distributions if non-null, an array of distributions to match
+   */
+  public LineIterator newGroups(Date since, String[]distributions)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(NEWGROUPS);
+    buffer.append(' ');
+    buffer.append(formatDate(since));
+    if (distributions != null)
+      {
+        buffer.append(' ');
+        for (int i = 0; i < distributions.length; i++)
+          {
+            if (i > 0)
+              {
+                buffer.append(',');
+              }
+            buffer.append(distributions[i]);
+          }
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case NEWGROUPS_LIST_FOLLOWS:
+        LineIterator li = new LineIterator(this);
+        pendingData = li;
+        return li;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC977:3.8 The NEWNEWS command
+
+  /**
+   * Returns an iterator over the list of message-ids posted or received to
+   * the specified newsgroup(s) since the specified date.
+   * NB this method suffers from a minor millenium bug.
+   *
+   * @param newsgroup the newsgroup wildmat
+   * @param since the date from which to list new articles
+   * @param distributions if non-null, a list of distributions to match
+   */
+  public LineIterator newNews(String newsgroup, Date since,
+                              String[] distributions)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(NEWNEWS);
+    buffer.append(' ');
+    buffer.append(newsgroup);
+    buffer.append(' ');
+    buffer.append(formatDate(since));
+    if (distributions != null)
+      {
+        buffer.append(' ');
+        for (int i = 0; i < distributions.length; i++)
+          {
+            if (i > 0)
+              {
+                buffer.append(',');
+              }
+            buffer.append(distributions[i]);
+          }
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case NEWNEWS_LIST_FOLLOWS:
+        LineIterator li = new LineIterator(this);
+        pendingData = li;
+        return li;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC977:3.9 The NEXT command
+
+  /**
+   * Sends a next article positioning command to the server.
+   * @return the article number/message-id pair associated with the new
+   * article
+   */
+  public ArticleResponse next()
+    throws IOException
+  {
+    return articleImpl(NEXT, null);
+  }
+
+  // RFC977:3.10 The POST command
+
+  /**
+   * Post an article. This is a two-stage process.
+   * If successful, returns an output stream to write the article to.
+   * Clients should call <code>write()</code> on the stream for all the
+   * bytes of the article, and finally call <code>close()</code>
+   * on the stream.
+   * No other method should be called in between.
+   * @see #postComplete
+   */
+  public OutputStream post()
+    throws IOException
+  {
+    send(POST);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case SEND_ARTICLE:
+        return new PostStream(this, false);
+      default:
+        // POSTING_NOT_ALLOWED
+        throw new NNTPException(response);
+      }
+  }
+  
+  /**
+   * Indicates that the client has finished writing all the bytes of the
+   * article.
+   * Called by the PostStream during <code>close()</code>.
+   * @see #post
+   */
+  void postComplete()
+    throws IOException
+  {
+    send(DOT);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case ARTICLE_POSTED:
+      case ARTICLE_TRANSFERRED:
+        return;
+      default:
+        // POSTING_FAILED
+        // TRANSFER_FAILED
+        // ARTICLE_REJECTED
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC977:3.11 The QUIT command
+
+  /**
+   * Close the connection.
+   * After calling this method, no further calls on this object are valid.
+   */
+  public void quit()
+    throws IOException
+  {
+    send(QUIT);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case CLOSING_CONNECTION:
+        socket.close();
+        return;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC977:3.12 The SLAVE command
+
+  /**
+   * Indicates to the server that this is a slave connection.
+   */
+  public void slave()
+    throws IOException
+  {
+    send(SLAVE);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case SLAVE_ACKNOWLEDGED:
+        break;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:1.1 The CHECK command
+
+  public boolean check(String messageId)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(CHECK);
+    buffer.append(' ');
+    buffer.append(messageId);
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case SEND_ARTICLE_VIA_TAKETHIS:
+        return true;
+      case ARTICLE_NOT_WANTED_VIA_TAKETHIS:
+        return false;
+      default:
+        // SERVICE_DISCONTINUED
+        // TRY_AGAIN_LATER
+        // TRANSFER_PERMISSION_DENIED
+        // COMMAND_NOT_RECOGNIZED
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:1.2 The MODE STREAM command
+
+  /**
+   * Attempt to initialise the connection in streaming mode.
+   * This is generally used to bypass the lock step nature of NNTP in order
+   * to perform a series of CHECK and TAKETHIS commands.
+   *
+   * @return true if the server supports streaming mode
+   */
+  public boolean modeStream()
+    throws IOException
+  {
+    send(MODE_STREAM);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case STREAMING_OK:
+        return true;
+      default:
+        // COMMAND_NOT_RECOGNIZED
+        return false;
+      }
+  }
+  
+  // RFC2980:1.3 The TAKETHIS command
+
+  /**
+   * Implements the out-of-order takethis command.
+   * The client uses the returned output stream to write all the bytes of the
+   * article. When complete, it calls <code>close()</code> on the
+   * stream.
+   * @see #takethisComplete
+   */
+  public OutputStream takethis(String messageId)
+    throws IOException
+  {
+    send(TAKETHIS + ' ' + messageId);
+    return new PostStream(this, true);
+  }
+
+  /**
+   * Completes a takethis transaction.
+   * Called by PostStream.close().
+   * @see #takethis
+   */
+  void takethisComplete()
+    throws IOException
+  {
+    send(DOT);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case ARTICLE_TRANSFERRED_OK:
+        return;
+      default:
+        // SERVICE_DISCONTINUED
+        // ARTICLE_TRANSFER_FAILED
+        // TRANSFER_PERMISSION_DENIED
+        // COMMAND_NOT_RECOGNIZED
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:1.4 The XREPLIC command
+
+  // TODO
+
+  // RFC2980:2.1.2 The LIST ACTIVE command
+
+  /**
+   * Returns an iterator over the groups specified according to the wildmat
+   * pattern. The iterator must be read fully before other commands are
+   * issued.
+   * @param wildmat the wildmat pattern. If null, returns all groups. If no
+   * groups are matched, returns an empty iterator.
+   */
+  public GroupIterator listActive(String wildmat)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(LIST_ACTIVE);
+    if (wildmat != null)
+      {
+        buffer.append(' ');
+        buffer.append(wildmat);
+      }
+    return listImpl(buffer.toString());
+  }
+  
+  // RFC2980:2.1.3 The LIST ACTIVE.TIMES command
+
+  /**
+   * Returns an iterator over the active.times file.
+   * Each ActiveTime object returned provides details of who created the
+   * newsgroup and when.
+   */
+  public ActiveTimesIterator listActiveTimes()
+    throws IOException
+  {
+    send(LIST_ACTIVE_TIMES);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case LIST_FOLLOWS:
+        return new ActiveTimesIterator(this);
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:2.1.4 The LIST DISTRIBUTIONS command
+
+  // TODO
+
+  // RFC2980:2.1.5 The LIST DISTRIB.PATS command
+
+  // TODO
+
+  // RFC2980:2.1.6 The LIST NEWSGROUPS command
+
+  /**
+   * Returns an iterator over the group descriptions for the given groups.
+   * @param wildmat if non-null, limits the groups in the iterator to the
+   * specified pattern
+   * @return an iterator over group name/description pairs
+   * @see #xgtitle
+   */
+  public PairIterator listNewsgroups(String wildmat)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(LIST_NEWSGROUPS);
+    if (wildmat != null)
+      {
+        buffer.append(' ');
+        buffer.append(wildmat);
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case LIST_FOLLOWS:
+        PairIterator pi = new PairIterator(this);
+        pendingData = pi;
+        return pi;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:2.1.7 The LIST OVERVIEW.FMT command
+
+  /**
+   * Returns an iterator over the order in which headers are stored in the
+   * overview database.
+   * Each line returned by the iterator contains one header field.
+   * @see #xover
+   */
+  public LineIterator listOverviewFmt()
+    throws IOException
+  {
+    send(LIST_OVERVIEW_FMT);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case LIST_FOLLOWS:
+        LineIterator li = new LineIterator(this);
+        pendingData = li;
+        return li;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:2.1.8 The LIST SUBSCRIPTIONS command
+
+  /**
+   * Returns a list of newsgroups suitable for new users of the server.
+   */
+  public GroupIterator listSubscriptions()
+    throws IOException
+  {
+    return listImpl(LIST_SUBSCRIPTIONS);
+  }
+
+  // RFC2980:2.2 The LISTGROUP command
+
+  /**
+   * Returns a listing of all the article numbers in the specified
+   * newsgroup. If the <code>group</code> parameter is null, the currently
+   * selected group is assumed.
+   * @param group the name of the group to list articles for
+   */
+  public ArticleNumberIterator listGroup(String group)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(LISTGROUP);
+    if (group != null)
+      {
+        buffer.append(' ');
+        buffer.append(group);
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read(), true);
+    switch (response.status)
+      {
+      case GROUP_SELECTED:
+        ArticleNumberIterator ani = new ArticleNumberIterator(this);
+        pendingData = ani;
+        return ani;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:2.3 The MODE READER command
+
+  /**
+   * Indicates to the server that this is a user-agent.
+   * @return true if posting is allowed, false otherwise.
+   */
+  public boolean modeReader()
+    throws IOException
+  {
+    send(MODE_READER);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case POSTING_ALLOWED:
+        canPost = true;
+        return canPost;
+      case POSTING_NOT_ALLOWED:
+        canPost = false;
+        return canPost;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:2.4 The XGTITLE command
+
+  /**
+   * Returns an iterator over the list of newsgroup descriptions.
+   * @param wildmat if non-null, the newsgroups to match
+   */
+  public PairIterator xgtitle(String wildmat)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(XGTITLE);
+    if (wildmat != null)
+      {
+        buffer.append(' ');
+        buffer.append(wildmat);
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case XGTITLE_LIST_FOLLOWS:
+        PairIterator pi = new PairIterator(this);
+        pendingData = pi;
+        return pi;
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:2.6 The XHDR command
+
+  public HeaderIterator xhdr(String header, String range)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(XHDR);
+    buffer.append(' ');
+    buffer.append(header);
+    if (range != null)
+      {
+        buffer.append(' ');
+        buffer.append(range);
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case HEAD_FOLLOWS:
+        HeaderIterator hi = new HeaderIterator(this);
+        pendingData = hi;
+        return hi;
+      default:
+        // NO_GROUP_SELECTED
+        // NO_SUCH_ARTICLE
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:2.7 The XINDEX command
+
+  // TODO
+
+  // RFC2980:2.8 The XOVER command
+
+  public OverviewIterator xover(Range range)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(XOVER);
+    if (range != null)
+      {
+        buffer.append(' ');
+        buffer.append(range.toString());
+      }
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case OVERVIEW_FOLLOWS:
+        OverviewIterator oi = new OverviewIterator(this);
+        pendingData = oi;
+        return oi;
+      default:
+        // NO_GROUP_SELECTED
+        // PERMISSION_DENIED
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:2.9 The XPAT command
+
+  // TODO
+
+  // RFC2980:2.10 The XPATH command
+
+  // TODO
+
+  // RFC2980:2.11 The XROVER command
+
+  // TODO
+
+  // RFC2980:2.12 The XTHREAD command
+
+  // TODO
+
+  // RFC2980:3.1.1 Original AUTHINFO
+
+  /**
+   * Basic authentication strategy.
+   * @param username the user to authenticate
+   * @param password the(cleartext) password
+   * @return true on success, false on failure
+   */
+  public boolean authinfo(String username, String password)
+    throws IOException
+  {
+    StringBuffer buffer = new StringBuffer(AUTHINFO_USER);
+    buffer.append(' ');
+    buffer.append(username);
+    send(buffer.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case AUTHINFO_OK:
+        return true;
+      case SEND_AUTHINFOPASS:
+        buffer.setLength(0);
+        buffer.append(AUTHINFO_PASS);
+        buffer.append(' ');
+        buffer.append(password);
+        send(buffer.toString());
+        response = parseResponse(read());
+        switch (response.status)
+          {
+          case AUTHINFO_OK:
+            return true;
+          case PERMISSION_DENIED:
+            return false;
+          default:
+            throw new NNTPException(response);
+          }
+      default:
+        // AUTHINFO_REJECTED
+        throw new NNTPException(response);
+      }
+  }
+
+  // RFC2980:3.1.2 AUTHINFO SIMPLE
+
+  /**
+   * Implementation of NNTP simple authentication.
+   * Note that use of this authentication strategy is highly deprecated,
+   * only use on servers that won't accept any other form of authentication.
+   */
+  public boolean authinfoSimple(String username, String password)
+    throws IOException
+  {
+    send(AUTHINFO_SIMPLE);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case SEND_AUTHINFO_SIMPLE:
+        StringBuffer buffer = new StringBuffer(username);
+        buffer.append(' ');
+        buffer.append(password);
+        send(buffer.toString());
+        response = parseResponse(read());
+        switch (response.status)
+          {
+          case AUTHINFO_SIMPLE_OK:
+            return true;
+          case AUTHINFO_SIMPLE_DENIED:
+            return false;
+          default:throw new NNTPException(response);
+          }
+      default:
+        throw new NNTPException(response);
+      }
+  }
+  
+  // RFC2980:3.1.3 AUTHINFO GENERIC
+
+  /**
+   * Authenticates the connection using the specified SASL mechanism,
+   * username and password.
+   * @param mechanism a SASL authentication mechanism, e.g. LOGIN, PLAIN,
+   * CRAM-MD5, GSSAPI
+   * @param username the authentication principal
+   * @param password the authentication credentials
+   */
+  public boolean authinfoGeneric(String mechanism,
+                                  String username, String password)
+    throws IOException
+  {
+    String[] m = new String[] { mechanism };
+    CallbackHandler ch = new SaslCallbackHandler(username, password);
+    // Avoid lengthy callback procedure for GNU Crypto
+    HashMap p = new HashMap();
+    p.put("gnu.crypto.sasl.username", username);
+    p.put("gnu.crypto.sasl.password", password);
+    SaslClient sasl =
+      Sasl.createSaslClient(m, null, "nntp",
+                             socket.getInetAddress().getHostName(),
+                             p, ch);
+    if (sasl == null)
+      {
+        return false;
+      }
+    
+    StringBuffer cmd = new StringBuffer(AUTHINFO_GENERIC);
+    cmd.append(' ');
+    cmd.append(mechanism);
+    if (sasl.hasInitialResponse())
+      {
+        cmd.append(' ');
+        byte[] init = sasl.evaluateChallenge(new byte[0]);
+        cmd.append(new String(init, "US-ASCII"));
+      }
+    send(cmd.toString());
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case AUTHINFO_OK:
+        String qop = (String) sasl.getNegotiatedProperty(Sasl.QOP);
+        if ("auth-int".equalsIgnoreCase(qop)
+            || "auth-conf".equalsIgnoreCase(qop))
+          {
+            InputStream is = socket.getInputStream();
+            is = new BufferedInputStream(is);
+            is = new SaslInputStream(sasl, is);
+            is = new CRLFInputStream(is);
+            in = new LineInputStream(is);
+            OutputStream os = socket.getOutputStream();
+            os = new BufferedOutputStream(os);
+            os = new SaslOutputStream(sasl, os);
+            out = new CRLFOutputStream(os);
+          }
+        return true;
+      case PERMISSION_DENIED:
+        return false;
+      case COMMAND_NOT_RECOGNIZED:
+      case SYNTAX_ERROR:
+      case INTERNAL_ERROR:
+      default:
+        throw new NNTPException(response);
+        // FIXME how does the server send continuations?
+      }
+  }
+  
+  // RFC2980:3.2 The DATE command
+
+  /**
+   * Returns the date on the server.
+   */
+  public Date date()
+    throws IOException
+  {
+    send(DATE);
+    StatusResponse response = parseResponse(read());
+    switch (response.status)
+      {
+      case DATE_OK:
+        String message = response.getMessage();
+        try
+          {
+            DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
+            return df.parse(message);
+          }
+        catch (ParseException e)
+          {
+            throw new IOException("Invalid date: " + message);
+          }
+      default:
+        throw new NNTPException(response);
+      }
+  }
+
+  // -- Utility functions --
+
+  /**
+   * Parse a response object from a response line sent by the server.
+   */
+  protected StatusResponse parseResponse(String line)
+    throws ProtocolException
+  {
+    return parseResponse(line, false);
+  }
+  
+  /**
+   * Parse a response object from a response line sent by the server.
+   * @param isListGroup whether we are invoking the LISTGROUP command
+   */
+  protected StatusResponse parseResponse(String line, boolean isListGroup)
+    throws ProtocolException
+  {
+    if (line == null)
+      {
+        throw new ProtocolException(hostname + " closed connection");
+      }
+    int start = 0, end;
+    short status = -1;
+    String statusText = line;
+    String message = null;
+    end = line.indexOf(' ', start);
+    if (end > start)
+      {
+        statusText = line.substring(start, end);
+        message = line.substring(end + 1);
+      }
+    try
+      {
+        status = Short.parseShort(statusText);
+      }
+    catch (NumberFormatException e)
+      {
+        throw new ProtocolException(line);
+      }
+    StatusResponse response;
+    switch (status)
+      {
+      case ARTICLE_FOLLOWS:
+      case HEAD_FOLLOWS:
+      case BODY_FOLLOWS:
+      case ARTICLE_RETRIEVED:
+      case GROUP_SELECTED:
+        /* The LISTGROUP command returns a list of articles with a 211,
+         * instead of the newsgroup totals returned with the GROUP command.
+         * Check for this case. */
+        if (status != GROUP_SELECTED || isListGroup)
+          {
+            try
+              {
+                ArticleResponse aresponse =
+                  new ArticleResponse(status, message);
+                // article number
+                start = end + 1;
+                end = line.indexOf(' ', start);
+                if (end > start)
+                  {
+                    aresponse.articleNumber =
+                      Integer.parseInt(line.substring(start, end));
+                  }
+                // message-id
+                start = end + 1;
+                end = line.indexOf(' ', start);
+                if (end > start)
+                  {
+                    aresponse.messageId = line.substring(start, end);
+                  }
+                else
+                  {
+                    aresponse.messageId = line.substring(start);
+                  }
+                response = aresponse;
+              }
+            catch (NumberFormatException e)
+              {
+                // This will happen for XHDR
+                response = new StatusResponse(status, message);
+              }
+            break;
+          }
+        // This is the normal case for GROUP_SELECTED
+        GroupResponse gresponse = new GroupResponse(status, message);
+        try
+          {
+            // count
+            start = end + 1;
+            end = line.indexOf(' ', start);
+            if (end > start)
+              {
+                gresponse.count =
+                  Integer.parseInt(line.substring(start, end));
+              }
+            // first
+            start = end + 1;
+            end = line.indexOf(' ', start);
+            if (end > start)
+              {
+                gresponse.first =
+                  Integer.parseInt(line.substring(start, end));
+              }
+            // last
+            start = end + 1;
+            end = line.indexOf(' ', start);
+            if (end > start)
+              {
+                gresponse.last =
+                  Integer.parseInt(line.substring(start, end));
+              }
+            // group
+            start = end + 1;
+            end = line.indexOf(' ', start);
+            if (end > start)
+              {
+                gresponse.group = line.substring(start, end);
+              }
+            else
+              {
+                gresponse.group = line.substring(start);
+              }
+          }
+        catch (NumberFormatException e)
+          {
+            throw new ProtocolException(line);
+          }
+        response = gresponse;
+        break;
+      default:
+        response = new StatusResponse(status, message);
+      }
+    return response;
+  }
+  
+  /**
+   * Send a single line to the server.
+   * @param line the line to send
+   */
+  protected void send(String line)
+    throws IOException
+  {
+    if (pendingData != null)
+      {
+        // Clear pending data
+        pendingData.readToEOF();
+        pendingData = null;
+      }
+    logger.log(NNTP_TRACE, ">" + line);
+    byte[] data = line.getBytes(US_ASCII);
+    out.write(data);
+    out.writeln();
+    out.flush();
+  }
+  
+  /**
+   * Read a single line from the server.
+   * @return a line of text
+   */
+  protected String read()
+    throws IOException
+  {
+    String line = in.readLine();
+    if (line == null)
+      {
+        logger.log(NNTP_TRACE, "<EOF");
+      }
+    else
+      {
+        logger.log(NNTP_TRACE, "<" + line);
+      }
+    return line;
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/nntp/NNTPConstants.java b/alien/src/gnu/inet/nntp/NNTPConstants.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/NNTPConstants.java
@@ -0,0 +1,404 @@
+/*
+ * NNTPConstants.java
+ * Copyright (C) 2002 The free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * NNTP status response codes.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ * @author <a href='mailto:jan.michalica@centire.com'>Jan Michalica</a>
+ */
+public interface NNTPConstants
+{
+
+  /**
+   * Indicates a line of help text.
+   */
+  public static final short HELP_TEXT = 100;
+
+  /**
+   * Indicates a DATE response.
+   */
+  public static final short DATE_OK = 111;
+
+  /**
+   * Indicates that the server is ready and posting is allowed.
+   */
+  public static final short POSTING_ALLOWED = 200;
+
+  /**
+   * Indicates that the server is ready but posting is not allowed.
+   */
+  public static final short NO_POSTING_ALLOWED = 201;
+
+  /**
+   * Indicates that the server has noted the slave status of the connection.
+   */
+  public static final short SLAVE_ACKNOWLEDGED = 202;
+
+  /**
+   * Indicates that the server has accepted streaming mode.
+   */
+  public static final short STREAMING_OK = 203;
+
+  /**
+   * Indicates that the server is closing the connection.
+   */
+  public static final short CLOSING_CONNECTION = 205;
+
+  /**
+   * Indicates that the newsgroup was successfully selected.
+   * Format of the message is "<tt>n f l s</tt> xxx"
+   * (<tt>n</tt> = estimated number of articles in group,
+   * <tt>f</tt> = first article number in the group,
+   * <tt>l</tt> = last article number in the group,
+   * <tt>s</tt> = name of the group.)
+   */
+  public static final short GROUP_SELECTED = 211;
+
+  /**
+   * Indicates that a list of valid newsgroups follows.
+   * The format of each following line is "<tt>g l f p</tt>"
+   * (<tt>g</tt> = newsgroup name,
+   * <tt>l</tt> = last article number in group,
+   * <tt>f</tt> = first article number in group,
+   * <tt>p</tt> = 'y' if posting to the group is allowed,
+   * 'n' otherwise)
+   */
+  public static final short LIST_FOLLOWS = 215;
+
+  /**
+   * Indicates that the article has been retrieved.
+   * The head and body of the article follow.
+   */
+  public static final short ARTICLE_FOLLOWS = 220;
+
+  /**
+   * Indicates that the article has been retrieved.
+   * The head of the article follows.
+   */
+  public static final short HEAD_FOLLOWS = 221;
+
+  /**
+   * Indicates that the article has been retrieved.
+   * The body of the article follows.
+   */
+  public static final short BODY_FOLLOWS = 222;
+
+  /**
+   * Indicates that the article has been retrieved.
+   * The text of the article must be requested separately.
+   */
+  public static final short ARTICLE_RETRIEVED = 223;
+
+  /**
+   * Indicates that a listing of overview information follows.
+   */
+  public static final short OVERVIEW_FOLLOWS = 224;
+
+  /**
+   * Indicates that a list of new articles by message-id follows.
+   */
+  public static final short NEWNEWS_LIST_FOLLOWS = 230;
+
+  /**
+   * Indicates that a list of new newsgroups follows.
+   * This code is issued following a successful NEWGROUPS command. The
+   * format of the listing is the same as for code 215 (list follows).
+   */
+  public static final short NEWGROUPS_LIST_FOLLOWS = 231;
+
+  /**
+   * Indicates that the article was correctly transferred.
+   */
+  public static final short ARTICLE_TRANSFERRED = 235;
+
+  /**
+   * Indicates that the server does not have the specified article and would
+   * like it to be transferred via TAKETHIS.
+   */
+  public static final short SEND_ARTICLE_VIA_TAKETHIS = 238;
+
+  /**
+   * Indicates that the server accepted the article transferred by a
+   * TAKETHIS command.
+   */
+  public static final short ARTICLE_TRANSFERRED_OK = 239;
+
+  /**
+   * Indicates that the article was successfully posted.
+   */
+  public static final short ARTICLE_POSTED = 240;
+
+  /**
+   * Indicates success of an AUTHINFO SIMPLE transaction.
+   */
+  public static final short AUTHINFO_SIMPLE_OK = 350;
+
+  /**
+   * Indicates that AUTHINFO authentication was successful.
+   */
+  public static final short AUTHINFO_OK = 281;
+
+  /**
+   * Indicates that the article to be transferred should be sent by the
+   * client. It should end with a CRLF-dot-CRLF sequence, i.e. a dot on a
+   * line by itself.
+   */
+  public static final short SEND_TRANSFER_ARTICLE = 335;
+
+  /**
+   * Indicates that the article to be posted should be sent by the
+   * client. It should end with a CRLF-dot-CRLF sequence, i.e. a dot on a
+   * line by itself.
+   */
+  public static final short SEND_ARTICLE = 340;
+
+  /**
+   * Instructs the client to send a username/password pair according to the
+   * AUTHINFO SIMPLE specification.
+   */
+  public static final short SEND_AUTHINFO_SIMPLE = 350;
+
+  /**
+   * Indicates that the server is ready to accept the AUTHINFO password.
+   */
+  public static final short SEND_AUTHINFOPASS = 381;
+  
+  /**
+   * Indicates that the server is ready to proceed with TLS negotiation.
+   */
+  public static final short CONTINUE_TLS_NEGOTIATION = 382;
+
+  /**
+   * Indicates that the service has been discontinued.
+   */
+  public static final short SERVICE_DISCONTINUED = 400;
+
+  /**
+   * Indicates that no such newsgroup exists.
+   */
+  public static final short NO_SUCH_GROUP = 411;
+
+  /**
+   * Indicates that no newsgroup has been selected.
+   */
+  public static final short NO_GROUP_SELECTED = 412;
+
+  /**
+   * Indicates that no article has been selected.
+   */
+  public static final short NO_ARTICLE_SELECTED = 420;
+
+  /**
+   * Indicates that there is no next article in this newsgroup.
+   */
+  public static final short NO_NEXT_ARTICLE = 421;
+
+  /**
+   * Indicates that there is no previous article in this newsgroup.
+   */
+  public static final short NO_PREVIOUS_ARTICLE = 422;
+
+  /**
+   * Indicates that no article with the specified number exists in this
+   * newsgroup.
+   */
+  public static final short NO_SUCH_ARTICLE_NUMBER = 423;
+
+  /**
+   * Indicates that the specified article could not be found.
+   */
+  public static final short NO_SUCH_ARTICLE = 430;
+
+  /**
+   * Indicates that the server is not currently in a state to accept an
+   * article, but may become so at a later stage.
+   */
+  public static final short TRY_AGAIN_LATER = 431;
+
+  /**
+   * Indicates that the server does not want the specified article.
+   * The client should not send the article.
+   */
+  public static final short ARTICLE_NOT_WANTED = 435;
+
+  /**
+   * Indicates that transfer of the specified article failed.
+   * The client should try to send the article again later.
+   */
+  public static final short TRANSFER_FAILED = 436;
+
+  /**
+   * Indicates that the specified article was rejected.
+   * The client should not attempt to send the article again.
+   */
+  public static final short ARTICLE_REJECTED = 437;
+
+  /**
+   * Indicates that the server already has the specified article, and
+   * therefore doesn't want it sent using TAKETHIS.
+   */
+  public static final short ARTICLE_NOT_WANTED_VIA_TAKETHIS = 438;
+
+  /**
+   * Indicates that an article transferred by a TAKETHIS command failed.
+   */
+  public static final short ARTICLE_TRANSFER_FAILED = 439;
+
+  /**
+   * Indicates that posting is not allowed.
+   */
+  public static final short POSTING_NOT_ALLOWED = 440;
+
+  /**
+   * Indicates that posting of the article failed.
+   * The client may attempt to post the article again.
+   */
+  public static final short POSTING_FAILED = 441;
+
+  /**
+   * Indicates that authentication via the AUTHINFO SIMPLE strategy is
+   * required.
+   */
+  public static final short AUTHINFO_SIMPLE_REQUIRED = 450;
+
+  /**
+   * Indicates an authentication failure using AUTHINFO SIMPLE.
+   */
+  public static final short AUTHINFO_SIMPLE_DENIED = 452;
+
+  /**
+   * Indicates that the client does not have the appropriate authorization
+   * to transfer an article.
+   */
+  public static final short TRANSFER_PERMISSION_DENIED = 480;
+
+  /**
+   * Indicates that an XGTITLE listing follows.
+   */
+  public static final short XGTITLE_LIST_FOLLOWS = 481;
+
+  /**
+   * Indicates the the authentication information supplied was not accepted
+   * by the server.
+   */
+  public static final short AUTHINFO_REJECTED = 482;
+  
+  /**
+   * Indicates that the server requires encrypted connection or stronger
+   * authentication in order to perform request.
+   */
+  public static final short ENCRYPTION_OR_AUTH_REQUIRED = 483;
+
+  /**
+   * Indicates that the command sent by the client was not understood by the
+   * server.
+   */
+  public static final short COMMAND_NOT_RECOGNIZED = 500;
+
+  /**
+   * Indicates that the command sent by the client was not a valid NNTP
+   * command.
+   */
+  public static final short SYNTAX_ERROR = 501;
+
+  /**
+   * Indicates that access restrictions deny permission to execute the
+   * command sent by the client.
+   */
+  public static final short PERMISSION_DENIED = 502;
+
+  /**
+   * Indicates that the server was unable to perform the command due to an
+   * internal error.
+   */
+  public static final short INTERNAL_ERROR = 503;
+  
+  /**
+   * Indicates that the server is unable to proceed with TLS negotiation.
+   */
+  public static final short TLS_INIT_ERROR = 580;
+
+  // -- Client commands --
+
+  public static final String ARTICLE = "ARTICLE";
+  public static final String AUTHINFO_USER = "AUTHINFO USER";
+  public static final String AUTHINFO_PASS = "AUTHINFO PASS";
+  public static final String AUTHINFO_SIMPLE = "AUTHINFO SIMPLE";
+  public static final String AUTHINFO_GENERIC = "AUTHINFO GENERIC";
+  public static final String BODY = "BODY";
+  public static final String CHECK = "CHECK";
+  public static final String DATE = "DATE";
+  public static final String HEAD = "HEAD";
+  public static final String STAT = "STAT";
+  public static final String GROUP = "GROUP";
+  public static final String HELP = "HELP";
+  public static final String IHAVE = "IHAVE";
+  public static final String LAST = "LAST";
+  public static final String LIST = "LIST";
+  public static final String LIST_ACTIVE = "LIST ACTIVE";
+  public static final String LIST_ACTIVE_TIMES = "LIST ACTIVE.TIMES";
+  public static final String LIST_DISTRIBUTIONS = "LIST DISTRIBUTIONS";
+  public static final String LIST_DISTRIB_PATS = "LIST DISTRIB.PATS";
+  public static final String LIST_NEWSGROUPS = "LIST NEWSGROUPS";
+  public static final String LIST_OVERVIEW_FMT = "LIST OVERVIEW.FMT";
+  public static final String LIST_SUBSCRIPTIONS = "LIST SUBSCRIPTIONS";
+  public static final String LISTGROUP = "LISTGROUP";
+  public static final String MODE_READER = "MODE READER";
+  public static final String MODE_STREAM = "MODE STREAM";
+  public static final String NEWGROUPS = "NEWGROUPS";
+  public static final String NEWNEWS = "NEWNEWS";
+  public static final String NEXT = "NEXT";
+  public static final String POST = "POST";
+  public static final String QUIT = "QUIT";
+  public static final String SLAVE = "SLAVE";
+  public static final String STARTTLS = "STARTTLS";
+  public static final String TAKETHIS = "TAKETHIS";
+  public static final String XGTITLE = "XGTITLE";
+  public static final String XHDR = "XHDR";
+  public static final String XINDEX = "XINDEX";
+  public static final String XOVER = "XOVER";
+  public static final String XPAT = "XPAT";
+  public static final String XPATH = "XPATH";
+  public static final String XREPLIC = "XREPLIC";
+  public static final String XROVER = "XROVER";
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/NNTPException.java b/alien/src/gnu/inet/nntp/NNTPException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/NNTPException.java
@@ -0,0 +1,74 @@
+/*
+ * $Id: NNTPException.java,v 1.6 2005/08/25 12:32:03 dog Exp $
+ * Copyright (C) 2003 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+
+/**
+ * An NNTP exception.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ * @version $Revision: 1.6 $ $Date: 2005/08/25 12:32:03 $
+ */
+public class NNTPException extends IOException
+{
+
+  /*
+   * The response that caused this exception.
+   */
+  protected final StatusResponse response;
+
+  /**
+   * Constructor.
+   */
+  protected NNTPException (StatusResponse response)
+  {
+    super (response.getMessage ());
+    this.response = response;
+  }
+
+  /**
+   * Returns the response that caused this exception.
+   */
+  public StatusResponse getResponse ()
+  {
+    return response;
+  }
+
+}
diff --git a/alien/src/gnu/inet/nntp/Newsrc.java b/alien/src/gnu/inet/nntp/Newsrc.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/Newsrc.java
@@ -0,0 +1,83 @@
+/*
+ * Newsrc.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.util.Iterator;
+
+/**
+ * Interface for a .newsrc configuration.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public interface Newsrc
+{
+
+  /**
+   * Returns an iterator over the names of the subscribed newsgroups.
+   * Each item returned is a String.
+   */
+  public Iterator list();
+
+  /**
+   * Indicates whether a newsgroup is subscribed in this newsrc.
+   */
+  public boolean isSubscribed(String newsgroup);
+
+  /**
+   * Sets whether a newsgroup is subscribed in this newsrc.
+   */
+  public void setSubscribed(String newsgroup, boolean subs);
+
+  /**
+   * Indicates whether an article is marked as seen in the specified newsgroup.
+   */
+  public boolean isSeen(String newsgroup, int article);
+
+  /**
+   * Sets whether an article is marked as seen in the specified newsgroup.
+   */
+  public void setSeen(String newsgroup, int article, boolean seen);
+
+  /**
+   * Closes the configuration, potentially saving any changes.
+   */
+  public void close();
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/Overview.java b/alien/src/gnu/inet/nntp/Overview.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/Overview.java
@@ -0,0 +1,83 @@
+/*
+ * Overview.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * An overview entry.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class Overview
+{
+
+  int articleNumber;
+  private List headers;
+
+  Overview(int articleNumber)
+  {
+    this.articleNumber = articleNumber;
+    headers = new ArrayList(8);
+  }
+
+  void add(String header)
+  {
+    headers.add(header);
+  }
+
+  /**
+   * Returns the article number this overview entry is associated with.
+   */
+  public int getArticleNumber()
+  {
+    return articleNumber;
+  }
+
+  /**
+   * Returns the header at the specified index.
+   */
+  public String getHeader(int index)
+  {
+    return (String) headers.get(index);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/OverviewIterator.java b/alien/src/gnu/inet/nntp/OverviewIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/OverviewIterator.java
@@ -0,0 +1,120 @@
+/*
+ * OverviewIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over an overview listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class OverviewIterator
+  extends LineIterator
+{
+
+  OverviewIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+
+  /**
+   * Returns the next overview entry.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextOverview();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+
+  /**
+   * Returns the next overview entry.
+   */
+  public Overview nextOverview()
+    throws IOException
+  {
+    String line = nextLine();
+
+    try
+      {
+        // Parse line
+        int start = 0, end;
+        end = line.indexOf('\t', start);
+        int articleNumber = Integer.parseInt(line.substring(start, end));
+        start = end + 1;
+        Overview overview = new Overview(articleNumber);
+        end = line.indexOf('\t', start);
+        while(end > -1)
+          {
+            String entry = line.substring(start, end);
+            overview.add(entry);
+            start = end + 1;
+            end = line.indexOf('\t', start);
+          }
+        String entry = line.substring(start);
+        overview.add(entry);
+        
+        return overview;
+      }
+    catch (StringIndexOutOfBoundsException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid overview line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+    catch (NumberFormatException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid overview line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/Pair.java b/alien/src/gnu/inet/nntp/Pair.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/Pair.java
@@ -0,0 +1,75 @@
+/*
+ * Pair.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * A pair of strings associated with one another.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class Pair
+{
+
+  String key;
+  String value;
+
+  Pair(String key, String value)
+  {
+    this.key = key;
+    this.value = value;
+  }
+
+  /**
+   * The key.
+   */
+  public String getKey()
+  {
+    return key;
+  }
+
+  /**
+   * The value.
+   */
+  public String getValue()
+  {
+    return value;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/PairIterator.java b/alien/src/gnu/inet/nntp/PairIterator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/PairIterator.java
@@ -0,0 +1,103 @@
+/*
+ * PairIterator.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator over a pair listing.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class PairIterator
+  extends LineIterator
+{
+
+  PairIterator(NNTPConnection connection)
+  {
+    super(connection);
+  }
+
+  /**
+   * Returns the next pair.
+   */
+  public Object next()
+  {
+    try
+      {
+        return nextPair();
+      }
+    catch (IOException e)
+      {
+        throw new NoSuchElementException("I/O error: " + e.getMessage());
+      }
+  }
+  
+  /**
+   * Returns the next pair.
+   */
+  public Pair nextPair()
+    throws IOException
+  {
+    String line = nextLine();
+
+    try
+      {
+        // Parse line
+        int start = 0, end;
+        end = line.indexOf(' ', start);
+        String key = line.substring(start, end);
+        start = end + 1;
+        String value = line.substring(start);
+        
+        return new Pair(key, value);
+      }
+    catch (StringIndexOutOfBoundsException e)
+      {
+        ProtocolException e2 =
+          new ProtocolException("Invalid pair line: " + line);
+        e2.initCause(e);
+        throw e2;
+      }
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/PendingData.java b/alien/src/gnu/inet/nntp/PendingData.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/PendingData.java
@@ -0,0 +1,61 @@
+/*
+ * PendingData.java
+ * Copyright (C) 2003 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.IOException;
+
+/**
+ * An object representing data outside the simple request/response model of
+ * NNTP. The client needs to read until the end of such data before it
+ * can process further responses.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public interface PendingData
+{
+
+  /**
+   * Reads to the end of this data.
+   * @exception IOException if an I/O error occurred
+   */
+  public void readToEOF()
+    throws IOException;
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/PostStream.java b/alien/src/gnu/inet/nntp/PostStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/PostStream.java
@@ -0,0 +1,139 @@
+/*
+ * PostStream.java
+ * Copyright (C) 2002, 2003 The free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+import java.io.FilterOutputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+/**
+ * A stream to which article contents should be written.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public final class PostStream
+  extends FilterOutputStream
+{
+
+  private static final int LF = 0x0a;
+  private static final int DOT = 0x2e;
+
+  NNTPConnection connection;
+  boolean isTakethis;
+  byte last;
+  
+  PostStream(NNTPConnection connection, boolean isTakethis)
+  {
+    super(connection.out);
+    this.connection = connection;
+    this.isTakethis = isTakethis;
+  }
+  
+  public void write(int c)
+    throws IOException
+  {
+    super.write(c);
+    if (c == DOT && last == LF)
+      {
+        super.write(c); // double up initial dot
+      }
+    last = (byte) c;
+  }
+
+  public void write(byte[] bytes)
+    throws IOException
+  {
+    write(bytes, 0, bytes.length);
+  }
+
+  public void write(byte[] bytes, int pos, int len)
+    throws IOException
+  {
+    int end = pos + len;
+    for (int i = pos; i < end; i++)
+      {
+        byte c = bytes[i];
+        if (c == DOT && last == LF)
+          {
+            // Double dot
+            if (i > pos)
+              {
+                // Write everything up to i
+                int l = i - pos;
+                super.write(bytes, pos, l);
+                pos += l;
+                len -= l;
+              }
+            else
+              {
+                super.write(DOT);
+              }
+          }
+        last = c;
+      }
+    if (len > 0)
+      {
+        super.write(bytes, pos, len);
+      }
+  }
+  
+  /**
+   * Close the stream.
+   * This calls NNTPConnection.postComplete().
+   */
+  public void close()
+    throws IOException
+  {
+    if (last != 0x0d)
+      {
+        // Need to add LF
+        write(0x0d);
+      }
+    if (isTakethis)
+      {
+        connection.takethisComplete();
+      }
+    else
+      {
+        connection.postComplete();
+      }
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/nntp/Range.java b/alien/src/gnu/inet/nntp/Range.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/Range.java
@@ -0,0 +1,55 @@
+/*
+ * Range.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * A range of article numbers.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public abstract class Range
+{
+
+  /**
+   * Indicates whether this range contains the specfied article number.
+   */
+  public abstract boolean contains(int articleNumber);
+
+}
+
diff --git a/alien/src/gnu/inet/nntp/StatusResponse.java b/alien/src/gnu/inet/nntp/StatusResponse.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/StatusResponse.java
@@ -0,0 +1,87 @@
+/*
+ * StatusResponse.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.nntp;
+
+/**
+ * An NNTP status response.
+ * This represents the status code/message pair sent by the server in
+ * response to client commands.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class StatusResponse
+{
+
+  /*
+   * The status code.
+   */
+  protected short status;
+
+  /*
+   * The message.
+   */
+  protected String message;
+
+  /**
+   * Constructor.
+   */
+  protected StatusResponse(short status, String message)
+  {
+    this.status = status;
+    this.message = message;
+  }
+
+  /**
+   * Returns the status code associated with this response.
+   */
+  public short getStatus()
+  {
+    return status;
+  }
+  
+  /**
+   * Returns the message associated with this response.
+   */
+  public String getMessage()
+  {
+    return message;
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/nntp/package.html b/alien/src/gnu/inet/nntp/package.html
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/nntp/package.html
@@ -0,0 +1,14 @@
+<body>
+
+<p>
+This is an NNTP client, implementing all of RFC 977, and many of the RFC 2980
+NNTP extensions. Especially, there is support for the XOVER and XHDR commands
+and simple authentication.
+</p>
+
+<p>
+There is also a newsrc mechanism for storing newsgroup subscriptions and
+read articles.
+</p>
+
+</body>
diff --git a/alien/src/gnu/inet/util/.#CRLFInputStream.java.1.9 b/alien/src/gnu/inet/util/.#CRLFInputStream.java.1.9
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/.#CRLFInputStream.java.1.9
@@ -0,0 +1,179 @@
+/*
+ * CRLFInputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * An input stream that filters out CR/LF pairs into LFs.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class CRLFInputStream
+  extends InputStream
+{
+
+  /**
+   * The CR octet.
+   */
+  public static final int CR = 13;
+
+  /**
+   * The LF octet.
+   */
+  public static final int LF = 10;
+
+  /**
+   * The underlying input stream.
+   */
+  protected InputStream in;
+
+  private boolean doReset;
+
+  /**
+   * Constructs a CR/LF input stream connected to the specified input
+   * stream.
+   */
+  public CRLFInputStream(InputStream in)
+  {
+    this.in = in.markSupported() ? in : new BufferedInputStream(in);
+  }
+
+  /**
+   * Reads the next byte of data from this input stream.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read()
+    throws IOException
+  {
+    int c = in.read();
+    if (c == CR)
+      {
+        in.mark(1);
+        int d = in.read();
+        if (d == LF)
+          {
+            c = d;
+          }
+        else
+          {
+            in.reset();
+          }
+      }
+    return c;
+  }
+  
+  /**
+   * Reads up to b.length bytes of data from this input stream into
+   * an array of bytes.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b)
+    throws IOException
+  {
+    return read(b, 0, b.length);
+  }
+
+  /**
+   * Reads up to len bytes of data from this input stream into an
+   * array of bytes, starting at the specified offset.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b, int off, int len)
+    throws IOException
+  {
+    in.mark(len + 1);
+    int l = in.read(b, off, len);
+    if (l > 0)
+      {
+        int i = indexOfCRLF(b, off, l);
+        if (doReset)
+          {
+            in.reset();
+            if (i != -1)
+              {
+                l = in.read(b, off, (i + 1) - off); // read to CR
+                in.read(); // skip LF
+                b[i] = LF; // fix CR as LF
+              }
+            else
+              {
+                l = in.read(b, off, len); // CR(s) but no LF
+              }
+          }
+      }
+    return l;
+  }
+
+  private int indexOfCRLF(byte[] b, int off, int len)
+    throws IOException
+  {
+    doReset = false;
+    int lm1 = len - 1;
+    for (int i = off; i < len; i++)
+      {
+        if (b[i] == CR)
+          {
+            int d;
+            if (i == lm1)
+              {
+                d = in.read();
+                doReset = true;
+              }
+            else
+              {
+                d = b[i + 1];
+              }
+            if (d == LF)
+              {
+                doReset = true;
+                return i;
+              }
+          }
+      }
+    return -1;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/BASE64.java b/alien/src/gnu/inet/util/BASE64.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/BASE64.java
@@ -0,0 +1,187 @@
+/*
+ * BASE64.java
+ * Copyright (C) 2003, 2010 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+/**
+ * Encodes and decodes text according to the BASE64 encoding.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public final class BASE64
+{
+
+  private static final byte[] src = {
+    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+    0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54,
+    0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64,
+    0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e,
+    0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
+    0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+    0x38, 0x39, 0x2b, 0x2f
+  };
+
+  private static final byte[] dst;
+  static
+  {
+    dst = new byte[0x100];
+    for (int i = 0x0; i < 0xff; i++)
+      {
+        dst[i] = -1;
+      }
+    for (int i = 0; i < src.length; i++)
+      {
+        dst[src[i]] = (byte) i;
+      }
+  }
+
+  private BASE64()
+  {
+  }
+
+  /**
+   * Encode the specified byte array using the BASE64 algorithm.
+   *
+   * @param bs the source byte array
+   */
+  public static byte[] encode(byte[] bs)
+  {
+    int si = 0, ti = 0;         // source/target array indices
+    /* target byte array */
+    byte[] bt = new byte[((bs.length + 2 - ((bs.length + 2) % 3)) * 4) / 3];
+    for (; si < bs.length; si += 3)
+      {
+        int buflen = bs.length - si;
+        if (buflen == 1)
+          {
+            byte b = bs[si];
+            int i = 0;
+            boolean flag = false;
+            bt[ti++] = src[b >>> 2 & 0x3f];
+            bt[ti++] = src[(b << 4 & 0x30) + (i >>> 4 & 0xf)];
+          }
+        else if (buflen == 2)
+          {
+            byte b1 = bs[si], b2 = bs[si + 1];
+            int i = 0;
+            bt[ti++] = src[b1 >>> 2 & 0x3f];
+            bt[ti++] = src[(b1 << 4 & 0x30) + (b2 >>> 4 & 0xf)];
+            bt[ti++] = src[(b2 << 2 & 0x3c) + (i >>> 6 & 0x3)];
+          }
+        else
+          {
+            byte b1 = bs[si], b2 = bs[si + 1], b3 = bs[si + 2];
+            bt[ti++] = src[b1 >>> 2 & 0x3f];
+            bt[ti++] = src[(b1 << 4 & 0x30) + (b2 >>> 4 & 0xf)];
+            bt[ti++] = src[(b2 << 2 & 0x3c) + (b3 >>> 6 & 0x3)];
+            bt[ti++] = src[b3 & 0x3f];
+          }
+      }
+    while (ti < bt.length)
+      {
+        bt[ti++] = 0x3d;
+      }
+    return bt;
+  }
+
+  /**
+   * Decode the specified byte array using the BASE64 algorithm.
+   *
+   * @param bs the source byte array
+   */
+  public static byte[] decode(byte[] bs)
+  {
+    int padding = 0;
+    while (bs.length - padding > 0 && bs[bs.length - padding - 1] == 0x3d)
+      {
+        padding++;
+      }
+    int srclen = bs.length - padding; /* strip padding characters */
+    byte[] buffer = new byte[(bs.length / 4) * 3 - padding]; /* target array */
+    int buflen = 0;
+    int si = 0;
+    int len = srclen - si;
+    while (len > 0)
+      {
+        byte b0 = dst[bs[si++] & 0xff];
+        byte b2 = dst[bs[si++] & 0xff];
+        buffer[buflen++] = (byte) (b0 << 2 & 0xfc | b2 >>> 4 & 0x3);
+        if (len > 2)
+          {
+            b0 = b2;
+            b2 = dst[bs[si++] & 0xff];
+            buffer[buflen++] = (byte) (b0 << 4 & 0xf0 | b2 >>> 2 & 0xf);
+            if (len > 3)
+              {
+                b0 = b2;
+                b2 = dst[bs[si++] & 0xff];
+                buffer[buflen++] = (byte) (b0 << 6 & 0xc0 | b2 & 0x3f);
+              }
+          }
+        len = srclen - si;
+      }
+    return buffer;
+  }
+  
+  public static void main(String[] args)
+  {
+    boolean decode = false;
+    for (int i = 0; i < args.length; i++)
+      {
+        if (args[i].equals("-d"))
+          {
+            decode = true;
+          }
+        else
+          {
+            try
+              {
+                byte[] in = args[i].getBytes("US-ASCII");
+                byte[] out = decode ? decode(in) : encode(in);
+                System.out.println(args[i] + " = " +
+                                   new String(out, "US-ASCII"));
+              }
+            catch (java.io.UnsupportedEncodingException e)
+              {
+                e.printStackTrace(System.err);
+              }
+          }
+      }
+  }
+  
+}
diff --git a/alien/src/gnu/inet/util/CRLFInputStream.java b/alien/src/gnu/inet/util/CRLFInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/CRLFInputStream.java
@@ -0,0 +1,180 @@
+/*
+ * CRLFInputStream.java
+ * Copyright (C) 2002,2006 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * An input stream that filters out CR/LF pairs into LFs.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class CRLFInputStream
+  extends InputStream
+{
+
+  /**
+   * The CR octet.
+   */
+  public static final int CR = 13;
+
+  /**
+   * The LF octet.
+   */
+  public static final int LF = 10;
+
+  /**
+   * The underlying input stream.
+   */
+  protected InputStream in;
+
+  private boolean doReset;
+
+  /**
+   * Constructs a CR/LF input stream connected to the specified input
+   * stream.
+   */
+  public CRLFInputStream(InputStream in)
+  {
+    this.in = in.markSupported() ? in : new BufferedInputStream(in);
+  }
+
+  /**
+   * Reads the next byte of data from this input stream.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read()
+    throws IOException
+  {
+    int c = in.read();
+    if (c == CR)
+      {
+        in.mark(1);
+        int d = in.read();
+        if (d == LF)
+          {
+            c = d;
+          }
+        else
+          {
+            in.reset();
+          }
+      }
+    return c;
+  }
+  
+  /**
+   * Reads up to b.length bytes of data from this input stream into
+   * an array of bytes.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b)
+    throws IOException
+  {
+    return read(b, 0, b.length);
+  }
+
+  /**
+   * Reads up to len bytes of data from this input stream into an
+   * array of bytes, starting at the specified offset.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b, int off, int len)
+    throws IOException
+  {
+    in.mark(len + 1);
+    int l = in.read(b, off, len);
+    if (l > 0)
+      {
+        int i = indexOfCRLF(b, off, l);
+        if (doReset)
+          {
+            in.reset();
+            if (i != -1)
+              {
+                l = in.read(b, off, (i + 1) - off); // read to CR
+                in.read(); // skip LF
+                b[i] = LF; // fix CR as LF
+              }
+            else
+              {
+                l = in.read(b, off, len); // CR(s) but no LF
+              }
+          }
+      }
+    return l;
+  }
+
+  private int indexOfCRLF(byte[] b, int off, int len)
+    throws IOException
+  {
+    doReset = false;
+    int end = off + len;
+    int em1 = end - 1;
+    for (int i = off; i < end; i++)
+      {
+        if (b[i] == CR)
+          {
+            int d;
+            if (i == em1)
+              {
+                d = in.read();
+                doReset = true;
+              }
+            else
+              {
+                d = b[i + 1];
+              }
+            if (d == LF)
+              {
+                doReset = true;
+                return i;
+              }
+          }
+      }
+    return -1;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/CRLFOutputStream.java b/alien/src/gnu/inet/util/CRLFOutputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/CRLFOutputStream.java
@@ -0,0 +1,185 @@
+/*
+ * CRLFOutputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/** 
+ * An output stream that filters LFs into CR/LF pairs.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class CRLFOutputStream
+  extends FilterOutputStream
+{
+
+  static final String US_ASCII = "US-ASCII";
+
+  /**
+   * The CR octet.
+   */
+  public static final int CR = 13;
+
+  /**
+   * The LF octet.
+   */
+  public static final int LF = 10;
+
+  /**
+   * The CR/LF pair.
+   */
+  public static final byte[] CRLF = { CR, LF };
+
+  /**
+   * The last byte read.
+   */
+  protected int last;
+
+  /**
+   * Constructs a CR/LF output stream connected to the specified output stream.
+   */
+  public CRLFOutputStream(OutputStream out)
+  {
+    super(out);
+    last = -1;
+  }
+
+  /**
+   * Writes a character to the underlying stream.
+   * @exception IOException if an I/O error occurred
+   */
+  public void write(int ch) throws IOException
+  {
+    if (ch == CR)
+      {
+        out.write(CRLF);
+      }
+    else if (ch == LF)
+      {
+        if (last != CR)
+          {
+            out.write(CRLF);
+          }
+      }
+    else
+      {
+        out.write(ch);
+      }
+    last = ch;
+  }
+  
+  /**
+   * Writes a byte array to the underlying stream.
+   * @exception IOException if an I/O error occurred
+   */
+  public void write(byte[] b)
+    throws IOException
+  {
+    write(b, 0, b.length);
+  }
+
+  /**
+   * Writes a portion of a byte array to the underlying stream.
+   * @exception IOException if an I/O error occurred
+   */
+  public void write(byte[] b, int off, int len)
+    throws IOException
+  {
+    int d = off;
+    len += off;
+    for (int i = off; i < len; i++)
+      {
+        switch (b[i])
+          {
+          case CR:
+            out.write (b, d, i - d);
+            out.write (CRLF, 0, 2);
+            d = i + 1;
+            break;
+          case LF:
+            if (last != CR)
+              {
+                out.write (b, d, i - d);
+                out.write (CRLF, 0, 2);
+              }
+            d = i + 1;
+            break;
+          }
+        last = b[i];
+      }
+    if (len - d > 0)
+      {
+        out.write (b, d, len - d);
+      }
+  }
+  
+  /**
+   * Writes the specified ASCII string to the underlying stream.
+   * @exception IOException if an I/O error occurred
+   */
+  public void write(String text)
+    throws IOException
+  {
+    try
+      {
+        byte[] bytes = text.getBytes(US_ASCII);
+        write(bytes, 0, bytes.length);
+        }
+    catch (UnsupportedEncodingException e)
+      {
+        throw new IOException("The US-ASCII encoding is not supported " +
+                              "on this system");
+      }
+  }
+
+  /**
+   * Writes a newline to the underlying stream.
+   * @exception IOException if an I/O error occurred
+   */
+  public void writeln()
+    throws IOException
+  {
+    out.write(CRLF, 0, 2);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/EmptyX509TrustManager.java b/alien/src/gnu/inet/util/EmptyX509TrustManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/EmptyX509TrustManager.java
@@ -0,0 +1,71 @@
+/*
+ * EmptyX509TrustManager.java
+ * Copyright (C) 2004 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ * 
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Empty implementation of an X509 trust manager.
+ * This implementation does not check any certificates in the chain.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class EmptyX509TrustManager
+  implements X509TrustManager
+{
+
+  public void checkClientTrusted(X509Certificate[] chain, String authType)
+    throws CertificateException
+  {
+  }
+
+  public void checkServerTrusted(X509Certificate[] chain, String authType)
+    throws CertificateException
+  {
+  }
+
+  public X509Certificate[] getAcceptedIssuers()
+  {
+    return new X509Certificate[0];
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/GetLocalHostAction.java b/alien/src/gnu/inet/util/GetLocalHostAction.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/GetLocalHostAction.java
@@ -0,0 +1,67 @@
+/*
+ * GetLocalHostAction.java
+ * Copyright (C) 2003 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ * 
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.PrivilegedAction;
+
+/**
+ * Privileged action to retrieve the local host InetAddress.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class GetLocalHostAction
+  implements PrivilegedAction
+{
+
+  public Object run()
+  {
+    try
+      {
+        return InetAddress.getLocalHost();
+      }
+    catch (UnknownHostException e)
+      {
+        return null;
+      }
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/GetSystemPropertyAction.java b/alien/src/gnu/inet/util/GetSystemPropertyAction.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/GetSystemPropertyAction.java
@@ -0,0 +1,69 @@
+/*
+ * GetSystemPropertyAction.java
+ * Copyright (C) 2004 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ * 
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.security.PrivilegedAction;
+
+/**
+ * Privileged action for retrieving system properties.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class GetSystemPropertyAction
+  implements PrivilegedAction
+{
+
+  final String name;
+
+  /**
+   * Constructor.
+   * @param name the the name of the system property to retrieve
+   */
+  public GetSystemPropertyAction(String name)
+  {
+    this.name = name;
+  }
+
+  public Object run()
+  {
+    return System.getProperty(name);
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/LaconicFormatter.java b/alien/src/gnu/inet/util/LaconicFormatter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/LaconicFormatter.java
@@ -0,0 +1,47 @@
+/*
+ * $Id: LaconicFormatter.java,v 1.1 2007/06/01 17:03:26 dog Exp $
+ * Copyright (C) 2007 The Free Software Foundation
+ * 
+ * This file is part of GNU JavaMail, a library.
+ * 
+ * GNU JavaMail is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ *(at your option) any later version.
+ * 
+ * GNU JavaMail 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ * 
+ * As a special exception, if you link this library with other files to
+ * produce an executable, this library does not by itself cause the
+ * resulting executable to be covered by the GNU General Public License.
+ * This exception does not however invalidate any other reasons why the
+ * executable file might be covered by the GNU General Public License.
+ */
+
+package gnu.inet.util;
+
+import java.util.logging.Formatter;
+import java.util.logging.LogRecord;
+
+/**
+ * A logging formatter that outputs only the log message.
+ *
+ * @author <a href='dog@gnu.org'>Chris Burdess</a>
+ */
+public class LaconicFormatter
+  extends Formatter
+{
+
+  public String format(LogRecord record)
+  {
+    return record.getMessage() + System.getProperty("line.separator");
+  }
+  
+}
diff --git a/alien/src/gnu/inet/util/LineInputStream.java b/alien/src/gnu/inet/util/LineInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/LineInputStream.java
@@ -0,0 +1,222 @@
+/*
+ * LineInputStream.java
+ * Copyright (C) 2002,2006 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * An input stream that can read lines of input.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class LineInputStream
+  extends InputStream
+{
+
+  /**
+   * The underlying input stream.
+   */
+  protected InputStream in;
+
+  /*
+   * Line buffer.
+   */
+  private ByteArrayOutputStream buf;
+
+  /*
+   * Encoding to use when translating bytes to characters.
+   */
+  private String encoding;
+
+  /*
+   * End-of-stream flag.
+   */
+  private boolean eof;
+
+  /**
+   * Whether we can use block reads.
+   */
+  private final boolean blockReads;
+
+  /**
+   * Constructor using the US-ASCII character encoding.
+   * @param in the underlying input stream
+   */
+  public LineInputStream(InputStream in)
+  {
+    this(in, "US-ASCII");
+  }
+
+  /**
+   * Constructor.
+   * @param in the underlying input stream
+   * @param encoding the character encoding to use
+   */
+  public LineInputStream(InputStream in, String encoding)
+  {
+    this.in = in;
+    buf = new ByteArrayOutputStream();
+    this.encoding = encoding;
+    eof = false;
+    blockReads = in.markSupported();
+  }
+
+  public int read()
+    throws IOException
+  {
+    return in.read();
+  }
+
+  public int read(byte[] buf)
+    throws IOException
+  {
+    return in.read(buf);
+  }
+
+  public int read(byte[] buf, int off, int len)
+    throws IOException
+  {
+    return in.read(buf, off, len);
+  }
+
+  /**
+   * Read a line of input.
+   */
+  public String readLine()
+    throws IOException
+  {
+    if (eof)
+      {
+        return null;
+      }
+    do
+      {
+        if (blockReads)
+          {
+            // Use mark and reset to read chunks of bytes
+            final int MIN_LENGTH = 1024;
+            int len, pos;
+            
+            len = in.available();
+            len = (len < MIN_LENGTH) ? MIN_LENGTH : len;
+            byte[] b = new byte[len];
+            in.mark(len);
+            // Read into buffer b
+            len = in.read(b, 0, len);
+            // Handle EOF
+            if (len == -1)
+              {
+                eof = true;
+                if (buf.size() == 0)
+                  {
+                    return null;
+                  }
+                else
+                  {
+                    // We don't care about resetting buf
+                    return buf.toString(encoding);
+                  }
+              }
+            // Get index of LF in b
+            pos = indexOf(b, len, (byte) 0x0a);
+            if (pos != -1)
+              {
+                // Write pos bytes to buf
+                buf.write(b, 0, pos);
+                // Reset stream, and read pos + 1 bytes
+                in.reset();
+                pos += 1;
+                while (pos > 0)
+                  {
+                    len = in.read(b, 0, pos);
+                    pos = (len == -1) ? -1 : pos - len;
+                  }
+                // Return line
+                String ret = buf.toString(encoding);
+                buf.reset();
+                return ret;
+              }
+            else
+              {
+                // Append everything to buf and fall through to re-read.
+                buf.write(b, 0, len);
+              }
+          }
+        else
+          {
+            // We must use character reads in order not to read too much
+            // from the underlying stream.
+            int c = in.read();
+            switch (c)
+              {
+              case -1:
+                eof = true;
+                if (buf.size() == 0)
+                  {
+                    return null;
+                  }
+                // Fall through and return contents of buffer.
+              case 0x0a:                // LF
+                String ret = buf.toString(encoding);
+                buf.reset();
+                return ret;
+              default:
+                buf.write(c);
+              }
+          }
+      }
+    while (true);
+  }
+
+  private int indexOf(byte[] b, int len, byte c)
+  {
+    for (int pos = 0; pos < len; pos++)
+      {
+        if (b[pos] == c)
+          {
+            return pos;
+          }
+      }
+    return -1;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/MessageInputStream.java b/alien/src/gnu/inet/util/MessageInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/MessageInputStream.java
@@ -0,0 +1,204 @@
+/*
+ * MessageInputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * A utility class for feeding message contents to messages.
+ * This stream returns -1 from <code>read</code> when the stream termination
+ * sequence LF,END,LF is read from the underlying stream.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class MessageInputStream
+  extends FilterInputStream
+{
+
+  /**
+   * The stream termination octet ('.').
+   */
+  public static final int END = 46;
+
+  /**
+   * The line termination octet ('\n').
+   */
+  public static final int LF = 10;
+
+  protected boolean eof;
+
+  protected int buf1 = -1;
+  protected int buf2 = -1;
+
+  protected int markBuf1;
+  protected int markBuf2;
+
+  /**
+   * Constructs a message input stream connected to the specified input stream.
+   */
+  public MessageInputStream(InputStream in)
+  {
+    super(in);
+    eof = false;
+  }
+
+  /**
+   * Reads the next byte of data from this message input stream.
+   * Returns -1 if the end of the message stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read()
+    throws IOException
+  {
+    if (eof)
+      {
+        return -1;
+      }
+    int c;
+    if (buf1 != -1)
+      {
+        c = buf1;
+        buf1 = buf2;
+        buf2 = -1;
+      }
+    else
+      {
+        c = super.read();
+      }
+    if (c == LF)
+      {
+        if (buf1 == -1)
+          {
+            buf1 = super.read();
+            if (buf1 == END)
+              {
+                buf2 = super.read();
+                if (buf2 == LF)
+                  {
+                    eof = true;
+                    // Allow the final LF to be read
+                  }
+              }
+          }
+        else if (buf1 == END)
+          {
+            if (buf2 == -1)
+              {
+                buf2 = super.read();
+                if (buf2 == LF)
+                  {
+                    eof = true;
+                  }
+              }
+            else if (buf2 == LF)
+              {
+                eof = true;
+              }
+          }
+      }
+    return c;
+  }
+  
+  /**
+   * Reads up to b.length bytes of data from this input stream into
+   * an array of bytes.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b)
+    throws IOException
+  {
+    return read(b, 0, b.length);
+  }
+
+  /**
+   * Reads up to len bytes of data from this input stream into an
+   * array of bytes, starting at the specified offset.
+   * Returns -1 if the end of the stream has been reached.
+   * @exception IOException if an I/O error occurs
+   */
+  public int read(byte[] b, int off, int len)
+    throws IOException
+  {
+    if (eof)
+      {
+        return -1;
+      }
+    int c, end = off + len;
+    for (int i = off; i < end; i++)
+      {
+        c = read();
+        if (c == -1)
+          {
+            len = i - off;
+            break;
+          }
+        else
+          {
+            b[i] = (byte) c;
+          }
+      }
+    return len;
+  }
+  
+  public boolean markSupported()
+  {
+    return in.markSupported();
+  }
+
+  public void mark(int readlimit)
+  {
+    in.mark(readlimit);
+    markBuf1 = buf1;
+    markBuf2 = buf2;
+  }
+
+  public void reset()
+    throws IOException
+  {
+    in.reset();
+    buf1 = markBuf1;
+    buf2 = markBuf2;
+    eof = false;
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/MessageOutputStream.java b/alien/src/gnu/inet/util/MessageOutputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/MessageOutputStream.java
@@ -0,0 +1,125 @@
+/*
+ * MessageOutputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream that escapes any dots on a line by themself with
+ * another dot, for the purposes of sending messages to SMTP and NNTP
+ * servers.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class MessageOutputStream
+  extends FilterOutputStream
+{
+
+  /**
+   * The stream termination octet.
+   */
+  public static final int END = 46;
+
+  /**
+   * The line termination octet.
+   */
+  public static final int LF = 10;
+
+  int[] last = { LF, LF };      // the last character written to the stream
+
+  /**
+   * Constructs a message output stream connected to the specified output
+   * stream.
+   * @param out the target output stream
+   */
+  public MessageOutputStream(OutputStream out)
+  {
+    super(out);
+  }
+
+  /**
+   * Character write.
+   */
+  public void write(int c)
+    throws IOException
+  {
+    if (last[0] == LF && last[1] == END && c == LF)
+      {
+        out.write (END);
+      }
+    out.write(c);
+    last[0] = last[1];
+    last[1] = c;
+  }
+  
+  public void write(byte[] bytes)
+    throws IOException
+  {
+    write(bytes, 0, bytes.length);
+  }
+
+  /**
+   * Block write.
+   */
+  public void write(byte[] bytes, int off, int len)
+    throws IOException
+  {
+    for (int i = 0; i < len; i++)
+      {
+        int c = (int) bytes[off + i];
+        if (last[0] == LF && last[1] == END && c == LF)
+          {
+            byte[] b2 = new byte[bytes.length + 1];
+            System.arraycopy(bytes, off, b2, off, i);
+            b2[off + i] = END;
+            System.arraycopy(bytes, off + i, b2, off + i + 1, len - i);
+            bytes = b2;
+            i++;
+            len++;
+          }
+        last[0] = last[1];
+        last[1] = c;
+      }
+    out.write(bytes, off, len);
+  }
+
+}
+
diff --git a/alien/src/gnu/inet/util/SaslCallbackHandler.java b/alien/src/gnu/inet/util/SaslCallbackHandler.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslCallbackHandler.java
@@ -0,0 +1,105 @@
+/*
+ * SaslCallbackHandler.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+/**
+ * A callback handler that can manage username and password callbacks.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public final class SaslCallbackHandler
+  implements CallbackHandler
+{
+
+  /*
+   * The username.
+   */
+  private final String username;
+
+  /*
+   * The password.
+   */
+  private final String password;
+
+  /**
+   * Constructor.
+   * @param username the value to respond to Name callbacks with
+   * @param password the value to respond to Password callbacks with
+   */
+  public SaslCallbackHandler(String username, String password)
+  {
+    this.username = username;
+    this.password = password;
+  }
+
+  /**
+   * Handle callbacks.
+   */
+  public void handle(Callback[] callbacks)
+    throws IOException, UnsupportedCallbackException
+  {
+    for (int i = 0; i < callbacks.length; i++)
+      {
+        if (callbacks[i] instanceof NameCallback)
+          {
+            NameCallback nc = (NameCallback) callbacks[i];
+            nc.setName(username);
+          }
+        else if (callbacks[i] instanceof PasswordCallback)
+          {
+            PasswordCallback pc = (PasswordCallback) callbacks[i];
+            pc.setPassword(password.toCharArray ());
+          }
+        else
+          {
+            throw new UnsupportedCallbackException(callbacks[i]);
+          }
+      }
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/SaslCramMD5.java b/alien/src/gnu/inet/util/SaslCramMD5.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslCramMD5.java
@@ -0,0 +1,179 @@
+/*
+ * SaslCramMD5.java
+ * Copyright (C) 2004 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+/**
+ * SASL mechanism for CRAM-MD5.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class SaslCramMD5
+  implements SaslClient
+{
+
+  private String username;
+  private String password;
+  private boolean complete;
+
+  public SaslCramMD5(String username, String password)
+  {
+    this.username = username;
+    this.password = password;
+  }
+
+  public String getMechanismName()
+  {
+    return "CRAM-MD5";
+  }
+
+  public boolean hasInitialResponse()
+  {
+    return false;
+  }
+
+  public byte[] evaluateChallenge(byte[] challenge)
+    throws SaslException
+  {
+    try
+      {
+        byte[] s = password.getBytes("US-ASCII");
+        byte[] digest = hmac_md5(s, challenge);
+        byte[] r0 = username.getBytes("US-ASCII");
+        byte[] r1 = new byte[r0.length + digest.length + 1];
+        System.arraycopy(r0, 0, r1, 0, r0.length); // add username
+        r1[r0.length] = 0x20; // SPACE
+        System.arraycopy(digest, 0, r1, r0.length+1, digest.length);
+        complete = true;
+        return r1;
+      }
+    catch (UnsupportedEncodingException e)
+      {
+        String msg = "Username or password contains non-ASCII characters";
+        throw new SaslException(msg, e);
+      }
+    catch (NoSuchAlgorithmException e)
+      {
+        String msg = "MD5 algorithm not available";
+        throw new SaslException(msg, e);
+      }
+  }
+
+  public boolean isComplete()
+  {
+    return complete;
+  }
+
+  public byte[] unwrap(byte[] incoming, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(incoming, off, ret, 0, len);
+    return ret;
+  }
+
+  public byte[] wrap(byte[] outgoing, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(outgoing, off, ret, 0, len);
+    return ret;
+  }
+
+  public Object getNegotiatedProperty(String name)
+  {
+    return null;
+  }
+
+  public void dispose()
+  {
+  }
+
+  /**
+   * Computes a CRAM digest using the HMAC algorithm:
+   * <pre>
+   * MD5(key XOR opad, MD5(key XOR ipad, text))
+   * </pre>.
+   * <code>secret</code> is null-padded to a length of 64 bytes.
+   * If the shared secret is longer than 64 bytes, the MD5 digest of the
+   * shared secret is used as a 16 byte input to the keyed MD5 calculation.
+   * See RFC 2104 for details.
+   */
+  private static byte[] hmac_md5(byte[] key, byte[] text)
+    throws NoSuchAlgorithmException
+  {
+    byte[] k_ipad = new byte[64];
+    byte[] k_opad = new byte[64];
+    byte[] digest;
+    MessageDigest md5 = MessageDigest.getInstance("MD5");
+    // if key is longer than 64 bytes reset it to key=MD5(key)
+    if (key.length>64)
+      {
+        md5.update(key);
+        key = md5.digest();
+      }
+    // start out by storing key in pads
+    System.arraycopy(key, 0, k_ipad, 0, key.length);
+    System.arraycopy(key, 0, k_opad, 0, key.length);
+    // XOR key with ipad and opad values
+    for (int i=0; i<64; i++)
+      {
+        k_ipad[i] ^= 0x36;
+        k_opad[i] ^= 0x5c;
+      }
+    // perform inner MD5
+    md5.reset();
+    md5.update(k_ipad);
+    md5.update(text);
+    digest = md5.digest();
+    // perform outer MD5
+    md5.reset();
+    md5.update(k_opad);
+    md5.update(digest);
+    digest = md5.digest();
+    return digest;
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/SaslInputStream.java b/alien/src/gnu/inet/util/SaslInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslInputStream.java
@@ -0,0 +1,179 @@
+/*
+ * SaslInputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import javax.security.sasl.SaslClient;
+
+/**
+ * A filter input stream that decodes all its received input using a SASL
+ * client.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class SaslInputStream
+  extends FilterInputStream
+{
+
+  /*
+   * The SASL client.
+   */
+  private final SaslClient sasl;
+
+  /*
+   * Overflow buffer.
+   */
+  private byte[] buf;
+
+  /*
+   * Offset in overflow buffer.
+   */
+  private int pos;
+
+  /**
+   * Constructor.
+   * @param sasl the SASL client
+   * @param in the underlying input stream
+   */
+  public SaslInputStream(SaslClient sasl, InputStream in)
+  {
+    super(in);
+    this.sasl = sasl;
+  }
+
+  /**
+   * Reads a single character.
+   */
+  public int read()
+    throws IOException
+  {
+    if (buf != null)
+      {
+        // Return next characer in buffer
+        int c = (int) buf[pos++];
+        if (pos == buf.length)
+          {
+            buf = null;
+          }
+        return c;
+      }
+    int c = super.read();
+    if (c == -1)
+      {
+        return c;
+      }
+    byte[] bytes = new byte[1];
+    byte[] unwrapped = sasl.unwrap(bytes, 0, 1);
+    // FIXME if we get 0 bytes, we have a problem
+    c = (int) unwrapped[0];
+    if (unwrapped.length > 1)
+      {
+        // Store in overflow buffer
+        int l = unwrapped.length - 1;
+        buf = new byte[l];
+        System.arraycopy(unwrapped, 1, buf, 0, l);
+        pos = 0;
+      }
+    return c;
+  }
+  
+  public int read(byte[] bytes)
+    throws IOException
+  {
+    return read(bytes, 0, bytes.length);
+  }
+
+  /**
+   * Block read.
+   */
+  public int read(byte[] bytes, int off, int len)
+    throws IOException
+  {
+    if (buf != null)
+      {
+        // Return bytes from buffer
+        int l = buf.length;
+        if (l - pos <= len)
+          {
+            System.arraycopy(buf, pos, bytes, off, l);
+            buf = null;
+            return l;
+          }
+        else
+          {
+            System.arraycopy(buf, pos, bytes, off, len);
+            pos += len;
+            return len;
+          }
+      }
+    int l = super.read(bytes, off, len);
+    if (l == -1)
+      {
+        return l;
+      }
+    byte[] unwrapped = sasl.unwrap(bytes, off, l);
+    int l2 = unwrapped.length;
+    if (l2 > len)
+      {
+        // Store excess bytes in buffer
+        int d = l2 - len;
+        buf = new byte[d];
+        System.arraycopy(unwrapped, 0, bytes, off, len);
+        System.arraycopy(unwrapped, len, buf, 0, d);
+        pos = 0;
+        return len;
+      }
+    else
+      {
+        System.arraycopy(unwrapped, 0, bytes, off, l2);
+        // Zero bytes from l2..l to ensure none of the original
+        // bytes received can be read by the caller
+        for (int i = l2; i < l; i++)
+          {
+            bytes[off + l2] = 0;
+          }
+        return l2;
+      }
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/SaslLogin.java b/alien/src/gnu/inet/util/SaslLogin.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslLogin.java
@@ -0,0 +1,134 @@
+/*
+ * SaslLogin.java
+ * Copyright (C) 2004 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.UnsupportedEncodingException;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+/**
+ * SASL mechanism for LOGIN.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class SaslLogin
+  implements SaslClient
+{
+
+  private static final int STATE_USERNAME = 0;
+  private static final int STATE_PASSWORD = 1;
+  private static final int STATE_COMPLETE = 2;
+
+  private String username;
+  private String password;
+  private int state;
+
+  public SaslLogin(String username, String password)
+  {
+    this.username = username;
+    this.password = password;
+    state = STATE_USERNAME;
+  }
+
+  public String getMechanismName()
+  {
+    return "LOGIN";
+  }
+
+  public boolean hasInitialResponse()
+  {
+    return false;
+  }
+
+  public byte[] evaluateChallenge(byte[] challenge)
+    throws SaslException
+  {
+    try
+      {
+        switch (state)
+          {
+          case STATE_USERNAME:
+            state = STATE_PASSWORD;
+            return username.getBytes("UTF-8");
+          case STATE_PASSWORD:
+            state = STATE_COMPLETE;
+            return password.getBytes("UTF-8");
+          default:
+            return new byte[0];
+          }
+      }
+    catch (UnsupportedEncodingException e)
+      {
+        String msg = "The UTF-8 character set is not supported by the VM";
+        throw new SaslException(msg, e);
+      }
+  }
+
+  public boolean isComplete()
+  {
+    return (state == STATE_COMPLETE);
+  }
+
+  public byte[] unwrap(byte[] incoming, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(incoming, off, ret, 0, len);
+    return ret;
+  }
+
+  public byte[] wrap(byte[] outgoing, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(outgoing, off, ret, 0, len);
+    return ret;
+  }
+
+  public Object getNegotiatedProperty(String name)
+  {
+    return null;
+  }
+
+  public void dispose()
+  {
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/SaslOutputStream.java b/alien/src/gnu/inet/util/SaslOutputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslOutputStream.java
@@ -0,0 +1,101 @@
+/*
+ * SaslOutputStream.java
+ * Copyright (C) 2002 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.security.sasl.SaslClient;
+
+/**
+ * A filter output stream that encodes data written to it using a SASL
+ * client.
+ *
+ * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
+ */
+public class SaslOutputStream
+  extends FilterOutputStream
+{
+
+  /*
+   * The SASL client used for encoding data.
+   */
+  private final SaslClient sasl;
+
+  /**
+   * Constructor.
+   * @param sasl the SASL client
+   * @param out the target output stream
+   */
+  public SaslOutputStream(SaslClient sasl, OutputStream out)
+  {
+    super(out);
+    this.sasl = sasl;
+  }
+
+  /**
+   * Character write.
+   */
+  public void write(int c)
+    throws IOException
+  {
+    byte[] bytes = new byte[1];
+    bytes[0] = (byte) c;
+    write(bytes, 0, 1);
+  }
+
+  public void write(byte[] bytes)
+    throws IOException
+  {
+    write(bytes, 0, bytes.length);
+  }
+  
+  /**
+   * Block write.
+   */
+  public void write(byte[] bytes, int off, int len)
+    throws IOException
+  {
+    byte[] wrapped = sasl.wrap(bytes, off, len);
+    super.write(wrapped, 0, wrapped.length);
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/SaslPlain.java b/alien/src/gnu/inet/util/SaslPlain.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/SaslPlain.java
@@ -0,0 +1,126 @@
+/*
+ * SaslPlain.java
+ * Copyright (C) 2004 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.io.UnsupportedEncodingException;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+/**
+ * SASL mechanism for PLAIN.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class SaslPlain
+  implements SaslClient
+{
+
+  private String username;
+  private String password;
+  private boolean complete;
+
+  public SaslPlain(String username, String password)
+  {
+    this.username = username;
+    this.password = password;
+  }
+
+  public String getMechanismName()
+  {
+    return "PLAIN";
+  }
+
+  public boolean hasInitialResponse()
+  {
+    return true;
+  }
+
+  public byte[] evaluateChallenge(byte[] challenge)
+    throws SaslException
+  {
+    try
+      {
+        byte[] a = username.getBytes("UTF-8");
+        byte[] b = password.getBytes("UTF-8");
+        byte[] c = new byte[(a.length * 2) + b.length + 2];
+        System.arraycopy(a, 0, c, 0, a.length);
+        System.arraycopy(a, 0, c, a.length + 1, a.length);
+        System.arraycopy(b, 0, c, (a.length * 2) + 2, b.length);
+        complete = true;
+        return c;
+      }
+    catch (UnsupportedEncodingException e)
+      {
+        String msg = "Username or password contains illegal UTF-8";
+        throw new SaslException(msg, e);
+      }
+  }
+
+  public boolean isComplete()
+  {
+    return complete;
+  }
+
+  public byte[] unwrap(byte[] incoming, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(incoming, off, ret, 0, len);
+    return ret;
+  }
+
+  public byte[] wrap(byte[] outgoing, int off, int len)
+    throws SaslException
+  {
+    byte[] ret = new byte[len - off];
+    System.arraycopy(outgoing, off, ret, 0, len);
+    return ret;
+  }
+
+  public Object getNegotiatedProperty(String name)
+  {
+    return null;
+  }
+
+  public void dispose()
+  {
+  }
+  
+}
+
diff --git a/alien/src/gnu/inet/util/TraceLevel.java b/alien/src/gnu/inet/util/TraceLevel.java
new file mode 100644
--- /dev/null
+++ b/alien/src/gnu/inet/util/TraceLevel.java
@@ -0,0 +1,67 @@
+/*
+ * TraceLevel.java
+ * Copyright (C) 2005 The Free Software Foundation
+ * 
+ * This file is part of GNU inetlib, a library.
+ * 
+ * GNU inetlib is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * GNU inetlib 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU 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
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library.  Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obliged to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package gnu.inet.util;
+
+import java.util.logging.Level;
+
+/**
+ * A logging level used for network trace information.
+ *
+ * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
+ */
+public class TraceLevel
+  extends Level
+{
+
+  /**
+   * The integer value for trace logging.
+   */
+  public static final int TRACE = 450;
+
+  /**
+   * Constructor.
+   * @param name the name of this level, normally the network protocol
+   */
+  public TraceLevel(String name)
+  {
+    super(name, TRACE);
+  }
+  
+}
+
diff --git a/alien/src/net/freeutils/httpserver/HTTPServer.java b/alien/src/net/freeutils/httpserver/HTTPServer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/freeutils/httpserver/HTTPServer.java
@@ -0,0 +1,2522 @@
+// THIS FILE HAS BEEN MODIFED SO THAT THE SERVER ONLY LISTENS ON LOCALHOST!
+/*
+ *  (c) copyright 2005-2009 Amichai Rothman
+ *
+ *  This file is part of the Java Lightweight HTTP Server.
+ *
+ *  The Java Lightweight HTTP Server is free software; you can redistribute
+ *  it and/or modify it under the terms of the GNU General Public License
+ *  as published by the Free Software Foundation; either version 2 of the
+ *  License, or at your option) any later version.
+ *
+ *  The Java Lightweight HTTP Server 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package net.freeutils.httpserver;
+
+import java.net.InetAddress;
+
+import java.io.*;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.*;
+import java.net.*;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * The {@code HTTPServer} class implements a light-weight HTTP server.
+ * 
+ * This server is 'conditionally compliant' with RFC 2616 ("Hypertext
+ * Transfer Protocol -- HTTP/1.1"), which means it supports all functionality
+ * required by the RFC, as well as some of the optional functionality.
+ * Among the features are virtual hosts, partial content (i.e. download
+ * continuation), file-based serving, automatic directory index generation,
+ * GET/HEAD/POST/OPTIONS/TRACE method support, multiple contexts per host,
+ * file upload support and more.
+ * 
+ * This server is multithreaded in its support for multiple concurrent HTTP
+ * connections, however its constituent classes are not thread-safe and require
+ * external synchronization if accessed by multiple threads concurrently.
+ * 
+ * This server is intentionally written as a single source file, in order
+ * to make it as easy as possible to integrate into any existing project - by 
+ * simply adding this single file to the project sources. It does, however,
+ * aim to maintain a structured and flexible design. There are no external
+ * package dependencies.
+ * 
+ * This file contains elaborate documentation of its classes and methods, as
+ * well as implementation details and references to specific RFC sections
+ * which clarify the logic behind the code. It is recommended that anyone
+ * attempting to modify the protocol-level functionality become acquainted with
+ * the RFC, in order to make sure that protocol compliance is not broken.
+ *
+ * @author Amichai Rothman
+ * @since  2008-07-24
+ */
+public class HTTPServer {
+
+    /**
+     * The SimpleDateFormat-compatible formats of dates which must be supported.
+     * Note that all generated date fields must be in the RFC 1123 format only,
+     * while the others are supported by recipients for backwards-compatibility.
+     */
+    public static final String[] DATE_PATTERNS = {
+        "EEE, dd MMM yyyy HH:mm:ss Z", // RFC 822, updated by RFC 1123
+        "EEEE, dd-MMM-yy HH:mm:ss Z",  // RFC 850, obsoleted by RFC 1036
+        "EEE MMM d HH:mm:ss yyyy"      // ANSI C's asctime() format
+    };
+    
+    /**
+     * A convenience array containing the carriage-return and line feed chars.
+     */
+    public static final byte[] CRLF = { 0x0d, 0x0a };
+    
+    /**
+     * The HTTP status description strings.
+     */
+    protected static final String[] statuses = new String[600];
+    
+    static {
+        // initialize status descriptions lookup table
+        Arrays.fill(statuses, "Unknown Status");
+        statuses[100] = "Continue";
+        statuses[200] = "OK";
+        statuses[204] = "No Content";
+        statuses[206] = "Partial Content";
+        statuses[301] = "Moved Permanently";
+        statuses[302] = "Found";
+        statuses[304] = "Not Modified";
+        statuses[307] = "Temporary Redirect";
+        statuses[400] = "Bad Request";
+        statuses[401] = "Unauthorized";
+        statuses[403] = "Forbidden";
+        statuses[404] = "Not Found";
+        statuses[412] = "Precondition Failed";
+        statuses[413] = "Request Entity Too Large";
+        statuses[414] = "Request-URI Too Large";
+        statuses[416] = "Requested Range Not Satisfiable";
+        statuses[417] = "Expectation Failed";
+        statuses[500] = "Internal Server Error";
+        statuses[501] = "Not Implemented";
+        statuses[502] = "Bad Gateway";
+        statuses[503] = "Service Unavailable";
+        statuses[504] = "Gateway Time-out";
+    }
+
+    /**
+     * A mapping of path suffixes (e.g. file extensions) to their corresponding
+     * MIME types.
+     */
+    protected static final Map<String, String> contentTypes =
+        new ConcurrentHashMap<String, String>();
+    
+    static {
+        // add some default common content types
+        // see http://www.iana.org/assignments/media-types/ for full list
+        addContentType("application/java-archive", "jar");
+        addContentType("application/javascript", "js");
+        addContentType("application/json", "json");
+        addContentType("application/msword", "doc");
+        addContentType("application/octet-stream", "exe");
+        addContentType("application/pdf", "pdf");
+        addContentType("application/vnd.ms-excel", "xls");
+        addContentType("application/vnd.ms-powerpoint", "ppt");
+        addContentType("application/x-compressed", "tgz");
+        addContentType("application/x-gzip", "gz");
+        addContentType("application/x-tar", "tar");
+        addContentType("application/xhtml+xml", "xhtml");
+        addContentType("application/zip", "zip");
+        addContentType("audio/mpeg", "mp3");
+        addContentType("image/gif", "gif");
+        addContentType("image/jpeg", "jpg", "jpeg");
+        addContentType("image/png", "png");
+        addContentType("image/svg+xml", "svg");
+        addContentType("image/x-icon", "ico");
+        addContentType("text/css", "css");
+        addContentType("text/html; charset=utf-8", "htm", "html");
+        addContentType("text/plain", "txt", "text", "log");
+        addContentType("text/xml", "xml");
+    }
+    
+    /**
+     * The {@code LimitedInputStream} provides access to a limited number
+     * of consecutive bytes from the underlying InputStream, starting at its
+     * current position. If this limit is reached, it behaves as though the end
+     * of stream has been reached (although the underlying stream remains open
+     * and may contain additional data).
+     */
+    public static class LimitedInputStream extends FilterInputStream {
+        
+        protected long limit; // decremented when read, until it reaches zero
+        protected boolean prematureEndException;
+
+        /**
+         * Constructs a LimitedInputStream with the given underlying
+         * input stream and limit.
+         * 
+         * @param in the underlying input stream
+         * @param limit the maximum number of bytes that may be consumed from
+         *        the underlying stream before this stream ends. If zero or
+         *        negative, this stream will be at its end from initialization.
+         * @param prematureEndException specifies the stream's behavior when
+         *        the underlying stream end is reached before the limit is
+         *        reached: if true, an exception is thrown, otherwise this
+         *        stream reaches its end as well (i.e. read() returns -1)
+         * @throws NullPointerException if the given stream is null
+         */
+        public LimitedInputStream(InputStream in, long limit,
+                boolean prematureEndException) {
+            super(in);
+            if (in == null)
+                throw new NullPointerException("input stream is null");
+            this.limit = limit < 0 ? 0 : limit;
+            this.prematureEndException = prematureEndException;
+        }
+        
+        public int read() throws IOException {
+            int res = limit == 0 ? -1 : in.read();
+            if (res == -1 && limit > 0 && prematureEndException)
+                throw new IOException("unexpected end of stream");
+            limit = res == -1 ? 0 : limit - 1;
+            return res;
+        }
+        
+        public int read(byte b[], int off, int len) throws IOException {
+            int res = limit == 0 ? -1
+                : in.read(b, off, len > limit ? (int)limit : len);
+            if (res == -1 && limit > 0 && prematureEndException)
+                throw new IOException("unexpected end of stream");
+            limit = res == -1 ? 0 : limit - res;
+            return res;            
+        }
+        
+        public long skip(long n) throws IOException {
+            long res = in.skip(n > limit ? limit : n);
+            limit -= res;
+            return res;
+        }
+        
+        public int available() throws IOException {
+            int res = in.available(); 
+            return res > limit ? (int)limit : res;
+        }
+        
+        public boolean markSupported() {
+            return false;
+        }
+    }
+    
+    /**
+     * The {@code ChunkedInputStream} decodes an InputStream whose data has the
+     * "chunked" transfer encoding applied to it, providing the underlying data.
+     */
+    public static class ChunkedInputStream extends LimitedInputStream {
+        
+        protected Headers headers;
+        protected boolean initialized;
+
+        /**
+         * Constructs a ChunkedInputStream with the given underlying stream, and
+         * a headers container to which the stream's trailing headers will be
+         * added.
+         * 
+         * @param in the underlying "chunked"-encoded input stream
+         * @param headers the headers container to which the stream's trailing
+         *        headers will be added, or null if they are to be discarded
+         * @throws NullPointerException if the given stream is null
+         */
+        public ChunkedInputStream(InputStream in, Headers headers) {
+            super(in, 0, true);
+            this.headers = headers;
+        }
+        
+        public int read() throws IOException {
+            return limit <= 0 && initChunk() < 0 ? -1 : super.read();
+        }
+        
+        public int read(byte b[], int off, int len) throws IOException {
+            return limit <= 0 && initChunk() < 0 ? -1 : super.read(b, off, len);
+        }
+        
+        /**
+         * Initializes the next chunk. If the previous chunk has not yet
+         * ended, or the end of stream has been reached, does nothing.
+         * 
+         * @return the length of the chunk, or -1 if the end of stream
+         *         has been reached
+         * @throws IOException if an IO error occurs or the stream is corrupt
+         */
+        protected long initChunk() throws IOException {
+            if (limit == 0) { // finished previous chunk
+                // read chunk-terminating CRLF if it's not the first chunk
+                if (initialized) {
+                    if (readLine(in).length() > 0)
+                        throw new IOException("chunk data must end with CRLF");
+                } else {
+                    initialized = true;
+                }
+                limit = parseChunkSize(readLine(in)); // read next chunk size
+                if (limit == 0) { // last chunk has size 0
+                    limit = -1; // mark end of stream
+                    // read trailing headers, if any
+                    Headers trailingHeaders = readHeaders(in);
+                    if (headers != null)
+                        headers.addAll(trailingHeaders);
+                }
+            }
+            return limit;
+        }
+        
+        /**
+         * Parses a chunk-size line.
+         *  
+         * @param line the chunk-size line to parse
+         * @return the chunk size
+         * @throws IllegalArgumentException if the chunk-size line is invalid
+         */
+        protected static long parseChunkSize(String line)
+                throws IllegalArgumentException {
+            int pos = line.indexOf(';');
+            if (pos > -1)
+                line = line.substring(0, pos); // ignore params, if any
+            try {
+                return parseLong(line, 16); // throws NFE
+            } catch (NumberFormatException nfe) {
+                throw new IllegalArgumentException(
+                    "invalid chunk size line: \"" + line + "\"");
+            }
+        }
+    }
+    
+    /**
+     * The {@code ChunkedOutputStream} encodes an OutputStream with the
+     * "chunked" transfer encoding. It should be used only when the content
+     * length is not known in advance, and with the response Transfer-Encoding
+     * header set to "chunked".
+     * <p>
+     * Data is written to the stream by invocations of the {@link #initChunk}
+     * method, each followed by writing to the stream exactly the specified
+     * number of data bytes for that chunk. The {@link #writeChunk} method can
+     * be used to this in one method call. To end the stream, the
+     * {@link #writeTrailingChunk} method must be called.
+     */
+    public static class ChunkedOutputStream extends FilterOutputStream {
+        
+        protected int state; // the current stream state
+
+        /**
+         * Constructs a ChunkedOutputStream with the given underlying stream.
+         * 
+         * @param out the underlying output stream to which the chunked stream
+         *        is written.
+         * @throws NullPointerException if the given stream is null
+         */
+        public ChunkedOutputStream(OutputStream out) {
+            super(out);
+            if (out == null)
+                throw new NullPointerException("output stream is null");
+        }
+        
+        /**
+         * Initializes a new chunk with the given size.
+         * 
+         * @param size the chunk size (must be positive)
+         * @throws IllegalArgumentException if size is negative
+         * @throws IOException if an IO error occurs, or the stream has
+         *         already been ended
+         */
+        public void initChunk(long size) throws IOException {
+            if (size < 0)
+                throw new IllegalArgumentException("invalid size: " + size);
+            if (state > 0)
+                out.write(CRLF); // end previous chunk
+            else if (state == 0)
+                state = 1; // start first chunk
+            else if (state < 0)
+                throw new IOException("chunked stream has already ended");
+            out.write(Long.toHexString(size).getBytes("ISO8859_1"));
+            out.write(CRLF);
+        }
+        
+        /**
+         * Writes the trailing chunk which ends the stream.
+         * 
+         * @param headers the (optional) trailing headers to write, or null
+         * @throws IOException if an error occurs
+         */
+        public void writeTrailingChunk(Headers headers) throws IOException {
+            initChunk(0); // zero-sized chunk marks the end of the stream
+            if (headers == null)
+                out.write(CRLF); // empty header block
+            else
+                headers.writeTo(out);
+            state = -1;
+        }
+        
+        /**
+         * Writes a chunk containing the given bytes. This method initializes a
+         * new chunk with the given size, and then writes the chunk data.
+         * 
+         * @param b an array containing the bytes to write
+         * @param off the offset within the array where the data starts
+         * @param len the length of the data in bytes
+         * @throws IOException if an error occurs
+         * @throws IndexOutOfBoundsException if the given offset or length
+         *         are outside the bounds of the given array
+         */
+        public void writeChunk(byte[] b, int off, int len) throws IOException {
+            if (len > 0)
+                initChunk(len);
+            write(b, off, len);
+        }
+    }
+    
+    /**
+     * The {@code MultipartInputStream} decodes an InputStream whose data has a
+     * "multipart/*" content type (see RFC 1521), providing the underlying data
+     * to its various parts.
+     */
+    public static class MultipartInputStream extends FilterInputStream {
+
+        protected final byte[] boundary;
+        protected final int boundaryLength;
+        protected final byte[] buf = new byte[4096];
+        protected int head, tail;
+        protected int extra;
+
+        /**
+         * Constructs a MultipartInputStream with the given underlying stream.
+         * 
+         * @param in the underlying multipart stream
+         * @param boundary the multipart boundary
+         * @throws NullPointerException if the given stream or boundary is null
+         * @throws IllegalArgumentException if the given boundary's size is not
+         *         between 1 and 70
+         */
+        protected MultipartInputStream(InputStream in, byte[] boundary) {
+            super(in);
+            int len = boundary.length;
+            if (len < 1 || len > 70)
+                throw new IllegalArgumentException("invalid boundary length");
+            this.boundary = new byte[len + 2];
+            this.boundary[0] = this.boundary[1] = '-';
+            System.arraycopy(boundary, 0, this.boundary, 2, len);
+            // calculate max boundary length: CRLF--boundary--CRLF
+            boundaryLength = len + 8;
+        }
+        
+        public int read() throws IOException {
+            if (!fill())
+                return -1;
+            return buf[head++];
+        }
+
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (!fill())
+                return -1;
+            len = Math.min(tail - head, len);
+            System.arraycopy(buf, head, b, off, len);
+            head += len;
+            return len;
+        }
+
+        public long skip(long n) throws IOException {
+            if (!fill())
+                return 0;
+            n = Math.min(tail - head, n);
+            head += n;
+            return n;
+        }
+        
+        public int available() throws IOException {
+            return tail - head;
+        }
+
+        public boolean markSupported() {
+            return false;
+        }
+        
+        /**
+         * Advances the stream position to the beginning of the next part.
+         * 
+         * @return true if successful, or false if there are no more parts
+         * @throws IOException if an error occurs
+         */
+        public boolean nextPart() throws IOException {
+            while (skip(buf.length) != 0); // skip rest of previous part and read boundary
+            head = tail = findBoundary()[1]; // start next part after previous boundary
+            extra -= tail;
+            return fill();
+        }
+        
+        /**
+         * Fills the buffer with more data of the current part.
+         * 
+         * @return true if more data is available, or false if the part's
+         *         end has been reached
+         * @throws IOException if an error occurs
+         */
+        protected boolean fill() throws IOException {
+            // check if we already have readable data
+            if (head != tail)
+                return true;
+            // shift extra unread data to beginning of buffer
+            if (tail > 0 && extra > 0)
+                System.arraycopy(buf, tail, buf, 0, extra);
+            head = tail = 0;
+            // read more until we have at least enough data for a boundary,
+            // and if possible, more readable data
+            do { 
+                int read = super.read(buf, extra, buf.length - extra);
+                if (read > -1) {
+                    extra += read;
+                } else if (extra < boundaryLength) {
+                    if (extra == 0)
+                        return false; // end of stream
+                    throw new IOException("missing end boundary");
+                }
+            } while (extra < boundaryLength);
+            // check if there's a boundary and update indices
+            int max = findBoundary()[0];
+            head = 0;
+            tail = max == -1 ? extra - boundaryLength : max;
+            extra -= tail;
+            return max != 0; // no more readable data in current part
+        }
+        
+        /**
+         * Finds the boundary within the current buffer's valid data.
+         * 
+         * @return an array containing the start index and length of
+         *         the found boundary, or -1 if it is not found
+         * @throws IOException if an error occurs
+         */
+        protected int[] findBoundary() throws IOException {
+            for (int i = 0; i <= extra - boundary.length; i++) {
+                int j = 0;
+                while (j < boundary.length && buf[i + j] == boundary[j])
+                    j++;
+                // if we found the boundary, add the prefix and suffix too
+                if (j == boundary.length) {
+                    if (buf[i + j] == '-' && buf[i + j + 1] == '-')
+                        j += 2; // end of entire multipart
+                    if (buf[i + j] != CRLF[0] || buf[i + j + 1] != CRLF[1])
+                        throw new IOException("boundary must end with CRLF");
+                    // include prefix CRLF, if exists
+                    if (i > 1 && buf[i-2] == CRLF[0] && buf[i-1] == CRLF[1]) {
+                        i -= 2;
+                        j += 2;
+                    }
+                    return new int[] { i, j + 2 }; // including ending CRLF
+                }
+            }
+            return new int[] { -1, -1 };
+        }
+    }
+    
+    /**
+     * The {@code MultipartIterator} iterates over the parts of a multipart/form-data request.
+     */
+    public static class MultipartIterator implements Iterator<MultipartIterator.Part> {
+        
+        /**
+         * The {@code Part} class encapsulates a single part of the multipart.
+         * <p>Note: the body input stream must not be closed.
+         */
+        public static class Part {
+            public String name;
+            public String filename;
+            public Headers headers;
+            public InputStream body;
+        }
+        
+        protected final MultipartInputStream in;
+        protected boolean next;
+        
+        /**
+         * Creates a new MultipartIterator from the given request.
+         * 
+         * @param req the multipart/form-data request
+         * @throws IOException if an IO error occurs
+         * @throws IllegalArgumentException if the given request's content type
+         *         is not multipart/form-data, or is missing the boundary
+         */
+        public MultipartIterator(Request req) throws IOException {
+            Map<String, String> params = req.getHeaders().getParams("Content-Type");
+            if (!params.containsKey("multipart/form-data"))
+                throw new IllegalArgumentException("given request is not of type multipart/form-data");
+            String boundary = params.get("boundary");
+            if (boundary == null)
+                throw new IllegalArgumentException("Content-Type is missing boundry");
+            in = new MultipartInputStream(req.getBody(), boundary.getBytes("US-ASCII"));
+        }
+        
+        public boolean hasNext() {
+            try {
+                return next || (next = in.nextPart());
+            } catch (IOException ioe) {
+                throw new RuntimeException(ioe);
+            }
+        }
+        
+        public Part next() {
+            if (!hasNext())
+                throw new NoSuchElementException();
+            next = false;
+            Part p = new Part();
+            try {
+                p.headers = readHeaders(in);
+            } catch (IOException ioe) {
+                throw new RuntimeException(ioe);
+            }
+            p.name = p.headers.getParams("Content-Disposition").get("name");
+            p.filename = p.headers.getParams("Content-Disposition").get("filename");
+            p.body = in;
+            return p;
+        }
+        
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+    
+    /**
+     * The {@code VirtualHost} class represents a virtual host in the server.
+     */
+    public static class VirtualHost {
+        
+        public static final String DEFAULT_HOST_NAME = "~DEFAULT~";
+        
+        protected final String name;
+        protected final Set<String> aliases = new CopyOnWriteArraySet<String>();
+        protected final Map<String, ContextHandler> contexts =
+            new ConcurrentHashMap<String, ContextHandler>();
+        protected volatile String directoryIndex = "index.html";
+        private volatile boolean allowGeneratedIndex;
+        
+        /**
+         * Constructs a VirtualHost with the given name.
+         * 
+         * @param name the host's name, or null if it is the default host
+         */
+        public VirtualHost(String name) {
+            this.name = name;
+        }
+        
+        /**
+         * Returns this host's name.
+         * 
+         * @return this host's name, or null if it is the default host
+         */
+        public String getName() {
+            return name;
+        }
+        
+        /**
+         * Adds an alias for this host.
+         * 
+         * @param alias the alias
+         */
+        public void addAlias(String alias) {
+            aliases.add(alias);
+        }
+
+        /**
+         * Returns this host's aliases.
+         * 
+         * @return the (unmodifiable) set of aliases (which may be empty)
+         */
+        public Set<String> getAliases() {
+            return Collections.unmodifiableSet(aliases);
+        }
+        
+        /**
+         * Sets the directory index file. For every request whose URI ends with
+         * a '/' (i.e. a directory), the index file is appended to the path,
+         * and the resulting resource is served if it exists. If it does not
+         * exist, an auto-generated index for the requested directory may be
+         * served, depending on whether {@link #setAllowGeneratedIndex
+         * a generated index is allowed}, otherwise an error is returned.
+         * The default directory index file is "index.html".
+         * 
+         * @param directoryIndex the directory index file, or null if no
+         *        index file should be used
+         */
+        public void setDirectoryIndex(String directoryIndex) {
+            this.directoryIndex = directoryIndex;
+        }
+        
+        /**
+         * Gets this host's directory index file.
+         * 
+         * @return the directory index file, or null
+         */
+        public String getDirectoryIndex() {
+            return directoryIndex;
+        }
+        
+        /**
+         * Sets whether auto-generated indices are allowed. If false,
+         * and a directory resource is requested, an error will be
+         * returned instead. 
+         * 
+         * @param allowed specifies whether generated indices are allowed
+         */
+        public void setAllowGeneratedIndex(boolean allowed) {
+            this.allowGeneratedIndex = allowed;
+        }
+        
+        /**
+         * Returns whether auto-generated indices are allowed.
+         * 
+         * @return whether auto-generated indices are allowed
+         */
+        public boolean isAllowGeneratedIndex() {
+            return allowGeneratedIndex;
+        }
+        
+        /**
+         * Returns the context handler for the given path.
+         * 
+         * If a context is not found for the given path, the search is repeated
+         * for its parent path, and so on until a base context is found. If
+         * neither the given path nor any of its parents has a context,
+         * null is returned.
+         * 
+         * @param path the context's path
+         * @return a context handler for the given path, or null if none exists
+         */
+        public ContextHandler getContext(String path) {
+            path = rtrim(path, '/'); // remove trailing slash
+            ContextHandler handler = null;
+            while (handler == null && path != null) {
+                handler = contexts.get(path);
+                path = getParentPath(path);
+            }
+            return handler;
+        }
+        
+        /**
+         * Adds a context and its corresponding context handler to this server.
+         * Paths are normalized by removing trailing slashes (except the root).
+         * 
+         * @param path the context's path (must start with '/')
+         * @param handler the context handler for the given path
+         * @throws IllegalArgumentException if path is malformed
+         */
+        public void addContext(String path, ContextHandler handler) {
+            if (path == null || !path.startsWith("/"))
+                throw new IllegalArgumentException("invalid path: " + path);
+            contexts.put(rtrim(path, '/'), handler);
+        }
+        
+        /**
+         * Adds context for all methods of the given object that are annotated
+         * with the {@link Context} annotation. The methods must have the exact
+         * same signature as {@link ContextHandler#serve(Request, Response)}
+         * and implement the same contract.
+         * 
+         * @param o the object whose annotated methods are added
+         * @throws IllegalArgumentException if a Context-annotated method
+         *         is invalid
+         */
+        public void addContexts(Object o) throws IllegalArgumentException {
+            for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {   
+                // add to contexts those with @Context annotation
+                for (Method m : c.getDeclaredMethods()) {
+                    Context context = m.getAnnotation(Context.class);
+                    if (context != null) {
+                        m.setAccessible(true); // allow access to private member
+                        Class<?>[] params = m.getParameterTypes();
+                        if (params.length != 2
+                            || !Request.class.isAssignableFrom(params[0])
+                            || !Response.class.isAssignableFrom(params[1])
+                            || !int.class.isAssignableFrom(m.getReturnType()))
+                                throw new IllegalArgumentException(
+                                    "@Context used with invalid method: " + m);
+                        String path = context.value();
+                        addContext(path, new MethodContextHandler(m, o));
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * The {@code Context} annotation decorates fields which are mapped
+     * to a context (path) within the server, and provide its contents.
+     * 
+     * @see VirtualHost#addContexts(Object)
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    public static @interface Context {
+        
+        /**
+         * The context (path) that this field maps to (must begin with '/').
+         */
+        String value();
+    }
+    
+    /**
+     * A {@code ContextHandler} is capable of serving content for
+     * resources within its context.
+     * 
+     * @see VirtualHost#addContext(String, ContextHandler)
+     */
+    public static interface ContextHandler {
+        
+        /**
+         * Serves the given request using the given response.
+         * 
+         * @param req the request to be served
+         * @param resp the response to be filled
+         * @return an HTTP status code, which will be used in returning
+         *         a default response appropriate for this status. If this
+         *         method invocation already sent anything in the response
+         *         (headers or content), it must return 0, and no further
+         *         processing will be done
+         * @throws IOException if an IO error occurs
+         */
+        public int serve(Request req, Response resp) throws IOException;
+    }
+    
+    /**
+     * The {@code FileContextHandler} services a context by mapping it
+     * to a file or folder (recursively) on disk.
+     */
+    public static class FileContextHandler implements ContextHandler {
+        
+        protected final File base;
+        protected final String context;
+        
+        public FileContextHandler(File dir, String context) throws IOException {
+            this.base = dir.getCanonicalFile();
+            this.context = rtrim(context, '/'); // remove trailing slash;
+        }
+        
+        public int serve(Request req, Response resp) throws IOException {
+            return serveFile(base, context, req, resp);
+        }
+    }
+    
+    /**
+     * The {@code MethodContextHandler} services a context by invoking
+     * a handler method on a specified object.
+     * 
+     * @see VirtualHost#addContexts(Object)
+     */
+    public static class MethodContextHandler implements ContextHandler {
+        
+        protected final Method m;
+        protected final Object obj;
+        
+        public MethodContextHandler(Method m, Object obj) {
+            this.m = m;
+            this.obj = obj;
+        }
+        
+        public int serve(Request req, Response resp) throws IOException {
+            try {
+                return (Integer)m.invoke(obj, req, resp);
+            } catch (InvocationTargetException ite) {
+                throw new IOException("error: " + ite.getCause().getMessage());
+            } catch (Exception e) {
+                throw new IOException("error: " + e);
+            }
+        }
+    }
+    
+    /**
+     * The {@code Header} class encapsulates a single HTTP header.
+     */
+    public static class Header {
+        
+        protected final String name;
+        protected final String value;
+        
+        /**
+         * Constructs a header with the given name and value.
+         * Leading and trailing whitespace are trimmed.
+         * 
+         * @param name the header name
+         * @param value the header value
+         * @throws NullPointerException if name or value is null
+         * @throws IllegalArgumentException if name is empty
+         */
+        public Header(String name, String value) {
+            this.name = name.trim();
+            this.value = value.trim();
+            // RFC2616#14.23 - header can have an empty value (e.g. Host)
+            if (this.name.length() == 0)
+                throw new IllegalArgumentException("name cannot be empty");
+        }
+        
+        /**
+         * Returns this header's name.
+         * 
+         * @return this header's name
+         */
+        public String getName() { return name; }
+        
+        /**
+         * Returns this header's value.
+         * 
+         * @return this header's value
+         */
+        public String getValue() { return value; }
+    }
+    
+    /**
+     * The {@code Headers} class encapsulates a collection of HTTP headers.
+     * 
+     * Header names are treated case-insensitively, although this class retains
+     * their original case. Header insertion order is maintained as well.
+     */
+    public static class Headers implements Iterable<Header> {
+        
+        // due to the requirements of case-insensitive name comparisons,
+        // retaining the original case, and retaining header insertion order,
+        // and due to the fact that the number of headers is generally
+        // quite small (usually under 12 headers), we use a simple array with
+        // linear access times, which proves to be more efficient and
+        // straightforward than the alternatives 
+        protected Header[] headers = new Header[12];
+        protected int count;
+        
+        /**
+         * Returns the number of added headers.
+         * 
+         * @return the number of added headers
+         */
+        public int size() {
+            return count;
+        }
+        
+        /**
+         * Returns the value of the first header with the given name.
+         * 
+         * @param name the header name (case insensitive)
+         * @return the header value, or null if none exists
+         */
+        public String get(String name) {
+            for (int i = 0; i < count; i++)
+                if (headers[i].getName().equalsIgnoreCase(name))
+                    return headers[i].getValue();
+            return null;
+        }
+        
+        /**
+         * Returns the Date value of the header with the given name.
+         * 
+         * @param name the header name (case insensitive)
+         * @return the header value as a Date, or null if none exists
+         *         or if the value is not in any supported date format
+         */
+        public Date getDate(String name) {
+            try {
+                String header = get(name);
+                return header == null ? null : parseDate(header);
+            } catch (IllegalArgumentException iae) {
+                return null;
+            }
+        }
+        
+        /**
+         * Returns whether there exists a header with the given name.
+         * 
+         * @param name the header name (case insensitive)
+         * @return whether there exists a header with the given name
+         */
+        public boolean contains(String name) {
+            return get(name) != null;
+        }
+        
+        /**
+         * Adds a header with the given name and value to the end of this
+         * collection of headers. Leading and trailing whitespace are trimmed. 
+         * 
+         * @param name the header name (case insensitive)
+         * @param value the header value
+         */
+        public void add(String name, String value) {
+            Header header = new Header(name, value); // also validates
+            // expand array if necessary
+            if (count == headers.length) {
+                Header[] spacious = new Header[2 * count];
+                System.arraycopy(headers, 0, spacious, 0, count);
+                headers = spacious;
+            }
+            headers[count++] = header; // inlining header would cause a bug!
+        }
+        
+        /**
+         * Adds all given headers to the end of this collection of headers,
+         * in their original order.
+         * 
+         * @param headers the headers to add
+         */
+        public void addAll(Headers headers) {
+            for (Header header : headers)
+                add(header.getName(), header.getValue());
+        }
+        
+        /**
+         * Adds a header with the given name and value, replacing the first
+         * existing header with the same name. If there is no existing header
+         * with the same name, it is added as in {@link #add}.
+         * 
+         * @param name the header name (case insensitive)
+         * @param value the header value
+         * @return the replaced header, or null if none existed
+         */
+        public Header replace(String name, String value) {
+            for (int i = 0; i < count; i++) {
+                if (headers[i].getName().equalsIgnoreCase(name)) {
+                    Header prev = headers[i];
+                    headers[i] = new Header(name, value);
+                    return prev;
+                }
+            }
+            add(name, value);
+            return null;
+        }
+        
+        /**
+         * Removes all headers with the given name (if any exist).
+         * 
+         * @param name the header name (case insensitive)
+         */
+        public void remove(String name) {
+            int j = 0;
+            for (int i = 0; i < count; i++)
+                if (!headers[i].getName().equalsIgnoreCase(name))
+                    headers[j++] = headers[i];
+            while (count > j)
+                headers[--count] = null;
+        }
+        
+        /**
+         * Writes the headers to the given stream (including trailing CRLF).
+         * 
+         * @param out the stream to write the headers to
+         * @throws IOException if an error occurs
+         */
+        public void writeTo(OutputStream out) throws IOException {
+            for (int i = 0; i < count; i++) {
+                String s = headers[i].getName() + ": " + headers[i].getValue();
+                out.write(s.getBytes("ISO8859_1"));
+                out.write(CRLF);
+            }
+            out.write(CRLF); // ends header block
+        }
+        
+        /**
+         * Returns a header's parameters. Parameter order is maintained,
+         * and the first key (in iteration order) is the header's value
+         * without the parameters.
+         * 
+         * @param name the header name (case insensitive)
+         * @return the header's parameter names and values
+         */
+        public Map<String, String> getParams(String name) {
+            Map<String, String> params = new LinkedHashMap<String, String>();
+            for (String param : split(get(name), ';')) {
+                String[] pair = split(param, '=');
+                String val = pair.length == 1 ? "" : ltrim(rtrim(pair[1], '"'), '"');
+                params.put(pair[0], val);
+            }
+            return params;
+        }
+        
+        /**
+         * Returns an iterator over the headers, in their insertion order.
+         * If the headers collection is modified during iteration, the
+         * iteration result is undefined. The remove operation is unsupported.
+         * 
+         * @return an Iterator over the headers
+         */
+        public Iterator<Header> iterator() {
+            return new Iterator<Header>() {
+                
+                int ind;
+                
+                public boolean hasNext() {
+                    return ind < count;
+                }
+
+                public Header next() {
+                    if (ind == count)
+                        throw new NoSuchElementException();
+                    return headers[ind++];
+                }
+
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        }
+    }
+    
+    /**
+     * The {@code Request} class encapsulates a single HTTP request.
+     */
+    public class Request {
+        
+        protected String method;
+        protected URI uri;
+        protected String version;
+        protected Headers headers;
+        protected InputStream body;
+        protected Map<String, String> params; // cached value
+        
+        /**
+         * Constructs a Request from the data in the given input stream.
+         * 
+         * @param in the input stream from which the request is read
+         * @throws IOException if an error occurs
+         */
+        public Request(InputStream in) throws IOException {
+            readRequestLine(in);
+            headers = readHeaders(in);
+            // RFC2616#3.6 - if "chunked" is used, it must be the last one
+            // RFC2616#4.4 - if non-identity Transfer-Encoding is present,
+            // it must either include "chunked" or close the connection after
+            // the body, and in any case ignore Content-Length.
+            // if there is no such Transfer-Encoding, use Content-Length
+            // if neither header exists, there is no body
+            String header = headers.get("Transfer-Encoding");
+            if (header != null && !header.equals("identity")) {
+                if (header.toLowerCase().contains("chunked"))
+                    body = new ChunkedInputStream(in, headers);
+                else
+                    body = in; // body ends when connection closes
+            } else {
+                header = headers.get("Content-Length");
+                long len = header == null ? 0 : parseLong(header, 10);
+                body = new LimitedInputStream(in, len, false);
+            }
+        }
+        
+        /**
+         * Returns the request method.
+         * 
+         * @return the request method
+         */
+        public String getMethod() { return method; }
+        
+        /**
+         * Returns the request URI.
+         * 
+         * @return the request URI
+         */
+        public URI getURI() { return uri; }
+        
+        /**
+         * Returns the request version string.
+         * 
+         * @return the request version string
+         */
+        public String getVersion() { return version; }
+        
+        /**
+         * Returns the input stream containing the request body.
+         * 
+         * @return the input stream containing the request body
+         */
+        public InputStream getBody() { return body; }
+        
+        /**
+         * Returns the request headers collection.
+         * 
+         * @return the request headers collection
+         */
+        public Headers getHeaders() { return headers; }
+        
+        /**
+         * Returns the path component of the request URI,
+         * after URL decoding has been applied (using the UTF-8 charset).
+         * 
+         * @return the decoded path component of the request URI
+         */
+        public String getPath() {
+            return uri.getPath();
+        }
+
+        /**
+         * Sets the path component of the request URI. This can be useful
+         * in URL rewriting, etc.
+         * 
+         * @param path the path to set
+         * @throws IllegalArgumentException if the given path is malformed
+         */
+        public void setPath(String path) {
+            try {
+                uri = new URI(uri.getScheme(), uri.getHost(),
+                    trimDuplicates(path, '/'), uri.getFragment());
+            } catch (URISyntaxException use) {
+                throw new IllegalArgumentException("error setting path", use);
+            }
+        }
+        
+        /**
+         * Returns the base URL (scheme, host and port) of the request resource.
+         * The host name is taken from the request URI or the Host header or a
+         * default host (see RFC2616#5.2).
+         * 
+         * @return the base URL of the requested resource, or null if it
+         *         is malformed
+         */
+        public URL getBaseURL() {
+            // normalize host header
+            String host = uri.getHost();
+            if (host == null) {
+                host = headers.get("Host");
+                if (host == null) // missing in HTTP/1.0
+                    host = detectLocalHostName();
+            }
+            int pos = host.indexOf(':');
+            host = pos == -1 ? host : host.substring(0, pos);
+            try {
+                return new URL("http", host, port, "");
+            } catch (MalformedURLException mue) {
+                return null;
+            }
+        }
+
+        /**
+         * Consumes (reads and discards) the entire request body.
+         * 
+         * @throws IOException if an error occurs
+         */
+        public void consumeBody() throws IOException {
+            if (body.read() != -1) { // small optimization
+                byte[] b = new byte[4096];
+                while (body.read(b) != -1);
+            }
+        }
+        
+        /**
+         * Returns the request parameters, which are parsed both from the query
+         * part of the request URI, and from the request body if its content
+         * type is "application/x-www-form-urlencoded" (i.e. submitted form).
+         * UTF-8 encoding is assumed in both cases.
+         * 
+         * @return the request parameters name-value pairs
+         * @throws IOException if an error occurs
+         * @see HTTPServer#parseParams(String)
+         */
+        public Map<String, String> getParams() throws IOException {
+            if (params != null)
+                return params;
+            params = new LinkedHashMap<String, String>();
+            String paramString = uri.getRawQuery();
+            if (paramString != null)
+                params.putAll(parseParams(paramString));
+            String contentType = headers.get("Content-Type");
+            if (contentType != null && contentType.toLowerCase()
+                    .startsWith("application/x-www-form-urlencoded"))
+                params.putAll(parseParams(
+                    readToken(body, -1, "UTF-8", 2097152))); // 2MB limit
+            return params;
+        }
+        
+        /**
+         * Returns the absolute (zero-based) content range value read
+         * from the Range header. If multiple ranges are requested, a single
+         * range containing all of them is returned.
+         * 
+         * @param length the full length of the requested resource
+         * @return the requested range, or null if the Range header
+         *         is missing or invalid
+         */
+        public long[] getRange(long length) {
+            String header = headers.get("Range");
+            return header == null || !header.startsWith("bytes=")
+                ? null : parseRange(header.substring(6), length);
+        }
+        
+        /**
+         * Reads the request line, parsing the method, URI and version string.
+         * 
+         * @param in the input stream from which the request line is read
+         * @throws IOException if an error occurs or the request line is invalid
+         */
+        protected void readRequestLine(InputStream in) throws IOException {
+            // RFC2616#4.1: should accept empty lines before request line
+            // RFC2616#19.3: tolerate additional whitespace between tokens
+            String line;
+            do { line = readLine(in); } while (line.length() == 0);
+            String[] tokens = split(line, ' ');
+            if (tokens.length != 3)
+                throw new IOException("invalid request line: \"" + line + "\"");
+            try {
+                method = tokens[0];
+                // must remove '//' prefix which constructor parses as host name
+                uri = new URI(trimDuplicates(tokens[1], '/'));
+                version = tokens[2];
+            } catch (URISyntaxException use) {
+                throw new IOException("invalid URI: " + use.getMessage());
+            }
+        }
+
+        /**
+         * Returns the virtual host corresponding to the requested host name,
+         * or the default host if none exists.
+         * 
+         * @return the virtual host corresponding to the requested host name,
+         *         or the default virtual host
+         */
+        public VirtualHost getVirtualHost() {
+            String name = getBaseURL().getHost();
+            VirtualHost host = HTTPServer.this.getVirtualHost(name);
+            return host != null ? host : HTTPServer.this.getVirtualHost(null);
+        }
+    }
+    
+    /**
+     * The {@code Response} class encapsulates a single HTTP response.
+     */
+    public class Response {
+        
+        protected OutputStream out;
+        protected Headers headers;
+        protected boolean discardBody;
+        
+        /**
+         * Constructs a Response whose output is written to the given stream.
+         * 
+         * @param out the stream to which the response is written
+         */
+        public Response(OutputStream out) {
+            this.out = out;
+            this.headers = new Headers();
+        }
+        
+        /**
+         * Sets whether this response's body is discarded or sent.
+         * 
+         * @param discardBody specifies whether the body is discarded or not
+         */
+        public void setDiscardBody(boolean discardBody) {
+            this.discardBody = discardBody;
+        }
+        
+        /**
+         * Returns whether the response body is discarded.
+         * 
+         * @return true if the response body is discarded, false otherwise
+         */
+        public boolean isDiscardBody() {
+            return discardBody;
+        }
+        
+        /**
+         * Returns an output stream into which the response body can be written.
+         * The body must be written only after the headers have been sent.
+         * 
+         * @return an output stream into which the response body can be written,
+         *         or null if isDiscardBody() returns true, in which case the
+         *         body should not be written
+         */
+        public OutputStream getBody() {
+            return discardBody ? null : out;
+        }
+        
+        /**
+         * Returns the request headers collection.
+         * 
+         * @return the request headers collection
+         */
+        public Headers getHeaders() { return headers; }
+        
+        /**
+         * Sends the response headers with the given response status.
+         * A Date header is added if it does not already exist.
+         * 
+         * @param status the response status
+         * @throws IOException if an error occurs
+         */
+        public void sendHeaders(int status) throws IOException {
+            if (!headers.contains("Date"))
+                headers.add("Date", formatDate(System.currentTimeMillis()));
+            headers.add("Server", "freeutils-HTTPServer/1.0");
+            String line = "HTTP/1.1 " + status + " " + statuses[status]; 
+            out.write(line.getBytes("ISO8859_1"));
+            out.write(CRLF);
+            headers.writeTo(out);
+            out.flush();
+        }
+        
+        /**
+         * Sends the response headers, including the given response status
+         * and description, and all response headers. If they do not already
+         * exist, the following headers are added as necessary:
+         * Content-Range, Content-Length, Content-Type, Last-Modified,
+         * ETag and Date. Ranges are properly calculated as well, with a 200
+         * status changed to a 206 status.
+         * 
+         * @param status the response status
+         * @param length the response body length, or negative if unknown
+         * @param lastModified the last modified date of the response resource,
+         *        or non-positive if unknown. A time in the future will be
+         *        replaced with the current system time.
+         * @param etag the ETag of the response resource, or null if unknown
+         *        (see RFC2616#3.11)
+         * @param contentType the content type of the response resource, or
+         *        null if unknown (in which case "application/octet-stream"
+         *        will be sent)
+         * @param range the content range that will be sent, or null if the
+         *        entire resource will be sent
+         * @throws IOException if an error occurs
+         */
+        public void sendHeaders(int status, long length,
+                long lastModified, String etag, String contentType,
+                long[] range) throws IOException {
+            if (range != null) {
+                headers.add("Content-Range", "bytes " + range[0] + "-" +
+                    range[1] + "/" + (length >= 0 ? length : "*"));
+                length = range[1] - range[0] + 1;
+                if (status == 200)
+                    status = 206;
+            }
+            if (length >= 0 && !headers.contains("Content-Length")
+                            && !headers.contains("Transfer-Encoding"))
+                headers.add("Content-Length", Long.toString(length));
+            if (!headers.contains("Content-Type")) {
+                if (contentType == null)
+                    contentType = "application/octet-stream";
+                headers.add("Content-Type", contentType);
+            }
+            if (lastModified > 0 && !headers.contains("Last-Modified")) {
+                if (lastModified > System.currentTimeMillis()) // RFC2616#14.29
+                    lastModified = System.currentTimeMillis();
+                headers.add("Last-Modified", formatDate(lastModified));
+            }
+            if (etag != null && !headers.contains("ETag"))
+                headers.add("ETag", etag);
+            sendHeaders(status);
+        }
+        
+        /**
+         * Sends the full response with the given status, and the given string
+         * as the body. The text is sent in the UTF-8 charset. If a
+         * Content-Type header was not explicitly set, it will be set to
+         * text/html, and so the text must contain valid (and properly
+         * {@link HTTPServer#escapeHTML escaped}) HTML.
+         * 
+         * @param status the response status
+         * @param text the text body (sent as text/html)
+         * @throws IOException if an error occurs
+         */
+        public void send(int status, String text) throws IOException {
+            byte[] content = text.getBytes("UTF-8");
+            sendHeaders(status, content.length, -1,
+                "\"H" + Integer.toHexString(text.hashCode()) + "\"",
+                "text/html; charset=utf-8", null);
+            if (!discardBody)
+                out.write(content);
+            out.flush();
+        }
+        
+        /**
+         * Sends an error response with the given status and detailed message.
+         * An HTML body is created containing the status and its description,
+         * as well as the message, which is escaped using the
+         * {@link HTTPServer#escapeHTML escape} method.
+         * 
+         * @param status the response status
+         * @param text the text body (sent as text/html)
+         * @throws IOException if an error occurs
+         */
+        public void sendError(int status, String text) throws IOException {
+            Formatter f = new Formatter();
+            f.format("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">%n" +
+                "<html>%n<head><title>%d %s</title></head>%n" +
+                "<body><h1>%d %s</h1>%n<p>%s</p>%n</body></html>",
+                status, statuses[status], status, statuses[status],
+                escapeHTML(text));
+            send(status, f.toString());
+        }        
+        
+        /**
+         * Sends an error response with the given status and default body.
+         * 
+         * @param status the response status
+         * @throws IOException if an error occurs
+         */
+        public void sendError(int status) throws IOException {
+            String text = status < 400 ? ":)" : "sorry it didn't work out :(";
+            sendError(status, text);
+        }
+        
+        /**
+         * Sends the request body. This method must be called only after the
+         * response headers have been sent (and indicate that there is a body).
+         * 
+         * @param body a stream containing the response body
+         * @param length the full length of the response body
+         * @param range the subrange within the request body that should be
+         *        sent, or null if the entire body should be sent
+         * @throws IOException if an error occurs
+         */
+        public void sendBody(InputStream body, long length, long[] range)
+                throws IOException {
+            if (!discardBody) {
+                if (range != null) {
+                    long offset = range[0];
+                    length = range[1] - range[0] + 1;
+                    while (offset > 0) {
+                        long skipped = body.skip(offset);
+                        if (skipped == 0)
+                            throw new IOException("can't skip to " + range[0]);
+                        offset -= skipped;
+                    }
+                }
+                transfer(body, out, length);
+            }
+            out.flush();
+        }
+
+        /**
+         * Sends a 301 or 302 response, redirecting the client to the given URL.
+         * 
+         * @param url the absolute URL to which the client is redirected
+         * @param permanent specifies whether a permanent (301) or
+         *        temporary (302) redirect status is sent
+         * @throws IOException if an IO error occurs or url is malformed
+         */
+        public void redirect(String url, boolean permanent) throws IOException {
+            try {
+                url = new URI(url).toASCIIString();
+            } catch (URISyntaxException e) {
+                throw new IOException("malformed URL: " + url);
+            }
+            headers.add("Location", url);
+            // some user-agents expect a body, so we send it
+            if (permanent)
+                sendError(301, "Permanently moved to " + url);
+            else
+                sendError(302, "Temporarily moved to " + url);
+        }
+    }
+    
+    /**
+     * The {@code SocketHandlerThread} handles accepted sockets.
+     */
+    protected class SocketHandlerThread extends Thread {
+        
+        public SocketHandlerThread() {
+            setName(getClass().getSimpleName() + "-" + port);
+        }
+
+        public void run() {
+            try {
+                while (!serv.isClosed()) {
+                    final Socket sock = serv.accept();
+                    executor.execute(new Runnable() {
+                        public void run() {
+                            try {
+                                sock.setSoTimeout(10000);
+                                handleConnection(sock);
+                            } catch (IOException ignore) {
+                            } finally {
+                                try {
+                                    sock.close();
+                                } catch (IOException ignore) {}
+                            }
+                        }
+                    });
+                }
+            } catch (IOException ignore) {}
+        }
+    }
+    
+    protected volatile int port;
+    protected volatile Executor executor;
+    protected volatile ServerSocket serv;
+    protected final Map<String, VirtualHost> hosts =
+        new ConcurrentHashMap<String, VirtualHost>();
+    
+    /**
+     * Constructs an HTTPServer which can accept connections on the given port.
+     * Note: the {@link #start()} method must be called to start accepting
+     * connections.
+     * 
+     * @param port the port on which this server will accept connections
+     */
+    public HTTPServer(int port) {
+        setPort(port);
+        addVirtualHost(new VirtualHost(null)); // add default virtual host
+    }
+    
+    /**
+     * Constructs an HTTPServer which can accept connections on the default HTTP port 80.
+     * Note: the {@link #start()} method must be called to start accepting
+     * connections.
+     */
+    public HTTPServer() {
+        this(80);
+    }
+    
+    /**
+     * Sets the port on which this server will accept connections.
+     * 
+     * @param port the port on which this server will accept connections
+     */
+    public void setPort(int port) {
+    	this.port = port;
+    }
+    
+    /**
+     * Sets the Executor used in servicing HTTP connections. If null,
+     * a default Executor is used.
+     * 
+     * @param executor the executor to use
+     */
+    public void setExecutor(Executor executor) {
+        this.executor = executor;
+    }
+    
+    /**
+     * Returns the virtual host with the given name.
+     * 
+     * @param name the name of the virtual host to return, or null for
+     *        the default virtual host
+     * @return the virtual host with the given name, or null if it doesn't exist
+     */
+    public VirtualHost getVirtualHost(String name) {
+        return hosts.get(name == null ? VirtualHost.DEFAULT_HOST_NAME : name);
+    }
+    
+    /**
+     * Returns all virtual hosts.
+     * 
+     * @return all virtual hosts (as an unmodifiable set)
+     */
+    public Set<VirtualHost> getVirtualHosts() {
+        return Collections.unmodifiableSet(
+            new HashSet<VirtualHost>(hosts.values()));
+    }
+    
+    /**
+     * Adds the given virtual host to the server.
+     * If the host's name or aliases already exist, they are overwritten.
+     * 
+     * @param host the virtual host to add
+     */
+    public void addVirtualHost(VirtualHost host) {
+        String name = host.getName();
+        hosts.put(name == null ? VirtualHost.DEFAULT_HOST_NAME : name, host);
+    }
+    
+    /**
+     * Starts this server. If it is already started, does nothing.
+     * Note: Once the server is started, configuration-altering methods
+     * of the server and its virtual hosts must not be used. To modify the
+     * configuration, the server must first be stopped.
+     * 
+     * @throws IOException if the server cannot begin accepting connections
+     */
+    public synchronized void start() throws IOException {
+        if (serv != null)
+            return;
+        // djk: ONLY LISTEN ON LOCALHOST!
+        serv = new ServerSocket(port, -1, InetAddress.getLocalHost());
+        if (executor == null) // assign default executor if needed
+            executor = Executors.newCachedThreadPool();
+        // register all host aliases (which may have been modified)
+        for (VirtualHost host : getVirtualHosts())
+            for (String alias : host.getAliases())
+                hosts.put(alias, host);
+        // start handling incoming connections
+        new SocketHandlerThread().start();
+    }
+    
+    /**
+     * Stops this server. If it is already stopped, does nothing.
+     */
+    public synchronized void stop() {
+        try {
+            if (serv != null)
+                serv.close();
+        } catch (IOException e) {}
+        serv = null;
+    }
+    
+    /**
+     * Handles communications for a single connection, on the given socket.
+     * Multiple subsequent transactions are handled on the connection, until
+     * the socket is closed by either side, an error occurs, or the request
+     * contains a "Connection: close" header which explicitly requests the
+     * connection be closed after the transaction ends.
+     * 
+     * @param sock the socket on which communications are handled
+     * @throws IOException if and error occurs
+     */
+    protected void handleConnection(Socket sock) throws IOException {
+        OutputStream out = new BufferedOutputStream(
+            sock.getOutputStream(), 4096);
+        InputStream in = new BufferedInputStream(sock.getInputStream(), 4096);
+        String connectionHeader;
+        do {
+            // create request and response 
+            Response resp = new Response(out);
+            Request req;
+            try {
+                req = new Request(in);
+            } catch (InterruptedIOException ignore) { // timeout
+                break;
+            } catch (IOException ioe) {
+                resp.sendError(400, "invalid request: " + ioe.getMessage());
+                break;
+            }
+            // handle request
+            try {
+                handleTransaction(req, resp);
+            } catch (InterruptedIOException ignore) { // timeout
+                break;
+            } catch (IOException ioe) {
+                resp.sendError(500,
+                    "error processing request: " + ioe.getMessage());
+                break;
+            }
+            out.flush(); // flush response output
+            // consume any leftover body data so next request can be processed
+            req.consumeBody();
+            // persist or close connection as necessary
+            connectionHeader = req.getHeaders().get("Connection");
+        } while (!"close".equalsIgnoreCase(connectionHeader));
+    }
+    
+    /**
+     * Handles a single transaction on a connection.
+     * 
+     * @param req the transaction request
+     * @param resp the transaction response (into which the response is written)
+     * @throws IOException if and error occurs
+     */
+    protected void handleTransaction(Request req, Response resp)
+            throws IOException {
+        Headers reqHeaders = req.getHeaders();
+        // validate request
+        String version = req.getVersion();
+        if (version.equals("HTTP/1.1")) {
+            if (!reqHeaders.contains("Host")) {
+                // RFC2616#14.23: missing Host header gets 400
+                resp.sendError(400, "missing required Host header");
+                return;
+            }
+            // return a continue response before reading body
+            String expect = reqHeaders.get("Expect");
+            if (expect != null) {
+                if (expect.equalsIgnoreCase("100-continue")) {
+                    Response tempResp = new Response(resp.getBody());
+                    tempResp.sendHeaders(100);
+                } else {
+                    // RFC2616#14.20: if unknown expect, send 417
+                    resp.sendError(417);
+                    return;
+                }
+            }
+        } else if (version.equals("HTTP/1.0") || version.equals("HTTP/0.9")) {
+            // RFC2616#14.10 - remove connection headers from older versions
+            for (String token : splitElements(reqHeaders.get("Connection")))
+                reqHeaders.remove(token);
+        } else {
+            resp.sendError(400, "unknown version: " + version);
+            return;
+        }
+        
+        // process the methods
+        String method = req.getMethod();
+        if (method.equals("GET") || method.equals("POST")) {
+            serve(req, resp);
+        } else if (method.equals("HEAD")) { // process normally but discard body
+            resp.setDiscardBody(true);
+            serve(req, resp);
+        } else if (method.equals("OPTIONS")) {
+            resp.getHeaders().add("Allow", "GET, HEAD, POST, OPTIONS, TRACE");
+            resp.getHeaders().add("Content-Length", "0"); // RFC2616#9.2
+            resp.sendHeaders(200);
+        } else if (method.equals("TRACE")) {
+            handleTrace(req, resp);
+        } else {
+            resp.sendError(501, "unsupported method: " + method);
+        }
+    }
+
+    /**
+     * Handles a TRACE method request.
+     * 
+     * @param req the request
+     * @param resp the response into which the content is written
+     * @throws IOException if an error occurs
+     */
+    public void handleTrace(Request req, Response resp) throws IOException {
+        resp.getHeaders().add("Content-Type", "message/http");
+        resp.getHeaders().add("Transfer-Encoding", "chunked");
+        resp.sendHeaders(200);
+        ChunkedOutputStream out = new ChunkedOutputStream(resp.getBody());
+        ByteArrayOutputStream headout = new ByteArrayOutputStream();
+        String responseLine = "TRACE " + req.getURI() + " " + req.getVersion();
+        headout.write(responseLine.getBytes("ISO8859_1"));
+        headout.write(CRLF);
+        req.getHeaders().writeTo(headout);
+        byte[] b = headout.toByteArray();
+        out.writeChunk(b, 0, b.length);
+        b = new byte[4096];
+        int count;
+        InputStream in = req.getBody();
+        while ((count = in.read(b)) != -1)
+            out.writeChunk(b, 0, count);
+        out.writeTrailingChunk(null);
+    }
+    
+    /**
+     * Serves the content for a request, using the context handler for the
+     * requested context.
+     * 
+     * @param req the request
+     * @param resp the response into which the content is written
+     * @throws IOException if an error occurs
+     */
+    protected void serve(Request req, Response resp) throws IOException {
+        // get context handler to handle request
+        String path = req.getPath();
+        ContextHandler handler = req.getVirtualHost().getContext(path);
+        if (handler == null) {
+            resp.sendError(404);
+            return;
+        }
+        // serve request
+        int status = 404;
+        // add directory index if necessary
+        if (path.endsWith("/")) {
+            String index = req.getVirtualHost().getDirectoryIndex();
+            if (index != null) {
+                req.setPath(path + index);
+                status = handler.serve(req, resp);
+                req.setPath(path);
+            }
+        }
+        if (status == 404)
+            status = handler.serve(req, resp);
+        if (status > 0)
+            resp.sendError(status);
+    }
+    
+    /**
+     * Adds a Content-Type mapping for the given path suffixes.
+     * If any of the path suffixes had a previous Content-Type associated
+     * with it, it is replaced with the given one. Path suffixes are
+     * considered case-insensitive, and contentType is converted to lowercase.
+     * 
+     * @param contentType the content type (MIME type) to be associated with
+     *        the given path suffixes
+     * @param suffixes the path suffixes which will be associated with
+     *        the contentType, e.g. the file extensions of served files
+     *        (excluding the '.' character)
+     */
+    public static void addContentType(String contentType, String... suffixes) {
+        for (String suffix : suffixes)
+            contentTypes.put(suffix.toLowerCase(), contentType.toLowerCase());
+    }
+    
+    /**
+     * Returns the content type for the given path, according to its suffix,
+     * or the given default content type if none can be determined.
+     * 
+     * @param path the path whose content type is requested
+     * @param def a default content type which is returned if none can be
+     *        determined
+     * @return the content type for the given path, or the given default
+     */
+    public static String getContentType(String path, String def) {
+        int dot = path.lastIndexOf('.');
+        String type = dot < 0 ? def
+            : contentTypes.get(path.substring(dot + 1).toLowerCase());
+        return type != null ? type : def;
+    }
+    
+    /**
+     * Returns the local host's auto-detected name.
+     * 
+     * @return the local host name
+     */
+    public static String detectLocalHostName() {
+        try {
+            return InetAddress.getLocalHost().getCanonicalHostName();
+        } catch (UnknownHostException uhe) {
+            return "localhost";
+        }
+    }
+    
+    /**
+     * Parses name-value pair parameters from the given
+     * "x-www-form-urlencoded" string. This is the encoding used both for
+     * parameters passed as the query of a GET method, and as the content of
+     * forms submitted using the POST method (as long as they use the default
+     * "application/x-www-form-urlencoded" encoding in their ENCTYPE attribute).
+     * UTF-8 encoding is assumed.
+     * 
+     * @param s an "application/x-www-form-urlencoded" string
+     * @return the parameter name-value pairs parsed from the given string,
+     *         or an empty map if it does not contain any
+     */
+    public static Map<String, String> parseParams(String s) {
+        if (s == null || s.length() == 0)
+            return Collections.emptyMap();
+        Map<String, String> params = new LinkedHashMap<String, String>(8);
+        Scanner sc = new Scanner(s).useDelimiter("&");
+        while (sc.hasNext()) {
+            String pair = sc.next();
+            int pos = pair.indexOf('=');
+            String name = pos == -1 ? pair : pair.substring(0, pos);
+            String val = pos == -1 ? "" : pair.substring(pos + 1);
+            try {
+                name = URLDecoder.decode(name.trim(), "UTF-8");
+                val = URLDecoder.decode(val.trim(), "UTF-8");
+                if (name.length() > 0)
+                    params.put(name, val);
+            } catch (UnsupportedEncodingException ignore) {} // never thrown
+        }
+        return params;
+    }
+    
+    /**
+     * Returns the absolute (zero-based) content range value specified
+     * by the given range string. If multiple ranges are requested, a single
+     * range containing all of them is returned.
+     * 
+     * @param range the string containing the range description
+     * @param length the full length of the requested resource
+     * @return the requested range, or null if the range value is invalid
+     */
+    public static long[] parseRange(String range, long length) {
+        long min = Long.MAX_VALUE;
+        long max = Long.MIN_VALUE;
+        try {
+            for (String token : splitElements(range)) {
+                long start, end;
+                int dash = token.indexOf('-');
+                if (dash == 0) { // suffix range
+                    start = length - parseLong(token.substring(1), 10);
+                    end = length - 1;
+                } else if (dash == token.length() - 1) { // open range
+                    start = parseLong(token.substring(0, dash), 10);
+                    end = length - 1;
+                } else { // explicit range
+                    start = parseLong(token.substring(0, dash), 10);
+                    end = parseLong(token.substring(dash + 1), 10);
+                }
+                if (end < start)
+                    throw new RuntimeException();
+                if (start < min)
+                    min = start;
+                if (end > max)
+                    max = end;
+            }
+            if (max < 0) // no tokens
+                throw new RuntimeException();
+            if (max >= length && min < length)
+                max = length - 1;
+            return new long[] { min, max }; // start might be >= length!
+        } catch (RuntimeException re) { // NFE, IOOBE or explicit RE
+            return null; // RFC2616#14.35.1 - ignore header if invalid
+        }
+    }
+    
+    /**
+     * Parses an unsigned long value. This method behaves the same as calling
+     * {@link Long#parseLong(String, int)}, but considers the string invalid
+     * if it starts with an ASCII minus sign ('-').
+     * 
+     * @param s the String containing the long representation to be parsed
+     * @param radix the radix to be used while parsing s
+     * @return the long represented by s in the specified radix
+     * @throws NumberFormatException if the string does not contain a parsable
+     *         long, or it starts with an ASCII minus sign
+     */
+    public static long parseLong(String s, int radix)
+            throws NumberFormatException {
+        long size = Long.parseLong(s, radix); // throws NumberFormatException
+        if (s.charAt(0) == '-')
+            throw new NumberFormatException("invalid digit: '-'");
+        return size;
+    }
+    
+    /**
+     * Parses a date string in one of the supported {@link #DATE_PATTERNS}.
+     * 
+     * Received date header values must be in one of the following formats:
+     * Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
+     * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
+     * Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
+     * 
+     * @param time a string representation of a time value
+     * @return the parsed date value
+     * @throws IllegalArgumentException if the given string does not contain
+     *         a valid date format in any of the supported formats
+     */
+    public static Date parseDate(String time) {
+        for (String pattern : DATE_PATTERNS) {
+            try {
+                SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.US);
+                df.setTimeZone(TimeZone.getTimeZone("GMT"));
+                return df.parse(time);
+            } catch (ParseException ignore) {}
+        }
+        throw new IllegalArgumentException("invalid date format: " + time);
+    }
+    
+    /**
+     * Formats the given time value as a string in RFC 1123 format.
+     * 
+     * @param time the time in milliseconds since January 1, 1970, 00:00:00 GMT
+     * @return the given time value as a string in RFC 1123 format
+     */
+    public static String formatDate(long time) {
+        return String.format("%ta, %<td %<tb %<tY %<tT GMT", time);
+    }
+    
+    /**
+     * Splits the given element list string (comma-separated header value)
+     * into its constituent non-empty trimmed elements.
+     * (RFC2616#2.1: element lists are delimited by a comma and optional LWS,
+     * and empty elements are ignored).
+     * 
+     * @param list the element list string
+     * @return the non-empty elements in the list, or an empty array
+     */
+    public static String[] splitElements(String list) {
+        return split(list, ',');
+    }
+    
+    /**
+     * Splits the given string into its constituent non-empty trimmed elements,
+     * which are delimited by the given character. This is a more direct
+     * and efficient implementation than using a regex (e.g. String.split()).
+     * 
+     * @param str the string to split
+     * @param delim the character used as the delimiter between elements
+     * @return the non-empty elements in the string, or an empty array
+     */
+    public static String[] split(String str, char delim) {
+        if (str == null)
+            return new String[0];
+        Collection<String> elements = new ArrayList<String>();
+        int len = str.length();
+        int start = 0;
+        while (start < len) {
+            int end = str.indexOf(delim, start);
+            if (end == -1)
+                end = len; // last token is until end of string
+            String element = str.substring(start, end).trim();
+            if (element.length() > 0)
+                elements.add(element);
+            start = end + 1;
+        }   
+        return elements.toArray(new String[elements.size()]);
+    }
+    
+    /**
+     * Returns the parent of the given path.
+     * 
+     * @param path the path whose parent is returned (must start with '/')
+     * @return the parent of the given path (excluding trailing slash),
+     *         or null if given path is the root path
+     */
+    public static String getParentPath(String path) {
+        path = rtrim(path, '/'); // remove trailing slash
+        int slash = path.lastIndexOf('/');
+        return slash == -1 ? null : path.substring(0, slash);
+    }
+    
+    /**
+     * Trims occurrences of the given character at the end of the given string.
+     * 
+     * @param str the string to trim
+     * @param c the character to remove
+     * @return the given string without the given character at its end,
+     *         or the string itself if it does not end with the character
+     */
+    public static String rtrim(String str, char c) {
+        int len = str.length();
+        int end;
+        for (end = len; end > 0 && str.charAt(end - 1) == c; end--);
+        return end == len ? str : str.substring(0, end);
+    }
+    
+    /**
+     * Trims occurrences of the given character at the beginning of the given string.
+     * 
+     * @param str the string to trim
+     * @param c the character to remove
+     * @return the given string without the given character at its beginning,
+     *         or the string itself if it does not begin with the character
+     */
+    public static String ltrim(String str, char c) {
+        int len = str.length();
+        int start;
+        for (start = 0; start < len && str.charAt(start) == c; start++);
+        return start == 0 ? str : str.substring(start);
+    }
+    
+    /**
+     * Trims duplicate consecutive occurrences of the given character within the
+     * given string, replacing them with a single instance of the character.
+     * 
+     * @param s the string to trim
+     * @param c the character to trim
+     * @return the given string with duplicate consecutive occurrences of c
+     *         replaced by a single instance of c
+     */
+    public static String trimDuplicates(String s, char c) {
+        int i = -1;
+        while ((i = s.indexOf(c, i + 1)) > -1) {
+            int end;
+            for (end = i + 1; end < s.length() && s.charAt(end) == c; end++);
+            if (end > i + 1)
+                s = s.substring(0, i + 1) + s.substring(end);
+        }
+        return s;
+    }
+    
+    /**
+     * Returns a human-friendly string approximating the given data size,
+     * e.g. "316", "1.8K", "324M", etc.
+     * 
+     * @param size the size to display
+     * @return a human-friendly string approximating the given data size
+     */
+    public static String toSizeString(long size) {
+        final char[] units = { ' ', 'K', 'M', 'G', 'T', 'P' };
+        int u;
+        double s;
+        for (u = 0, s = size; s >= 1000; u++, s /= 1024);
+        return String.format(s < 10 ? "%.1f%c" : "%.0f%c", s, units[u]);
+    }
+    
+    /**
+     * Returns an HTML-escaped version of the given string for safe display
+     * within a web page. The characters '&', '>' and '<' must always be
+     * escaped, and single and double quotes must be escaped within
+     * attribute values; this method escapes them always. This method can
+     * be used for generating both HTML and XHTML valid content.
+     * 
+     * @param s the string to escape
+     * @return the escaped string
+     * @see <a href="http://www.w3.org/International/questions/qa-escapes">
+     *      The W3C FAQ</a>
+     */
+    public static String escapeHTML(String s) {
+        int len = s.length();
+        StringBuilder es = new StringBuilder(len + 30);
+        int start = 0;
+        for (int i = 0; i < len; i++) {
+            String ref = null;
+            switch (s.charAt(i)) {
+                case '&': ref = "&"; break;
+                case '>': ref = ">"; break;
+                case '<': ref = "<"; break;
+                case '"': ref = """; break;
+                case '\'': ref = "'"; break;
+            }
+            if (ref != null) {
+                es.append(s.substring(start, i)).append(ref);
+                start = i + 1;
+            }
+        }
+        return start == 0 ? s : es.append(s.substring(start)).toString();
+    }
+    
+    /**
+     * Transfers data from an input stream to an output stream.
+     * 
+     * @param in the input stream to transfer from
+     * @param out the output stream to transfer to
+     * @param len the number of bytes to transfer. If negative, the entire
+     *        contents of the input stream are transfered.
+     * @throws IOException if an IO error occurs or the input stream ends
+     *         before the requested number of bytes have been read
+     */
+    public static void transfer(InputStream in, OutputStream out, long len)
+            throws IOException {
+        byte[] buf = new byte[4096];
+        while (len != 0) {
+            int count = len < 0 || buf.length < len ? buf.length : (int)len;
+            count = in.read(buf, 0, count);
+            if (count == -1) {
+                if (len > 0)
+                    throw new IOException("unexpected end of stream");
+                break;
+            }
+            out.write(buf, 0, count);
+            len -= len > 0 ? count : 0;
+        }
+    }
+    
+    /**
+     * Reads the token starting at the current stream position and ending at
+     * the first occurrence of the given delimiter byte, in the given encoding.
+     * 
+     * @param in the stream from which the token is read
+     * @param delim the byte value which marks the end of the token,
+     *        or -1 if the token ends at the end of the stream
+     * @param enc a character-encoding name
+     * @param maxLength the maximum length (in bytes) to read
+     * @return the read token, excluding the delimiter
+     * @throws UnsupportedEncodingException if the encoding is not supported
+     * @throws IOException if an IO error occurs, or the stream end is
+     *         reached before the delimiter is found (and it is not -1),
+     *         or the maximum length is reached before the token end is reached
+     */
+    public static String readToken(InputStream in, int delim,
+            String enc, int maxLength) throws IOException {
+        // note: we avoid using a ByteArrayOutputStream here because it
+        // suffers the overhead of synchronization for each byte written
+        int buflen = maxLength < 512 ? maxLength : 512; // start with less
+        byte[] buf = new byte[buflen];
+        int count = 0;
+        int c;
+        while ((c = in.read()) != -1 && c != delim) {
+            if (count == buflen) { // expand buffer
+                if (count == maxLength)
+                    throw new IOException("token too large (" + count + ")");
+                buflen = maxLength < 2 * buflen ? maxLength : 2 * buflen;
+                byte[] expanded = new byte[buflen];
+                System.arraycopy(buf, 0, expanded, 0, count);
+                buf = expanded;
+            }
+            buf[count++] = (byte)c;
+        }        
+        if (c == -1 && delim != -1)
+            throw new IOException("unexpected end of stream");
+        return new String(buf, 0, count, enc);
+    }
+    
+    /**
+     * Reads the ISO-8859-1 encoded string starting at the current stream
+     * position and ending at the first occurrence of the LF character.
+     * 
+     * @param in the stream from which the line is read
+     * @return the read string, excluding the terminating LF character
+     *         and (if exists) the CR character immediately preceding it
+     * @throws IOException if an IO error occurs, or the stream end is
+     *         reached before an LF character is found, or the line is
+     *         longer than 8192 bytes
+     * @see #readToken(InputStream,int,String,int)
+     */
+    public static String readLine(InputStream in) throws IOException {
+        String s = readToken(in, '\n', "ISO8859_1", 8192);
+        return s.length() > 0 && s.charAt(s.length() - 1) == '\r'
+            ? s.substring(0, s.length() - 1) : s;
+    }
+    
+    /**
+     * Reads headers from the given stream. Headers are read according to the
+     * RFC, including folded headers, element lists, and multiple headers
+     * (which are concatenated into a single element list header).
+     * Leading and trailing whitespace is removed.
+     * 
+     * @param in the stream from which the headers are read
+     * @return the read headers (possibly empty, if none exist)
+     * @throws IOException if an IO error occurs or the headers are malformed
+     *         or there are more than 100 header lines
+     */
+    public static Headers readHeaders(InputStream in) throws IOException {
+        Headers headers = new Headers();
+        String line;
+        String prevLine = "";
+        int count = 0;
+        while ((line = readLine(in)).length() > 0) {
+            int first;
+            for (first = 0; first < line.length() &&
+                Character.isWhitespace(line.charAt(first)); first++);
+            if (first > 0) // unfold header continuation line
+                line = prevLine + ' ' + line.substring(first);
+            int separator = line.indexOf(':');
+            if (separator == -1)
+                throw new IOException("invalid header: \"" + line + "\"");
+            String name = line.substring(0, separator);
+            String value = line.substring(separator + 1).trim(); // ignore LWS
+            Header replaced = headers.replace(name, value);
+            // concatenate repeated headers (distinguishing repeat from fold)
+            if (replaced != null && first == 0) {
+                value = replaced.getValue() + ", " + value;
+                line = name + ": " + value;                    
+                headers.replace(name, value);
+            }
+            prevLine = line;
+            if (++count > 100)
+                throw new IOException("too many header lines");
+        }
+        return headers;
+    }
+    
+    /**
+     * Matches the given ETag value against the given ETags. A match is found
+     * if the given ETag is not null, and either the ETags contain a "*" value,
+     * or one of them is identical to the given ETag. If strong comparison is
+     * used, tags beginning with the weak ETag prefix "W/" never match.
+     * See RFC2616#3.11, RFC2616#13.3.3.
+     * 
+     * @param strong if true, strong comparison is used, otherwise weak
+     *        comparison is used
+     * @param etags the ETags to match against
+     * @param etag the ETag to match
+     * @return true if the ETag is matched, false otherwise
+     */
+    public static boolean match(boolean strong, String[] etags, String etag) {
+        if (etag == null || strong && etag.startsWith("W/"))
+            return false;
+        for (String e : etags)
+            if (e.equals("*") ||
+                    (e.equals(etag) && !(strong && (e.startsWith("W/")))))
+                return true;
+        return false;
+    }
+    
+    /**
+     * Calculates the appropriate response status for the given request and
+     * its resource's last-modified time and ETag, based on the conditional
+     * headers present in the request.
+     *  
+     * @param req the request
+     * @param lastModified the resource's last modified time
+     * @param etag the resource's ETag
+     * @return the appropriate response status for the request
+     */
+    public static int getConditionalStatus(Request req,
+            long lastModified, String etag) {
+        Headers headers = req.getHeaders();
+        // If-Match
+        String header = headers.get("If-Match");
+        if (header != null && !match(true, splitElements(header), etag))
+            return 412;
+        // If-Unmodified-Since
+        Date date = headers.getDate("If-Unmodified-Since");
+        if (date != null && lastModified > date.getTime())
+            return 412;
+        // If-Modified-Since
+        int status = 200;
+        boolean force = false;
+        date = headers.getDate("If-Modified-Since");
+        if (date != null && date.getTime() <= System.currentTimeMillis()) {
+            if (lastModified > date.getTime())
+                force = true;
+            else
+                status = 304;
+        }
+        // If-None-Match
+        header = headers.get("If-None-Match");
+        if (header != null) {
+            if (match(true, splitElements(header), etag))
+                status = req.getMethod().equals("GET")
+                    || req.getMethod().equals("HEAD") ? 304 : 412;
+            else
+                force = true;
+        }
+        return force ? 200 : status;
+    }
+    
+    /**
+     * Serves a context's contents from a file based resource.
+     * 
+     * The file is located by stripping the given context prefix from
+     * the request's path, and appending the result to the given base directory.
+     * 
+     * Missing, forbidden and otherwise invalid files return the appropriate
+     * error response. Directories are served as an HTML index page if the
+     * virtual host allows one, or a forbidden error otherwise. Files are
+     * sent with their corresponding content types, and handle conditional
+     * and partial retrievals according to the RFC.
+     * 
+     * @param base the base directory to which the context is mapped
+     * @param context the context which is mapped to the base directory
+     * @param req the request
+     * @param resp the response into which the content is written
+     * @return the HTTP status code to return, or 0 if a response was sent
+     * @throws IOException if an error occurs
+     */
+    public static int serveFile(File base, String context,
+            Request req, Response resp) throws IOException {
+        String relativePath = req.getPath().substring(context.length());
+        File file = new File(base, relativePath).getCanonicalFile();
+        if (!file.exists() || file.isHidden()) {
+            return 404;
+        } else if (!file.canRead()
+                   || !file.getPath().startsWith(base.getPath())) { // validate
+            return 403;
+        } else if (file.isDirectory()) {
+            if (relativePath.endsWith("/") || relativePath.length() == 0) {
+                if (!req.getVirtualHost().isAllowGeneratedIndex())
+                    return 403;
+                resp.send(200, createIndex(file, req.getPath()));
+            } else { // redirect to the normalized directory URL ending with '/'
+                resp.redirect(req.getBaseURL() + req.getPath() + "/", true);
+            }
+        } else {
+            serveFileContent(file, req, resp);
+        }
+        return 0;
+    }
+    
+    /**
+     * Serves the contents of a file, with its corresponding content types,
+     * last modification time, etc. conditional and partial retrievals are
+     * handled according to the RFC.
+     * 
+     * @param file the existing and readable file whose contents are served
+     * @param req the request
+     * @param resp the response into which the content is written
+     * @throws IOException if an error occurs
+     */
+    public static void serveFileContent(File file, Request req, Response resp)
+            throws IOException {
+        long len = file.length();
+        long lastModified = file.lastModified();
+        String etag = "W/\"" + lastModified + "\""; // a weak tag based on date
+        int status = 200;
+        // handle range or conditional request
+        long[] range = req.getRange(len);
+        if (range == null) {
+            status = getConditionalStatus(req, lastModified, etag);
+        } else {
+            String ifRange = req.getHeaders().get("If-Range");
+            if (ifRange == null) {
+                if (range[0] >= len)
+                    status = 416; // unsatisfiable range
+                else
+                    status = getConditionalStatus(req, lastModified, etag);
+            } else {
+                if (range[0] >= len) {
+                    // RFC2616#14.16,10.4.17: invalid If-Range gets everything
+                    range = null;
+                } else { // send either range or everything
+                    if (!ifRange.startsWith("\"")
+                            && !ifRange.startsWith("W/")) {
+                        Date date = req.getHeaders().getDate("If-Range");
+                        if (date != null && lastModified > date.getTime())
+                            range = null; // modified - send everything
+                    } else if (!ifRange.equals(etag)) {
+                        range = null; // modified - send everything 
+                    }
+                }
+            }
+        }
+        // send the response
+        Headers respHeaders = resp.getHeaders();
+        switch (status) {
+            case 304: // no other headers or body allowed
+                respHeaders.add("ETag", etag);
+                respHeaders.add("Last-Modified", formatDate(lastModified));
+                resp.sendHeaders(304);
+                return;
+            case 412:
+                resp.sendHeaders(412);
+                return;
+            case 416:
+                respHeaders.add("Content-Range", "bytes */" + len);
+                resp.sendHeaders(416);
+                return;
+            case 200:
+                // send OK response
+                resp.sendHeaders(200, len, lastModified, etag,
+                    getContentType(file.getName(), "application/octet-stream"),
+                    range);
+                // send body
+                FileInputStream fis = new FileInputStream(file);
+                try {
+                    resp.sendBody(fis, len, range);
+                } finally {
+                    fis.close();
+                }
+                return;
+            default:
+                resp.sendHeaders(500); // should never happen
+                return;
+        }
+    }
+    
+    /**
+     * Serves the contents of a directory as an HTML file index.
+     * 
+     * @param dir the existing and readable directory whose contents are served
+     * @param path the displayed base path corresponding to dir
+     * @return an HTML string containing the file index for the directory
+     * @throws IOException if an error occurs
+     */
+    public static String createIndex(File dir, String path) throws IOException {
+        if (!path.endsWith("/"))
+            path += "/";
+        // calculate name column width
+        int w = 21; // minimum width
+        for (String name : dir.list())
+            if (name.length() > w)
+                w = name.length();
+        w += 2; // with room for added slash and space
+        // note: we use apache's format, for consistent user experience
+        Formatter f = new Formatter(Locale.US);
+        f.format("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">%n" +
+            "<html><head><title>Index of %s</title></head>%n" +
+            "<body><h1>Index of %s</h1>%n" +
+            "<pre> Name%" + (w - 5) + "s Last modified      Size<hr>",
+            path, path, "");
+        if (path.length() > 1) // add parent link if not root path
+            f.format(" <a href=\"%s/\">Parent Directory</a>%"
+                + (w + 5) + "s-%n", getParentPath(path), "");
+        for (File file : dir.listFiles()) {
+            try {
+                String name = file.getName() + (file.isDirectory() ? "/" : "");
+                String size = file.isDirectory() ? "- "
+                                                 : toSizeString(file.length());
+                // properly url-encode the link
+                String link = new URI(null, path + name, null).toASCIIString(); 
+                f.format(" <a href=\"%s\">%s</a>%-" + (w - name.length()) +
+                    "s‎%td-%<tb-%<tY %<tR%6s%n",
+                    link, name, "", file.lastModified(), size);
+            } catch (URISyntaxException ignore) {}
+        }
+        f.format("</pre></body></html>");
+        return f.toString();
+    }
+    
+    /**
+     * Starts a stand-alone HTTP server, serving files from disk.
+     * 
+     * @param args the command line arguments
+     */
+    public static void main(String[] args) {
+        try {
+            if (args.length == 0) {
+                System.err.printf("Usage: %s <directory> [port]%n",
+                    HTTPServer.class.getName());
+                return;
+            }
+            File dir = new File(args[0]);
+            if (!dir.canRead()) {
+                System.err.println("error opening " + dir.getAbsolutePath());
+                return;
+            }
+            int port = args.length < 2 ? 80 : Integer.parseInt(args[1]);
+            HTTPServer server = new HTTPServer(port);
+            VirtualHost host = server.getVirtualHost(null); // default host
+            host.setAllowGeneratedIndex(true);
+            host.addContext("/", new FileContextHandler(dir, "/"));
+            server.start();
+        } catch (Exception e) {
+            System.err.println("error: " + e.getMessage());
+        }
+    }
+}
diff --git a/alien/src/net/pterodactylus/fcp/ARK.java b/alien/src/net/pterodactylus/fcp/ARK.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ARK.java
@@ -0,0 +1,99 @@
+/*
+ * jFCPlib - ARK.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Container for ARKs (address resolution keys).
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ARK {
+
+	/** The public URI of the ARK. */
+	private final String publicURI;
+
+	/** The private URI of the ARK. */
+	private final String privateURI;
+
+	/** The number of the ARK. */
+	private final int number;
+
+	/**
+	 * Creates a new ARK with the given URI and number.
+	 *
+	 * @param publicURI
+	 *            The public URI of the ARK
+	 * @param number
+	 *            The number of the ARK
+	 */
+	public ARK(String publicURI, String number) {
+		this(publicURI, null, number);
+	}
+
+	/**
+	 * Creates a new ARK with the given URIs and number.
+	 *
+	 * @param publicURI
+	 *            The public URI of the ARK
+	 * @param privateURI
+	 *            The private URI of the ARK
+	 * @param number
+	 *            The number of the ARK
+	 */
+	public ARK(String publicURI, String privateURI, String number) {
+		if ((publicURI == null) || (number == null)) {
+			throw new NullPointerException(((publicURI == null) ? "publicURI" : "number") + " must not be null");
+		}
+		this.publicURI = publicURI;
+		this.privateURI = privateURI;
+		try {
+			this.number = Integer.valueOf(number);
+		} catch (NumberFormatException nfe1) {
+			throw new IllegalArgumentException("number must be numeric", nfe1);
+		}
+	}
+
+	/**
+	 * Returns the public URI of the ARK.
+	 *
+	 * @return The public URI of the ARK
+	 */
+	public String getPublicURI() {
+		return publicURI;
+	}
+
+	/**
+	 * Returns the private URI of the ARK.
+	 *
+	 * @return The private URI of the ARK
+	 */
+	public String getPrivateURI() {
+		return privateURI;
+	}
+
+	/**
+	 * Returns the number of the ARK.
+	 *
+	 * @return The number of the ARK
+	 */
+	public int getNumber() {
+		return number;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/AbstractSendFeedMessage.java b/alien/src/net/pterodactylus/fcp/AbstractSendFeedMessage.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/AbstractSendFeedMessage.java
@@ -0,0 +1,44 @@
+/*
+ * jFCPlib - AbstractSendFeedMessage.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Abstract base implementation for the {@code Send*Feed} commands.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public abstract class AbstractSendFeedMessage extends FcpMessage {
+
+	/**
+	 * Creates a new “Send*Feed” command.
+	 *
+	 * @param name
+	 *            The name of the command
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param nodeIdentifier
+	 *            The identifier of the peer node
+	 */
+	protected AbstractSendFeedMessage(String name, String identifier, String nodeIdentifier) {
+		super(name);
+		setField("Identifier", identifier);
+		setField("NodeIdentifier", nodeIdentifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/AddPeer.java b/alien/src/net/pterodactylus/fcp/AddPeer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/AddPeer.java
@@ -0,0 +1,101 @@
+/*
+ * jFCPlib - AddPeer.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.net.URL;
+
+/**
+ * The “AddPeer” request adds a peer to the node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class AddPeer extends FcpMessage {
+
+	/**
+	 * Creates a new “AddPeer” request.
+	 */
+	private AddPeer() {
+		super("AddPeer");
+	}
+
+	/**
+	 * Creates a new “AddPeer” request that reads the noderef of the peer from
+	 * the given file.
+	 *
+	 * @param file
+	 *            The file to read the noderef from
+	 */
+	public AddPeer(String file) {
+		this();
+		setField("File", file);
+	}
+
+	/**
+	 * Creates a new “AddPeer” request that reads the noderef of the peer from
+	 * the given URL.
+	 *
+	 * @param url
+	 *            The URL to read the noderef from
+	 */
+	public AddPeer(URL url) {
+		this();
+		setField("URL", String.valueOf(url));
+	}
+
+	/**
+	 * Creates a new “AddPeer” request that adds the peer given by the noderef.
+	 *
+	 * @param nodeRef
+	 *            The noderef of the peer
+	 */
+	public AddPeer(NodeRef nodeRef) {
+		this();
+		setNodeRef(nodeRef);
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Sets the noderef of the peer to add.
+	 *
+	 * @param nodeRef
+	 *            The noderef of the peer
+	 */
+	private void setNodeRef(NodeRef nodeRef) {
+		setField("lastGoodVersion", nodeRef.getLastGoodVersion().toString());
+		setField("opennet", String.valueOf(nodeRef.isOpennet()));
+		setField("identity", nodeRef.getIdentity());
+		setField("myName", nodeRef.getMyName());
+		setField("location", String.valueOf(nodeRef.getLocation()));
+		setField("testnet", String.valueOf(nodeRef.isTestnet()));
+		setField("version", String.valueOf(nodeRef.getVersion()));
+		setField("physical.udp", nodeRef.getPhysicalUDP());
+		setField("ark.pubURI", nodeRef.getARK().getPublicURI());
+		setField("ark.number", String.valueOf(nodeRef.getARK().getNumber()));
+		setField("dsaPubKey.y", nodeRef.getDSAPublicKey());
+		setField("dsaGroup.g", nodeRef.getDSAGroup().getBase());
+		setField("dsaGroup.p", nodeRef.getDSAGroup().getPrime());
+		setField("dsaGroup.q", nodeRef.getDSAGroup().getSubprime());
+		setField("auth.negTypes", FcpUtils.encodeMultiIntegerField(nodeRef.getNegotiationTypes()));
+		setField("sig", nodeRef.getSignature());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/AllData.java b/alien/src/net/pterodactylus/fcp/AllData.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/AllData.java
@@ -0,0 +1,102 @@
+/*
+ * jFCPlib - AllData.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.InputStream;
+
+/**
+ * The “AllData” message carries the payload of a successful {@link ClientGet}
+ * request. You will only received this message if the {@link ClientGet} request
+ * was started with a return type of {@link ReturnType#direct}. If you get this
+ * message and decide that the data is for you, call
+ * {@link #getPayloadInputStream()} to get the data. If an AllData message
+ * passes through all registered {@link FcpListener}s without the payload being
+ * consumed, the payload is discarded!
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class AllData extends BaseMessage {
+
+	/** The payload. */
+	private InputStream payloadInputStream;
+
+	/**
+	 * Creates an “AllData” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 * @param payloadInputStream
+	 *            The payload
+	 */
+	AllData(FcpMessage receivedMessage, InputStream payloadInputStream) {
+		super(receivedMessage);
+		this.payloadInputStream = payloadInputStream;
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the length of the data.
+	 *
+	 * @return The length of the data, or <code>-1</code> if the length could
+	 *         not be parsed
+	 */
+	public long getDataLength() {
+		return FcpUtils.safeParseLong(getField("DataLength"));
+	}
+
+	/**
+	 * Returns the startup time of the request.
+	 *
+	 * @return The startup time of the request (in milliseconds since Jan 1,
+	 *         1970 UTC), or <code>-1</code> if the time could not be parsed
+	 */
+	public long getStartupTime() {
+		return FcpUtils.safeParseLong(getField("StartupTime"));
+	}
+
+	/**
+	 * Returns the completion time of the request.
+	 *
+	 * @return The completion time of the request (in milliseconds since Jan 1,
+	 *         1970 UTC), or <code>-1</code> if the time could not be parsed
+	 */
+	public long getCompletionTime() {
+		return FcpUtils.safeParseLong(getField("CompletionTime"));
+	}
+
+	/**
+	 * Returns the payload input stream. You <strong>have</strong> consume the
+	 * input stream before returning from the
+	 * {@link FcpListener#receivedAllData(FcpConnection, AllData)} method!
+	 *
+	 * @return The payload
+	 */
+	public InputStream getPayloadInputStream() {
+		return payloadInputStream;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/BaseMessage.java b/alien/src/net/pterodactylus/fcp/BaseMessage.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/BaseMessage.java
@@ -0,0 +1,74 @@
+/*
+ * jFCPlib - BaseMessage.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.Map;
+
+/**
+ * A basic message abstraction that wraps a received FCP message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class BaseMessage {
+
+	/** The received message, wrapped here. */
+	private final FcpMessage receivedMessage;
+
+	/**
+	 * Creates a new base message that wraps the given message.
+	 *
+	 * @param receivedMessage
+	 *            The FCP message that was received
+	 */
+	BaseMessage(FcpMessage receivedMessage) {
+		this.receivedMessage = receivedMessage;
+	}
+
+	/**
+	 * Returns the name of the message.
+	 *
+	 * @return The name of the message
+	 */
+	public String getName() {
+		return receivedMessage.getName();
+	}
+
+	/**
+	 * Returns the content of the field.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @return The content of the field, or <code>null</code> if there is no
+	 *         such field
+	 */
+	protected String getField(String field) {
+		return receivedMessage.getField(field);
+	}
+
+	/**
+	 * Returns all fields from the received message.
+	 *
+	 * @see FcpMessage#getFields()
+	 * @return All fields from the message
+	 */
+	protected Map<String, String> getFields() {
+		return receivedMessage.getFields();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ClientGet.java b/alien/src/net/pterodactylus/fcp/ClientGet.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ClientGet.java
@@ -0,0 +1,210 @@
+/*
+ * jFCPlib - ClientGet.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ClientGet” request is used for download files from the Freenet node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ClientGet extends FcpMessage {
+
+	/**
+	 * Creates a new “ClientGet” request.
+	 *
+	 * @param uri
+	 *            The URI to get
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public ClientGet(String uri, String identifier) {
+		this(uri, identifier, ReturnType.direct);
+	}
+
+	/**
+	 * Creates a new “ClientGet” request.
+	 *
+	 * @param uri
+	 *            The URI to get
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param returnType
+	 *            The return type of the request
+	 */
+	public ClientGet(String uri, String identifier, ReturnType returnType) {
+		super("ClientGet");
+		setField("URI", uri);
+		setField("Identifier", identifier);
+		setField("ReturnType", String.valueOf(returnType));
+	}
+
+	/**
+	 * Sets whether the local data store should be ignored when searching for a
+	 * key.
+	 *
+	 * @param ignoreDataStore
+	 *            <code>true</code> to ignore the local data store,
+	 *            <code>false</code> to include it
+	 */
+	public void setIgnoreDataStore(boolean ignoreDataStore) {
+		setField("IgnoreDS", String.valueOf(ignoreDataStore));
+	}
+
+	/**
+	 * Sets whether the search for the key should be restricted to the local
+	 * data store only.
+	 *
+	 * @param dsOnly
+	 *            <code>true</code> to restrict the search to the local data
+	 *            store, <code>false</code> to search on other nodes, too
+	 */
+	public void setDataStoreOnly(boolean dsOnly) {
+		setField("DSonly", String.valueOf(dsOnly));
+	}
+
+	/**
+	 * Sets the verbosity of the request.
+	 *
+	 * @param verbosity
+	 *            The verbosity of the request
+	 */
+	public void setVerbosity(Verbosity verbosity) {
+		setField("Verbosity", String.valueOf(verbosity));
+	}
+
+	/**
+	 * Sets the maximum size of the file to retrieve. If the file is larger than
+	 * this size the request will fail!
+	 *
+	 * @param maxSize
+	 *            The maximum size of the file to retrieve
+	 */
+	public void setMaxSize(long maxSize) {
+		setField("MaxSize", String.valueOf(maxSize));
+	}
+
+	/**
+	 * Sets the maximum size of temporary files created by the node. If a
+	 * temporary file is larger than this size the request will fail!
+	 *
+	 * @param maxTempSize
+	 *            The maximum size of temporary files
+	 */
+	public void setMaxTempSize(long maxTempSize) {
+		setField("MaxTempSize", String.valueOf(maxTempSize));
+	}
+
+	/**
+	 * The maximum number of retries in case a block can not be retrieved.
+	 *
+	 * @param maxRetries
+	 *            The maximum number of retries for failed blocks,
+	 *            <code>-1</code> to try forever
+	 */
+	public void setMaxRetries(int maxRetries) {
+		setField("MaxRetries", String.valueOf(maxRetries));
+	}
+
+	/**
+	 * Sets the priority of the request.
+	 *
+	 * @param priority
+	 *            The priority of the request
+	 */
+	public void setPriority(Priority priority) {
+		setField("PriorityClass", String.valueOf(priority));
+	}
+
+	/**
+	 * Sets the persistence of the request.
+	 *
+	 * @param persistence
+	 *            The persistence of the request
+	 */
+	public void setPersistence(Persistence persistence) {
+		setField("Persistence", String.valueOf(persistence));
+	}
+
+	/**
+	 * Sets the client token of the request.
+	 *
+	 * @param clientToken
+	 *            The client token of the request
+	 */
+	public void setClientToken(String clientToken) {
+		setField("ClientToken", clientToken);
+	}
+
+	/**
+	 * Sets whether the request should be visible on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> to make the request visible on the global
+	 *            queue, <code>false</code> for client-local queue only
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Sets whether to request the “binary blob” for a key.
+	 *
+	 * @param binaryBlob
+	 *            <code>true</code> to request the binary blob,
+	 *            <code>false</code> to get the “real thing”
+	 */
+	public void setBinaryBlob(boolean binaryBlob) {
+		setField("BinaryBlob", String.valueOf(binaryBlob));
+	}
+
+	/**
+	 * Sets the allowed MIME types of the requested file. If the MIME type of
+	 * the file does not match one of the given MIME types the request will
+	 * fail!
+	 *
+	 * @param allowedMimeTypes
+	 *            The allowed MIME types
+	 */
+	public void setAllowedMimeTypes(String... allowedMimeTypes) {
+		setField("AllowedMIMETypes", FcpUtils.encodeMultiStringField(allowedMimeTypes));
+	}
+
+	/**
+	 * Sets the filename to download the file to. You should only call this
+	 * method if your return type is {@link ReturnType#disk}!
+	 *
+	 * @param filename
+	 *            The filename to download the file to
+	 */
+	public void setFilename(String filename) {
+		setField("Filename", filename);
+	}
+
+	/**
+	 * Sets the name for the temporary file. You should only call this method if
+	 * your return type is {@link ReturnType#disk}!
+	 *
+	 * @param tempFilename
+	 *            The name of the temporary file
+	 */
+	public void setTempFilename(String tempFilename) {
+		setField("TempFilename", tempFilename);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ClientHello.java b/alien/src/net/pterodactylus/fcp/ClientHello.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ClientHello.java
@@ -0,0 +1,58 @@
+/*
+ * jFCPlib - ClientHello.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ClientHello” message that <i>must</i> be sent to the node first thing
+ * after calling {@link FcpConnection#connect()}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ClientHello extends FcpMessage {
+
+	/**
+	 * Creates a new “ClientHello” message with the given client name. The
+	 * client name has to be unique to the node otherwise you will get a
+	 * {@link CloseConnectionDuplicateClientName} response from the node!
+	 *
+	 * @param clientName
+	 *            The unique client name
+	 */
+	public ClientHello(String clientName) {
+		this(clientName, "2.0");
+	}
+
+	/**
+	 * Creates a new “ClientHello” message with the given client name. The
+	 * client name has to be unique to the node otherwise you will get a
+	 * {@link CloseConnectionDuplicateClientName} response from the node! The
+	 * expected FCP version is currently ignored by the node.
+	 *
+	 * @param clientName
+	 *            The unique client name
+	 * @param expectedVersion
+	 *            The FCP version that the node is expected to talk
+	 */
+	public ClientHello(String clientName, String expectedVersion) {
+		super("ClientHello");
+		setField("Name", clientName);
+		setField("ExpectedVersion", expectedVersion);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ClientPut.java b/alien/src/net/pterodactylus/fcp/ClientPut.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ClientPut.java
@@ -0,0 +1,262 @@
+/*
+ * jFCPlib - ClientPut.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ClientPut” requests inserts a single file into freenet, either uploading
+ * it directly with this messge ({@link UploadFrom#direct}), uploading it from
+ * disk ({@link UploadFrom#disk}) or by creating a redirect to another URI (
+ * {@link UploadFrom#redirect}).
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ClientPut extends FcpMessage {
+
+	/**
+	 * Creates a new “ClientPut” message that inserts a file to the given URI.
+	 * The file data <em>has</em> to be supplied to this message using
+	 * {@link #setPayloadInputStream(java.io.InputStream)}! Using this
+	 * constructor is the same as using
+	 * {@link #ClientPut(String, String, UploadFrom)} with
+	 * {@link UploadFrom#direct} as third parameter.
+	 *
+	 * @param uri
+	 *            The URI to insert the file to
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public ClientPut(String uri, String identifier) {
+		this(uri, identifier, UploadFrom.direct);
+	}
+
+	/**
+	 * Creates a new “ClientPut” message that inserts a file to the given URI.
+	 * Depending on <code>uploadFrom</code> the file data has to be supplied in
+	 * different ways: If <code>uploadFrom</code> is {@link UploadFrom#direct},
+	 * use {@link #setPayloadInputStream(java.io.InputStream)} to supply the
+	 * input data. If <code>uploadFrom</code> is {@link UploadFrom#disk}, use
+	 * {@link #setFilename(String)} to supply the file to upload. You have to
+	 * test your direct-disk access (see {@link TestDDARequest},
+	 * {@link TestDDAReply}, {@link TestDDAResponse}, {@link TestDDAComplete})
+	 * before using this option! If <code>uploadFrom</code> is
+	 * {@link UploadFrom#redirect}, use {@link #setTargetURI(String)} to set the
+	 * target URI of the redirect.
+	 *
+	 * @param uri
+	 *            The URI to insert to
+	 * @param identifier
+	 *            The identifier of the insert
+	 * @param uploadFrom
+	 *            The source of the upload
+	 */
+	public ClientPut(String uri, String identifier, UploadFrom uploadFrom) {
+		super("ClientPut");
+		setField("URI", uri);
+		setField("Identifier", identifier);
+		setField("UploadFrom", String.valueOf(uploadFrom));
+	}
+
+	/**
+	 * The MIME type of the content.
+	 *
+	 * @param metadataContentType
+	 *            The MIME type of the content
+	 */
+	public void setMetadataContentType(String metadataContentType) {
+		setField("Metadata.ContentType", metadataContentType);
+	}
+
+	/**
+	 * The verbosity of the request. Depending on this parameter you will
+	 * received only the bare minimum of messages for the request (i.e. “it
+	 * completed”) or a whole lot more.
+	 *
+	 * @see Verbosity
+	 * @param verbosity
+	 *            The verbosity of the request
+	 */
+	public void setVerbosity(Verbosity verbosity) {
+		setField("Verbosity", String.valueOf(verbosity));
+	}
+
+	/**
+	 * The number of retries for a request if the initial try failed.
+	 *
+	 * @param maxRetries
+	 *            The maximum number of retries after failure, or
+	 *            <code>-1</code> to retry forever.
+	 */
+	public void setMaxRetries(int maxRetries) {
+		setField("MaxRetries", String.valueOf(maxRetries));
+	}
+
+	/**
+	 * Sets the priority of the request.
+	 *
+	 * @param priority
+	 *            The priority of the request
+	 */
+	public void setPriority(Priority priority) {
+		setField("PriorityClass", String.valueOf(priority));
+	}
+
+	/**
+	 * Determines whether the node should really insert the data or generate the
+	 * final CHK only.
+	 *
+	 * @param getCHKOnly
+	 *            <code>true</code> to generate the final CHK only,
+	 *            <code>false</code> to really insert the data
+	 */
+	public void setGetCHKOnly(boolean getCHKOnly) {
+		setField("GetCHKOnly", String.valueOf(getCHKOnly));
+	}
+
+	/**
+	 * Sets whether an insert request should be forked when it is cached.
+	 *
+	 * @param forkOnCacheable
+	 *            {@code true} to fork the insert when it is cached, {@code
+	 *            false} otherwise
+	 */
+	public void setForkOnCacheable(boolean forkOnCacheable) {
+		setField("ForkOnCacheable", String.valueOf(forkOnCacheable));
+	}
+
+	/**
+	 * Sets the number of additional inserts of single blocks.
+	 *
+	 * @param extraInsertsSingleBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSingleBlock(int extraInsertsSingleBlock) {
+		setField("ExtraInsertsSingleBlock", String.valueOf(extraInsertsSingleBlock));
+	}
+
+	/**
+	 * Sets the number of additional inserts of splitfile header blocks.
+	 *
+	 * @param extraInsertsSplitfileHeaderBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSplitfileHeaderBlock(int extraInsertsSplitfileHeaderBlock) {
+		setField("ExtraInsertsSplitfileHeaderBlock", String.valueOf(extraInsertsSplitfileHeaderBlock));
+	}
+
+	/**
+	 * Determines whether this request appears on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> to put the request on the global queue,
+	 *            <code>false</code> for the client-local queue.
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Determines whether the node should skip compression because the file has
+	 * already been compressed.
+	 *
+	 * @param dontCompress
+	 *            <code>true</code> to skip compression of the data in the node,
+	 *            <code>false</code> to allow compression
+	 */
+	public void setDontCompress(boolean dontCompress) {
+		setField("DontCompress", String.valueOf(dontCompress));
+	}
+
+	/**
+	 * Sets an optional client token. This client token is mentioned in progress
+	 * and other request-related messages and can be used to identify this
+	 * request.
+	 *
+	 * @param clientToken
+	 *            The client token
+	 */
+	public void setClientToken(String clientToken) {
+		setField("ClientToken", clientToken);
+	}
+
+	/**
+	 * Sets the persistence of this request.
+	 *
+	 * @param persistence
+	 *            The persistence of this request
+	 */
+	public void setPersistence(Persistence persistence) {
+		setField("Persistence", String.valueOf(persistence));
+	}
+
+	/**
+	 * Sets the target filename of the inserted file. This value is ignored for
+	 * all inserts that do not have “CHK@” as a target.
+	 *
+	 * @param targetFilename
+	 *            The filename of the target
+	 */
+	public void setTargetFilename(String targetFilename) {
+		setField("TargetFilename", targetFilename);
+	}
+
+	/**
+	 * Determines whether to encode the complete file early in the life of the
+	 * request.
+	 *
+	 * @param earlyEncode
+	 *            <code>true</code> to generate the final key long before the
+	 *            file is completely fetchable
+	 */
+	public void setEarlyEncode(boolean earlyEncode) {
+		setField("EarlyEncode", String.valueOf(earlyEncode));
+	}
+
+	/**
+	 * Sets the length of the data that will be transferred after this message
+	 * if <code>uploadFrom</code> is {@link UploadFrom#direct} is used.
+	 *
+	 * @param dataLength
+	 *            The length of the data
+	 */
+	public void setDataLength(long dataLength) {
+		setField("DataLength", String.valueOf(dataLength));
+	}
+
+	/**
+	 * Sets the name of the file to upload the data from.
+	 *
+	 * @param filename
+	 *            The filename to upload
+	 */
+	public void setFilename(String filename) {
+		setField("Filename", filename);
+	}
+
+	/**
+	 * If <code>uploadFrom</code> is {@link UploadFrom#redirect}, use this
+	 * method to determine that target of the redirect.
+	 *
+	 * @param targetURI
+	 *            The target URI to redirect to
+	 */
+	public void setTargetURI(String targetURI) {
+		setField("TargetURI", targetURI);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ClientPutComplexDir.java b/alien/src/net/pterodactylus/fcp/ClientPutComplexDir.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ClientPutComplexDir.java
@@ -0,0 +1,251 @@
+/*
+ * jFCPlib - ClientPutComplexDir.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import net.pterodactylus.fcp.FileEntry.DirectFileEntry;
+
+/**
+ * The “ClientPutComplexDir” lets you upload a directory with different sources
+ * for each file.
+ *
+ * @see FileEntry
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ClientPutComplexDir extends FcpMessage {
+
+	/** The index for added file entries. */
+	private int fileIndex = 0;
+
+	/** The input streams from {@link DirectFileEntry}s. */
+	private final List<InputStream> directFileInputStreams = new ArrayList<InputStream>();
+
+	/**
+	 * Creates a new “ClientPutComplexDir” with the given identifier and URI.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param uri
+	 *            The URI to insert the directory to
+	 */
+	public ClientPutComplexDir(String identifier, String uri) {
+		super("ClientPutComplexDir");
+		setField("Identifier", identifier);
+		setField("URI", uri);
+	}
+
+	/**
+	 * Sets the verbosity of the request.
+	 *
+	 * @param verbosity
+	 *            The verbosity of the request
+	 */
+	public void setVerbosity(Verbosity verbosity) {
+		setField("Verbosity", String.valueOf(verbosity));
+	}
+
+	/**
+	 * Sets the maximum number of retries for failed blocks.
+	 *
+	 * @param maxRetries
+	 *            The maximum number of retries for failed blocks, or
+	 *            <code>-1</code> to retry endlessly
+	 */
+	public void setMaxRetries(int maxRetries) {
+		setField("MaxRetries", String.valueOf(maxRetries));
+	}
+
+	/**
+	 * Sets the priority of the request.
+	 *
+	 * @param priority
+	 *            The priority of the request
+	 */
+	public void setPriority(Priority priority) {
+		setField("PriorityClass", String.valueOf(priority));
+	}
+
+	/**
+	 * Sets whether to generate the final URI only.
+	 *
+	 * @param getCHKOnly
+	 *            <code>true</code> to generate the final CHK only,
+	 *            <code>false</code> to complete the insert
+	 */
+	public void setGetCHKOnly(boolean getCHKOnly) {
+		setField("GetCHKOnly", String.valueOf(getCHKOnly));
+	}
+
+	/**
+	 * Sets whether an insert request should be forked when it is cached.
+	 *
+	 * @param forkOnCacheable
+	 *            {@code true} to fork the insert when it is cached, {@code
+	 *            false} otherwise
+	 */
+	public void setForkOnCacheable(boolean forkOnCacheable) {
+		setField("ForkOnCacheable", String.valueOf(forkOnCacheable));
+	}
+
+	/**
+	 * Sets the number of additional inserts of single blocks.
+	 *
+	 * @param extraInsertsSingleBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSingleBlock(int extraInsertsSingleBlock) {
+		setField("ExtraInsertsSingleBlock", String.valueOf(extraInsertsSingleBlock));
+	}
+
+	/**
+	 * Sets the number of additional inserts of splitfile header blocks.
+	 *
+	 * @param extraInsertsSplitfileHeaderBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSplitfileHeaderBlock(int extraInsertsSplitfileHeaderBlock) {
+		setField("ExtraInsertsSplitfileHeaderBlock", String.valueOf(extraInsertsSplitfileHeaderBlock));
+	}
+
+	/**
+	 * Sets whether the request is on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> to put the request on the global queue,
+	 *            <code>false</code> to put it on the client-local queue
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Sets whether the node should not try to compress the data.
+	 *
+	 * @param dontCompress
+	 *            <code>true</code> to skip compression of the data,
+	 *            <code>false</code> to try and compress the data
+	 */
+	public void setDontCompress(boolean dontCompress) {
+		setField("DontCompress", String.valueOf(dontCompress));
+	}
+
+	/**
+	 * Sets the client token of the request.
+	 *
+	 * @param clientToken
+	 *            The client token of the request
+	 */
+	public void setClientToken(String clientToken) {
+		setField("ClientToken", clientToken);
+	}
+
+	/**
+	 * Sets the persistence of the request.
+	 *
+	 * @param persistence
+	 *            The persistence of the request
+	 */
+	public void setPersistence(Persistence persistence) {
+		setField("Persistence", String.valueOf(persistence));
+	}
+
+	/**
+	 * Sets the target filename of the request. This is useful for inserts that
+	 * go to “CHK@” only and creates a manifest with a single file.
+	 *
+	 * @param targetFilename
+	 *            The target filename
+	 */
+	public void setTargetFilename(String targetFilename) {
+		setField("TargetFilename", targetFilename);
+	}
+
+	/**
+	 * Sets whether to encode the complete data early to generate the
+	 * {@link URIGenerated} message early.
+	 *
+	 * @param earlyEncode
+	 *            <code>true</code> to encode the complete data early,
+	 *            <code>false</code> otherwise
+	 */
+	public void setEarlyEncode(boolean earlyEncode) {
+		setField("EarlyEncode", String.valueOf(earlyEncode));
+	}
+
+	/**
+	 * Sets the default name. This is the name of the file that should be shown
+	 * if no file was specified.
+	 *
+	 * @param defaultName
+	 *            The default name
+	 */
+	public void setDefaultName(String defaultName) {
+		setField("DefaultName", defaultName);
+	}
+
+	/**
+	 * Adds an entry for a file.
+	 *
+	 * @param fileEntry
+	 *            The file entry to add
+	 */
+	public void addFileEntry(FileEntry fileEntry) {
+		Map<String, String> fields = fileEntry.getFields();
+		for (Entry<String, String> fieldEntry : fields.entrySet()) {
+			setField("Files." + fileIndex + "." + fieldEntry.getKey(), fieldEntry.getValue());
+		}
+		fileIndex++;
+		if (fileEntry instanceof FileEntry.DirectFileEntry) {
+			directFileInputStreams.add(((DirectFileEntry) fileEntry).getInputStream());
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Do not call this method to add input streams! The input streams, if any,
+	 * will be taken directly from the {@link FileEntry}s and the stream you set
+	 * here will be overridden!
+	 */
+	@Override
+	public void setPayloadInputStream(InputStream payloadInputStream) {
+		/* do nothing. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void write(OutputStream outputStream) throws IOException {
+		/* create payload stream. */
+		setPayloadInputStream(new SequenceInputStream(Collections.enumeration(directFileInputStreams)));
+		/* write out all the fields. */
+		super.write(outputStream);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ClientPutDiskDir.java b/alien/src/net/pterodactylus/fcp/ClientPutDiskDir.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ClientPutDiskDir.java
@@ -0,0 +1,191 @@
+/*
+ * jFCPlib - ClientPutDiskDir.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ClientPutDiskDir” message is used to insert a complete directory from
+ * the disk to a single key.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ClientPutDiskDir extends FcpMessage {
+
+	/**
+	 * Creates a new “ClientPutDiskDir” message.
+	 *
+	 * @param uri
+	 *            The URI to insert the file to
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param directory
+	 *            The name of the directory to insert
+	 */
+	public ClientPutDiskDir(String uri, String identifier, String directory) {
+		super("ClientPutDiskDir");
+		setField("URI", uri);
+		setField("Identifier", identifier);
+		setField("Filename", directory);
+	}
+
+	/**
+	 * The verbosity of the request. Depending on this parameter you will
+	 * received only the bare minimum of messages for the request (i.e. “it
+	 * completed”) or a whole lot more.
+	 *
+	 * @see Verbosity
+	 * @param verbosity
+	 *            The verbosity of the request
+	 */
+	public void setVerbosity(Verbosity verbosity) {
+		setField("Verbosity", String.valueOf(verbosity));
+	}
+
+	/**
+	 * The number of retries for a request if the initial try failed.
+	 *
+	 * @param maxRetries
+	 *            The maximum number of retries after failure, or
+	 *            <code>-1</code> to retry forever.
+	 */
+	public void setMaxRetries(int maxRetries) {
+		setField("MaxRetries", String.valueOf(maxRetries));
+	}
+
+	/**
+	 * Sets the priority of the request.
+	 *
+	 * @param priority
+	 *            The priority of the request
+	 */
+	public void setPriority(Priority priority) {
+		setField("PriorityClass", String.valueOf(priority));
+	}
+
+	/**
+	 * Determines whether the node should really insert the data or generate the
+	 * final CHK only.
+	 *
+	 * @param getCHKOnly
+	 *            <code>true</code> to generate the final CHK only,
+	 *            <code>false</code> to really insert the data
+	 */
+	public void setGetCHKOnly(boolean getCHKOnly) {
+		setField("GetCHKOnly", String.valueOf(getCHKOnly));
+	}
+
+	/**
+	 * Sets whether an insert request should be forked when it is cached.
+	 *
+	 * @param forkOnCacheable
+	 *            {@code true} to fork the insert when it is cached, {@code
+	 *            false} otherwise
+	 */
+	public void setForkOnCacheable(boolean forkOnCacheable) {
+		setField("ForkOnCacheable", String.valueOf(forkOnCacheable));
+	}
+
+	/**
+	 * Sets the number of additional inserts of single blocks.
+	 *
+	 * @param extraInsertsSingleBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSingleBlock(int extraInsertsSingleBlock) {
+		setField("ExtraInsertsSingleBlock", String.valueOf(extraInsertsSingleBlock));
+	}
+
+	/**
+	 * Sets the number of additional inserts of splitfile header blocks.
+	 *
+	 * @param extraInsertsSplitfileHeaderBlock
+	 *            The number of additional inserts
+	 */
+	public void setExtraInsertsSplitfileHeaderBlock(int extraInsertsSplitfileHeaderBlock) {
+		setField("ExtraInsertsSplitfileHeaderBlock", String.valueOf(extraInsertsSplitfileHeaderBlock));
+	}
+
+	/**
+	 * Determines whether this request appears on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> to put the request on the global queue,
+	 *            <code>false</code> for the client-local queue.
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Determines whether the node should skip compression because the file has
+	 * already been compressed.
+	 *
+	 * @param dontCompress
+	 *            <code>true</code> to skip compression of the data in the node,
+	 *            <code>false</code> to allow compression
+	 */
+	public void setDontCompress(boolean dontCompress) {
+		setField("DontCompress", String.valueOf(dontCompress));
+	}
+
+	/**
+	 * Sets an optional client token. This client token is mentioned in progress
+	 * and other request-related messages and can be used to identify this
+	 * request.
+	 *
+	 * @param clientToken
+	 *            The client token
+	 */
+	public void setClientToken(String clientToken) {
+		setField("ClientToken", clientToken);
+	}
+
+	/**
+	 * Sets the persistence of this request.
+	 *
+	 * @param persistence
+	 *            The persistence of this request
+	 */
+	public void setPersistence(Persistence persistence) {
+		setField("Persistence", String.valueOf(persistence));
+	}
+
+	/**
+	 * Sets the name of the default file. The default file is shown when the key
+	 * is requested with an additional name.
+	 *
+	 * @param defaultName
+	 *            The name of the default file
+	 */
+	public void setDefaultName(String defaultName) {
+		setField("DefaultName", defaultName);
+	}
+
+	/**
+	 * Sets whether unreadable files allow the insert to continue.
+	 *
+	 * @param allowUnreadableFiles
+	 *            <code>true</code> to just ignore unreadable files,
+	 *            <code>false</code> to let the insert fail when an unreadable
+	 *            file is encountered
+	 */
+	public void setAllowUnreadableFiles(boolean allowUnreadableFiles) {
+		setField("AllowUnreadableFiles", String.valueOf(allowUnreadableFiles));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/CloseConnectionDuplicateClientName.java b/alien/src/net/pterodactylus/fcp/CloseConnectionDuplicateClientName.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/CloseConnectionDuplicateClientName.java
@@ -0,0 +1,39 @@
+/*
+ * jFCPlib - CloseConnectionDuplicateClientName.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “CloseConnectionDuplicateClientName” message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class CloseConnectionDuplicateClientName extends BaseMessage {
+
+	/**
+	 * Creates a new CloseConnectionDuplicateClientName message that wraps the
+	 * given message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	CloseConnectionDuplicateClientName(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ConfigData.java b/alien/src/net/pterodactylus/fcp/ConfigData.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ConfigData.java
@@ -0,0 +1,128 @@
+/*
+ * jFCPlib - ConfigData.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ConfigData” message contains various aspects of the node’s configuration.
+ *
+ * @see GetConfig
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ConfigData extends BaseMessage {
+
+	/**
+	 * Creates a new “ConfigData” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	ConfigData(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the current value of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The current value of the option
+	 */
+	public String getCurrent(String option) {
+		return getField("current." + option);
+	}
+
+	/**
+	 * Returns the short description of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The short description of the option
+	 */
+	public String getShortDescription(String option) {
+		return getField("shortDescription." + option);
+	}
+
+	/**
+	 * Returns the long description of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The long description of the option
+	 */
+	public String getLongDescription(String option) {
+		return getField("longDescription." + option);
+	}
+
+	/**
+	 * Returns the data type of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The data type of the option
+	 */
+	public String getDataType(String option) {
+		return getField("dataType." + option);
+	}
+
+	/**
+	 * Returns the default value of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The default value of the option
+	 */
+	public String getDefault(String option) {
+		return getField("default." + option);
+	}
+
+	/**
+	 * Returns the sort order of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The sort order of the option, or <code>-1</code> if the sort
+	 *         order could not be parsed
+	 */
+	public int getSortOrder(String option) {
+		return FcpUtils.safeParseInt(getField("sortOrder." + option));
+	}
+
+	/**
+	 * Returns the expert flag of the given option.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The expert flag of the option
+	 */
+	public boolean getExpertFlag(String option) {
+		return Boolean.valueOf(getField("expertFlag." + option));
+	}
+
+	/**
+	 * Returns the force-write flag of the given option
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @return The force-write flag of the given option
+	 */
+	public boolean getForceWriteFlag(String option) {
+		return Boolean.valueOf(getField("forceWriteFlag." + option));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/DSAGroup.java b/alien/src/net/pterodactylus/fcp/DSAGroup.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/DSAGroup.java
@@ -0,0 +1,85 @@
+/*
+ * jFCPlib - DSAGroup.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.security.interfaces.DSAParams;
+
+/**
+ * Container for the DSA group of a peer. A DSA group consists of a base (called
+ * “g”), a prime (called “p”) and a subprime (called “q”).
+ *
+ * @see DSAParams
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class DSAGroup {
+
+	/** The base of the DSA group. */
+	private final String base;
+
+	/** The prime of the DSA group. */
+	private final String prime;
+
+	/** The subprime of the DSA group. */
+	private final String subprime;
+
+	/**
+	 * Creates a new DSA group with the given base (“g”), prime (“p”), and
+	 * subprime (“q”).
+	 *
+	 * @param base
+	 *            The base of the DSA group
+	 * @param prime
+	 *            The prime of the DSA group
+	 * @param subprime
+	 *            The subprime of the DSA group
+	 */
+	public DSAGroup(String base, String prime, String subprime) {
+		this.base = base;
+		this.prime = prime;
+		this.subprime = subprime;
+	}
+
+	/**
+	 * Returns the base (“g”) of the DSA group.
+	 *
+	 * @return The base of the DSA group
+	 */
+	public String getBase() {
+		return base;
+	}
+
+	/**
+	 * Returns the prime (“p”) of the DSA group.
+	 *
+	 * @return The prime of the DSA group
+	 */
+	public String getPrime() {
+		return prime;
+	}
+
+	/**
+	 * Returns the subprime (“q”) of the DSA group.
+	 *
+	 * @return The subprime of the DSA group
+	 */
+	public String getSubprime() {
+		return subprime;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/DataFound.java b/alien/src/net/pterodactylus/fcp/DataFound.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/DataFound.java
@@ -0,0 +1,77 @@
+/*
+ * jFCPlib - DataFound.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “DataFound” message signals the client that the data requested by a
+ * {@link ClientGet} operation has been found. This message does not include the
+ * actual data, though.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class DataFound extends BaseMessage {
+
+	/**
+	 * Creates a new “DataFound” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	DataFound(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if the request is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the content type of the data.
+	 *
+	 * @return The content type of the data
+	 */
+	public String getMetadataContentType() {
+		return getField("Metadata.ContentType");
+	}
+
+	/**
+	 * Returns the length of the data.
+	 *
+	 * @return The length of the data
+	 */
+	public long getDataLength() {
+		return FcpUtils.safeParseLong(getField("DataLength"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/EndListPeerNotes.java b/alien/src/net/pterodactylus/fcp/EndListPeerNotes.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/EndListPeerNotes.java
@@ -0,0 +1,39 @@
+/*
+ * jFCPlib - EndListPeerNotes.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “EndListPeerNotes” message signals the end of a list of “PeerNote”
+ * messages.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class EndListPeerNotes extends BaseMessage {
+
+	/**
+	 * Creates a new “EndListPeerNotes” message that wraps the received message.
+	 *
+	 * @param fcpMessage
+	 *            The received message
+	 */
+	EndListPeerNotes(FcpMessage fcpMessage) {
+		super(fcpMessage);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/EndListPeers.java b/alien/src/net/pterodactylus/fcp/EndListPeers.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/EndListPeers.java
@@ -0,0 +1,47 @@
+/*
+ * jFCPlib - EndListPeers.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * This message marks the end of a list of “Peer” replies.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class EndListPeers extends BaseMessage {
+
+	/**
+	 * Creates a new “EndListPeers” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The message that was received
+	 */
+	EndListPeers(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/EndListPersistentRequests.java b/alien/src/net/pterodactylus/fcp/EndListPersistentRequests.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/EndListPersistentRequests.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - EndListPersistentRequests.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “EndListPersistentRequests” message signals the end of a list of
+ * {@link PersistentGet} and {@link PersistentPut} requests.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class EndListPersistentRequests extends BaseMessage {
+
+	/**
+	 * Creates a new “EndListPersistentRequests” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	EndListPersistentRequests(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FCPPluginMessage.java b/alien/src/net/pterodactylus/fcp/FCPPluginMessage.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FCPPluginMessage.java
@@ -0,0 +1,76 @@
+/*
+ * jFCPlib - PluginMessage.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * An “CPPluginMessage” sends a message with custom parameters and (optional)
+ * payload to a plugin.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FCPPluginMessage extends FcpMessage {
+
+	/**
+	 * Creates a new “FCPPluginMessage” message for the given plugin.
+	 *
+	 * @param pluginClass
+	 *            The name of the plugin class
+	 */
+	public FCPPluginMessage(String pluginClass) {
+		super("FCPPluginMessage");
+		setField("PluginName", pluginClass);
+	}
+
+	/**
+	 * Sets the identifier of the request. Though this is still optional you are
+	 * encouraged to include it because the plugin might reply in random order
+	 * to requests.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public void setIdentifier(String identifier) {
+		setField("Identifier", identifier);
+	}
+
+	/**
+	 * Sets a custom parameter for the plugin.
+	 *
+	 * @param key
+	 *            The key of the parameter
+	 * @param value
+	 *            The value of the parameter
+	 */
+	public void setParameter(String key, String value) {
+		setField("Param." + key, value);
+	}
+
+	/**
+	 * Sets the length of data of the optional payload. If you call this method
+	 * you also have to call {@link #setPayloadInputStream(java.io.InputStream)}
+	 * !
+	 *
+	 * @param dataLength
+	 *            The length of data in the payload input stream
+	 */
+	public void setDataLength(long dataLength) {
+		setField("DataLength", String.valueOf(dataLength));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FCPPluginReply.java b/alien/src/net/pterodactylus/fcp/FCPPluginReply.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FCPPluginReply.java
@@ -0,0 +1,117 @@
+/*
+ * jFCPlib - FCPPluginReply.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * The “FCPPluginReply” is sent by a plugin as a response to a
+ * {@link FCPPluginMessage} message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FCPPluginReply extends BaseMessage {
+
+	/** The payload input stream. */
+	private final InputStream payloadInputStream;
+
+	/**
+	 * Creates a new “FCPPluginReply” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 * @param payloadInputStream
+	 *            The optional input stream for the payload
+	 */
+	FCPPluginReply(FcpMessage receivedMessage, InputStream payloadInputStream) {
+		super(receivedMessage);
+		this.payloadInputStream = payloadInputStream;
+	}
+
+	/**
+	 * Returns the name of the plugin.
+	 *
+	 * @return The name of the plugin
+	 */
+	public String getPluginName() {
+		return getField("PluginName");
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the length of the optional payload.
+	 *
+	 * @return The length of the payload, or <code>-1</code> if there is no
+	 *         payload or the length could not be parsed
+	 */
+	public long getDataLength() {
+		return FcpUtils.safeParseLong(getField("DataLength"));
+	}
+
+	/**
+	 * Returns a reply from the plugin.
+	 *
+	 * @param key
+	 *            The name of the reply
+	 * @return The value of the reply
+	 */
+	public String getReply(String key) {
+		return getField("Replies." + key);
+	}
+
+	/**
+	 * Returns all replies from the plugin. The plugin sends replies as normal
+	 * message fields prefixed by “Replies.”. The keys of the returned map do
+	 * not contain this prefix!
+	 *
+	 * @return All replies from the plugin
+	 */
+	public Map<String, String> getReplies() {
+		Map<String, String> fields = getFields();
+		Map<String, String> replies = new HashMap<String, String>();
+		for (Entry<String, String> field : fields.entrySet()) {
+			if (field.getKey().startsWith("Replies.")) {
+				replies.put(field.getKey().substring(8), field.getValue());
+			}
+		}
+		return replies;
+	}
+
+	/**
+	 * Returns the optional payload.
+	 *
+	 * @return The payload of the reply, or <code>null</code> if there is no
+	 *         payload
+	 */
+	public InputStream getPayloadInputStream() {
+		return payloadInputStream;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpAdapter.java b/alien/src/net/pterodactylus/fcp/FcpAdapter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpAdapter.java
@@ -0,0 +1,305 @@
+/*
+ * jFCPlib - FcpAdapter.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Adapter for {@link FcpListener}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpAdapter implements FcpListener {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see FcpListener#receivedNodeData(FcpConnection, NodeData)
+	 */
+	public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see FcpListener#receivedTestDDAReply(FcpConnection, TestDDAReply)
+	 */
+	public void receivedTestDDAReply(FcpConnection fcpConnection, TestDDAReply testDDAReply) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedTestDDAComplete(FcpConnection fcpConnection, TestDDAComplete testDDAComplete) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedURIGenerated(FcpConnection fcpConnection, URIGenerated uriGenerated) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedAllData(FcpConnection fcpConnection, AllData allData) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedStartedCompression(FcpConnection fcpConnection, StartedCompression startedCompression) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedFinishedCompression(FcpConnection fcpConnection, FinishedCompression finishedCompression) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedUnknownPeerNoteType(FcpConnection fcpConnection, UnknownPeerNoteType unknownPeerNoteType) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedUnknownNodeIdentifier(FcpConnection fcpConnection, UnknownNodeIdentifier unknownNodeIdentifier) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedConfigData(FcpConnection fcpConnection, ConfigData configData) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedIdentifierCollision(FcpConnection fcpConnection, IdentifierCollision identifierCollision) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPersistentPutDir(FcpConnection fcpConnection, PersistentPutDir persistentPutDir) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPersistentRequestRemoved(FcpConnection fcpConnection, PersistentRequestRemoved persistentRequestRemoved) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedSubscribedUSKUpdate(FcpConnection fcpConnection, SubscribedUSKUpdate subscribedUSKUpdate) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPluginInfo(FcpConnection fcpConnection, PluginInfo pluginInfo) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPersistentRequestModified(FcpConnection fcpConnection, PersistentRequestModified persistentRequestModified) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedPutFetchable(FcpConnection fcpConnection, PutFetchable putFetchable) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedSentFeed(FcpConnection source, SentFeed sentFeed) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedBookmarkFeed(FcpConnection fcpConnection, ReceivedBookmarkFeed receivedBookmarkFeed) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void receivedMessage(FcpConnection fcpConnection, FcpMessage fcpMessage) {
+		/* empty. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
+		/* empty. */
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpConnection.java b/alien/src/net/pterodactylus/fcp/FcpConnection.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpConnection.java
@@ -0,0 +1,499 @@
+/*
+ * jFCPlib - FpcConnection.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.Closeable;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * An FCP connection to a Freenet node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpConnection implements Closeable {
+
+	/** Logger. */
+	private static final Logger logger = Logging.getLogger(FcpConnection.class.getName());
+
+	/** The default port for FCP v2. */
+	public static final int DEFAULT_PORT = 9481;
+
+	/** Listener management. */
+	private final FcpListenerManager fcpListenerManager = new FcpListenerManager(this);
+
+	/** The address of the node. */
+	private final InetAddress address;
+
+	/** The port number of the node’s FCP port. */
+	private final int port;
+
+	/** The remote socket. */
+	private Socket remoteSocket;
+
+	/** The input stream from the node. */
+	private InputStream remoteInputStream;
+
+	/** The output stream to the node. */
+	private OutputStream remoteOutputStream;
+
+	/** The connection handler. */
+	private FcpConnectionHandler connectionHandler;
+
+	/** Incoming message statistics. */
+	private static final Map<String, Integer> incomingMessageStatistics = Collections.synchronizedMap(new HashMap<String, Integer>());
+
+	/**
+	 * Creates a new FCP connection to the freenet node running on localhost,
+	 * using the default port.
+	 *
+	 * @throws UnknownHostException
+	 *             if the hostname can not be resolved
+	 */
+	public FcpConnection() throws UnknownHostException {
+		this(InetAddress.getLocalHost());
+	}
+
+	/**
+	 * Creates a new FCP connection to the Freenet node running on the given
+	 * host, listening on the default port.
+	 *
+	 * @param host
+	 *            The hostname of the Freenet node
+	 * @throws UnknownHostException
+	 *             if <code>host</code> can not be resolved
+	 */
+	public FcpConnection(String host) throws UnknownHostException {
+		this(host, DEFAULT_PORT);
+	}
+
+	/**
+	 * Creates a new FCP connection to the Freenet node running on the given
+	 * host, listening on the given port.
+	 *
+	 * @param host
+	 *            The hostname of the Freenet node
+	 * @param port
+	 *            The port number of the node’s FCP port
+	 * @throws UnknownHostException
+	 *             if <code>host</code> can not be resolved
+	 */
+	public FcpConnection(String host, int port) throws UnknownHostException {
+		this(InetAddress.getByName(host), port);
+	}
+
+	/**
+	 * Creates a new FCP connection to the Freenet node running at the given
+	 * address, listening on the default port.
+	 *
+	 * @param address
+	 *            The address of the Freenet node
+	 */
+	public FcpConnection(InetAddress address) {
+		this(address, DEFAULT_PORT);
+	}
+
+	/**
+	 * Creates a new FCP connection to the Freenet node running at the given
+	 * address, listening on the given port.
+	 *
+	 * @param address
+	 *            The address of the Freenet node
+	 * @param port
+	 *            The port number of the node’s FCP port
+	 */
+	public FcpConnection(InetAddress address, int port) {
+		this.address = address;
+		this.port = port;
+	}
+
+	//
+	// LISTENER MANAGEMENT
+	//
+
+	/**
+	 * Adds the given listener to the list of listeners.
+	 *
+	 * @param fcpListener
+	 *            The listener to add
+	 */
+	public void addFcpListener(FcpListener fcpListener) {
+		fcpListenerManager.addListener(fcpListener);
+	}
+
+	/**
+	 * Removes the given listener from the list of listeners.
+	 *
+	 * @param fcpListener
+	 *            The listener to remove
+	 */
+	public void removeFcpListener(FcpListener fcpListener) {
+		fcpListenerManager.removeListener(fcpListener);
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Connects to the node.
+	 *
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws IllegalStateException
+	 *             if there is already a connection to the node
+	 */
+	public synchronized void connect() throws IOException, IllegalStateException {
+		if (connectionHandler != null) {
+			throw new IllegalStateException("already connected, disconnect first");
+		}
+		logger.info("connecting to " + address + ":" + port + "…");
+		remoteSocket = new Socket(address, port);
+		remoteInputStream = remoteSocket.getInputStream();
+		remoteOutputStream = remoteSocket.getOutputStream();
+		new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start();
+	}
+
+	/**
+	 * Disconnects from the node. If there is no connection to the node, this
+	 * method does nothing.
+	 *
+	 * @deprecated Use {@link #close()} instead
+	 */
+	@Deprecated
+	public synchronized void disconnect() {
+		close();
+	}
+
+	/**
+	 * Closes the connection. If there is no connection to the node, this method
+	 * does nothing.
+	 */
+	public void close() {
+		handleDisconnect(null);
+	}
+
+	/**
+	 * Sends the given FCP message.
+	 *
+	 * @param fcpMessage
+	 *            The FCP message to send
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException {
+		logger.fine("sending message: " + fcpMessage.getName());
+		fcpMessage.write(remoteOutputStream);
+	}
+
+	//
+	// PACKAGE-PRIVATE METHODS
+	//
+
+	/**
+	 * Handles the given message, notifying listeners. This message should only
+	 * be called by {@link FcpConnectionHandler}.
+	 *
+	 * @param fcpMessage
+	 *            The received message
+	 */
+	void handleMessage(FcpMessage fcpMessage) {
+		logger.fine("received message: " + fcpMessage.getName());
+		String messageName = fcpMessage.getName();
+		countMessage(messageName);
+		if ("SimpleProgress".equals(messageName)) {
+			fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage));
+		} else if ("ProtocolError".equals(messageName)) {
+			fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage));
+		} else if ("PersistentGet".equals(messageName)) {
+			fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage));
+		} else if ("PersistentPut".equals(messageName)) {
+			fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage));
+		} else if ("PersistentPutDir".equals(messageName)) {
+			fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage));
+		} else if ("URIGenerated".equals(messageName)) {
+			fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage));
+		} else if ("EndListPersistentRequests".equals(messageName)) {
+			fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage));
+		} else if ("Peer".equals(messageName)) {
+			fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage));
+		} else if ("PeerNote".equals(messageName)) {
+			fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage));
+		} else if ("StartedCompression".equals(messageName)) {
+			fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage));
+		} else if ("FinishedCompression".equals(messageName)) {
+			fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage));
+		} else if ("GetFailed".equals(messageName)) {
+			fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage));
+		} else if ("PutFetchable".equals(messageName)) {
+			fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage));
+		} else if ("PutSuccessful".equals(messageName)) {
+			fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage));
+		} else if ("PutFailed".equals(messageName)) {
+			fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage));
+		} else if ("DataFound".equals(messageName)) {
+			fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage));
+		} else if ("SubscribedUSKUpdate".equals(messageName)) {
+			fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage));
+		} else if ("IdentifierCollision".equals(messageName)) {
+			fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage));
+		} else if ("AllData".equals(messageName)) {
+			LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
+			fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream));
+			try {
+				payloadInputStream.consume();
+			} catch (IOException ioe1) {
+				/* well, ignore. when the connection handler fails, all fails. */
+			}
+		} else if ("EndListPeerNotes".equals(messageName)) {
+			fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage));
+		} else if ("EndListPeers".equals(messageName)) {
+			fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage));
+		} else if ("SSKKeypair".equals(messageName)) {
+			fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage));
+		} else if ("PeerRemoved".equals(messageName)) {
+			fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage));
+		} else if ("PersistentRequestModified".equals(messageName)) {
+			fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage));
+		} else if ("PersistentRequestRemoved".equals(messageName)) {
+			fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage));
+		} else if ("UnknownPeerNoteType".equals(messageName)) {
+			fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage));
+		} else if ("UnknownNodeIdentifier".equals(messageName)) {
+			fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage));
+		} else if ("FCPPluginReply".equals(messageName)) {
+			LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
+			fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream));
+			try {
+				payloadInputStream.consume();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		} else if ("PluginInfo".equals(messageName)) {
+			fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(fcpMessage));
+		} else if ("NodeData".equals(messageName)) {
+			fcpListenerManager.fireReceivedNodeData(new NodeData(fcpMessage));
+		} else if ("TestDDAReply".equals(messageName)) {
+			fcpListenerManager.fireReceivedTestDDAReply(new TestDDAReply(fcpMessage));
+		} else if ("TestDDAComplete".equals(messageName)) {
+			fcpListenerManager.fireReceivedTestDDAComplete(new TestDDAComplete(fcpMessage));
+		} else if ("ConfigData".equals(messageName)) {
+			fcpListenerManager.fireReceivedConfigData(new ConfigData(fcpMessage));
+		} else if ("NodeHello".equals(messageName)) {
+			fcpListenerManager.fireReceivedNodeHello(new NodeHello(fcpMessage));
+		} else if ("CloseConnectionDuplicateClientName".equals(messageName)) {
+			fcpListenerManager.fireReceivedCloseConnectionDuplicateClientName(new CloseConnectionDuplicateClientName(fcpMessage));
+		} else if ("SentFeed".equals(messageName)) {
+			fcpListenerManager.fireSentFeed(new SentFeed(fcpMessage));
+		} else if ("ReceivedBookmarkFeed".equals(messageName)) {
+			fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage));
+		} else {
+			fcpListenerManager.fireMessageReceived(fcpMessage);
+		}
+	}
+
+	/**
+	 * Handles a disconnect from the node.
+	 *
+	 * @param throwable
+	 *            The exception that caused the disconnect, or <code>null</code>
+	 *            if there was no exception
+	 */
+	synchronized void handleDisconnect(Throwable throwable) {
+            //System.err.println("DISCONNECTED: " + throwable);
+		FcpUtils.close(remoteInputStream);
+		FcpUtils.close(remoteOutputStream);
+		FcpUtils.close(remoteSocket);
+		if (connectionHandler != null) {
+			connectionHandler.stop();
+			connectionHandler = null;
+			fcpListenerManager.fireConnectionClosed(throwable);
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Incremets the counter in {@link #incomingMessageStatistics} by
+	 * <cod>1</code> for the given message name.
+	 *
+	 * @param name
+	 *            The name of the message to count
+	 */
+	private void countMessage(String name) {
+		int oldValue = 0;
+		if (incomingMessageStatistics.containsKey(name)) {
+			oldValue = incomingMessageStatistics.get(name);
+		}
+		incomingMessageStatistics.put(name, oldValue + 1);
+		logger.finest("count for " + name + ": " + (oldValue + 1));
+	}
+
+	/**
+	 * Returns a limited input stream from the node’s input stream.
+	 *
+	 * @param dataLength
+	 *            The length of the stream
+	 * @return The limited input stream
+	 */
+	private synchronized LimitedInputStream getInputStream(long dataLength) {
+		if (dataLength <= 0) {
+			return new LimitedInputStream(null, 0);
+		}
+		return new LimitedInputStream(remoteInputStream, dataLength);
+	}
+
+	/**
+	 * A wrapper around an {@link InputStream} that only supplies a limit number
+	 * of bytes from the underlying input stream.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	private static class LimitedInputStream extends FilterInputStream {
+
+		/** The remaining number of bytes that can be read. */
+		private long remaining;
+
+		/**
+		 * Creates a new LimitedInputStream that supplies at most
+		 * <code>length</code> bytes from the given input stream.
+		 *
+		 * @param inputStream
+		 *            The input stream
+		 * @param length
+		 *            The number of bytes to read
+		 */
+		public LimitedInputStream(InputStream inputStream, long length) {
+			super(inputStream);
+			remaining = length;
+		}
+
+		/**
+		 * @see java.io.FilterInputStream#available()
+		 */
+		@Override
+		public synchronized int available() throws IOException {
+			if (remaining == 0) {
+				return 0;
+			}
+			return (int) Math.min(super.available(), Math.min(Integer.MAX_VALUE, remaining));
+		}
+
+		/**
+		 * @see java.io.FilterInputStream#read()
+		 */
+		@Override
+		public synchronized int read() throws IOException {
+			int read = -1;
+			if (remaining > 0) {
+				read = super.read();
+				remaining--;
+			}
+			return read;
+		}
+
+		/**
+		 * @see java.io.FilterInputStream#read(byte[], int, int)
+		 */
+		@Override
+		public synchronized int read(byte[] b, int off, int len) throws IOException {
+			if (remaining == 0) {
+				return -1;
+			}
+			int toCopy = (int) Math.min(len, Math.min(remaining, Integer.MAX_VALUE));
+			int read = super.read(b, off, toCopy);
+			remaining -= read;
+			return read;
+		}
+
+		/**
+		 * @see java.io.FilterInputStream#skip(long)
+		 */
+		@Override
+		public synchronized long skip(long n) throws IOException {
+			if ((n < 0) || (remaining == 0)) {
+				return 0;
+			}
+			long skipped = super.skip(Math.min(n, remaining));
+			remaining -= skipped;
+			return skipped;
+		}
+
+		/**
+		 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
+		 * {@link #reset()} are not supported.
+		 *
+		 * @see java.io.FilterInputStream#mark(int)
+		 */
+		@Override
+		public synchronized void mark(int readlimit) {
+			/* do nothing. */
+		}
+
+		/**
+		 * {@inheritDoc}
+		 *
+		 * @see java.io.FilterInputStream#markSupported()
+		 * @return <code>false</code>
+		 */
+		@Override
+		public boolean markSupported() {
+			return false;
+		}
+
+		/**
+		 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
+		 * {@link #reset()} are not supported.
+		 *
+		 * @see java.io.FilterInputStream#reset()
+		 */
+		@Override
+		public synchronized void reset() throws IOException {
+			/* do nothing. */
+		}
+
+		/**
+		 * Consumes the input stream, i.e. read all bytes until the limit is
+		 * reached.
+		 *
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 */
+		public synchronized void consume() throws IOException {
+			while (remaining > 0) {
+				skip(remaining);
+			}
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpConnectionHandler.java b/alien/src/net/pterodactylus/fcp/FcpConnectionHandler.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpConnectionHandler.java
@@ -0,0 +1,168 @@
+/*
+ * jFCPlib - FcpConnectionHandler.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Handles an FCP connection to a node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+class FcpConnectionHandler implements Runnable {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(FcpConnectionHandler.class.getName());
+
+	/** The underlying connection. */
+	private final FcpConnection fcpConnection;
+
+	/** The input stream from the node. */
+	private final InputStream remoteInputStream;
+
+	/** Whether to stop the connection handler. */
+	private boolean shouldStop;
+
+	/** Whether the next read line feed should be ignored. */
+	private boolean ignoreNextLinefeed;
+
+	/**
+	 * Creates a new connection handler that operates on the given connection
+	 * and input stream.
+	 *
+	 * @param fcpConnection
+	 *            The underlying FCP connection
+	 * @param remoteInputStream
+	 *            The input stream from the node
+	 */
+	public FcpConnectionHandler(FcpConnection fcpConnection, InputStream remoteInputStream) {
+		this.fcpConnection = fcpConnection;
+		this.remoteInputStream = remoteInputStream;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void run() {
+		FcpMessage fcpMessage = null;
+		Throwable throwable = null;
+		while (true) {
+			synchronized (this) {
+				if (shouldStop) {
+					break;
+				}
+			}
+			try {
+				String line = readLine();
+				logger.log(Level.FINEST, "read line: %1$s", line);
+				if (line == null) {
+					break;
+				}
+				if (line.length() == 0) {
+					continue;
+				}
+				line = line.trim();
+                                //System.err.println("SERVER: " + line); // DCI
+				if (fcpMessage == null) {
+					fcpMessage = new FcpMessage(line);
+					continue;
+				}
+				if ("EndMessage".equalsIgnoreCase(line) || "Data".equalsIgnoreCase(line)) {
+					fcpConnection.handleMessage(fcpMessage);
+					fcpMessage = null;
+				}
+				int equalSign = line.indexOf('=');
+				if (equalSign == -1) {
+					/* something's fishy! */
+					continue;
+				}
+				String field = line.substring(0, equalSign);
+				String value = line.substring(equalSign + 1);
+				assert fcpMessage != null : "fcp message is null";
+				fcpMessage.setField(field, value);
+			} catch (IOException ioe1) {
+				throwable = ioe1;
+				break;
+			}
+		}
+		fcpConnection.handleDisconnect(throwable);
+	}
+
+	/**
+	 * Stops the connection handler.
+	 */
+	public void stop() {
+		synchronized (this) {
+			shouldStop = true;
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Reads bytes from {@link #remoteInputStream} until ‘\r’ or ‘\n’ are
+	 * encountered and decodes the read bytes using UTF-8.
+	 *
+	 * @return The decoded line
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private String readLine() throws IOException {
+		byte[] readBytes = new byte[512];
+		int readIndex = 0;
+		while (true) {
+			int nextByte = remoteInputStream.read();
+			if (nextByte == -1) {
+				if (readIndex == 0) {
+					return null;
+				}
+				break;
+			}
+			if (nextByte == 10) {
+				if (!ignoreNextLinefeed) {
+					break;
+				}
+			}
+			ignoreNextLinefeed = false;
+			if (nextByte == 13) {
+				ignoreNextLinefeed = true;
+				break;
+			}
+			if (readIndex == readBytes.length) {
+				/* recopy & enlarge array */
+				byte[] newReadBytes = new byte[readBytes.length * 2];
+				System.arraycopy(readBytes, 0, newReadBytes, 0, readBytes.length);
+				readBytes = newReadBytes;
+			}
+			readBytes[readIndex++] = (byte) nextByte;
+		}
+		ByteBuffer byteBuffer = ByteBuffer.wrap(readBytes, 0, readIndex);
+		return Charset.forName("UTF-8").decode(byteBuffer).toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpKeyPair.java b/alien/src/net/pterodactylus/fcp/FcpKeyPair.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpKeyPair.java
@@ -0,0 +1,65 @@
+/*
+ * jFCPlib - FcpKeyPair.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Container for an SSK keypair.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpKeyPair {
+
+	/** The public key. */
+	private final String publicKey;
+
+	/** The private key. */
+	private final String privateKey;
+
+	/**
+	 * Creates a new keypair from the given keys.
+	 *
+	 * @param publicKey
+	 *            The public key
+	 * @param privateKey
+	 *            The private key
+	 */
+	public FcpKeyPair(String publicKey, String privateKey) {
+		this.publicKey = publicKey;
+		this.privateKey = privateKey;
+	}
+
+	/**
+	 * Returns the public key of this keypair.
+	 *
+	 * @return The public key
+	 */
+	public String getPublicKey() {
+		return publicKey;
+	}
+
+	/**
+	 * Returns the private key of this keypair.
+	 *
+	 * @return The private key
+	 */
+	public String getPrivateKey() {
+		return privateKey;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpListener.java b/alien/src/net/pterodactylus/fcp/FcpListener.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpListener.java
@@ -0,0 +1,428 @@
+/*
+ * jFCPlib - FpcListener.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.EventListener;
+
+/**
+ * Interface for objects that want to be notified on certain FCP events.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public interface FcpListener extends EventListener {
+
+	/**
+	 * Notifies a listener that a “NodeHello” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param nodeHello
+	 *            The “NodeHello” message
+	 */
+	public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello);
+
+	/**
+	 * Notifies a listener that a “CloseConnectionDuplicateClientName” message
+	 * was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param closeConnectionDuplicateClientName
+	 *            The “CloseConnectionDuplicateClientName” message
+	 */
+	public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName);
+
+	/**
+	 * Notifies a listener that a “SSKKeypair” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received themessage
+	 * @param sskKeypair
+	 *            The “SSKKeypair” message
+	 */
+	public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair);
+
+	/**
+	 * Notifies a listener that a “Peer” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param peer
+	 *            The “Peer” message
+	 */
+	public void receivedPeer(FcpConnection fcpConnection, Peer peer);
+
+	/**
+	 * Notifies a listener that an “EndListPeers” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that recevied the message
+	 * @param endListPeers
+	 *            The “EndListPeers” message
+	 */
+	public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers);
+
+	/**
+	 * Notifies a listener that a “PeerNote” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param peerNote
+	 *            The “PeerNote” message
+	 */
+	public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote);
+
+	/**
+	 * Notifies a listener that an “EndListPeerNotes” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param endListPeerNotes
+	 *            The “EndListPeerNotes” message
+	 */
+	public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes);
+
+	/**
+	 * Notifies a listener that a “PeerRemoved” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param peerRemoved
+	 *            The “PeerRemoved” message
+	 */
+	public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved);
+
+	/**
+	 * Notifies a listener that a “NodeData” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param nodeData
+	 *            The “NodeData” message
+	 */
+	public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData);
+
+	/**
+	 * Notifies a listener that a “TestDDAReply” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param testDDAReply
+	 *            The “TestDDAReply” message
+	 */
+	public void receivedTestDDAReply(FcpConnection fcpConnection, TestDDAReply testDDAReply);
+
+	/**
+	 * Notifies a listener that a “TestDDAComplete” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param testDDAComplete
+	 *            The “TestDDAComplete” message
+	 */
+	public void receivedTestDDAComplete(FcpConnection fcpConnection, TestDDAComplete testDDAComplete);
+
+	/**
+	 * Notifies a listener that a “PersistentGet” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param persistentGet
+	 *            The “PersistentGet” message
+	 */
+	public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet);
+
+	/**
+	 * Notifies a listener that a “PersistentPut” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param persistentPut
+	 *            The “PersistentPut” message
+	 */
+	public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut);
+
+	/**
+	 * Notifies a listener that a “EndListPersistentRequests” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param endListPersistentRequests
+	 *            The “EndListPersistentRequests” message
+	 */
+	public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests);
+
+	/**
+	 * Notifies a listener that a “URIGenerated” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param uriGenerated
+	 *            The “URIGenerated” message
+	 */
+	public void receivedURIGenerated(FcpConnection fcpConnection, URIGenerated uriGenerated);
+
+	/**
+	 * Notifies a listener that a “DataFound” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param dataFound
+	 *            The “DataFound” message
+	 */
+	public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound);
+
+	/**
+	 * Notifies a listener that an “AllData” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param allData
+	 *            The “AllData” message
+	 */
+	public void receivedAllData(FcpConnection fcpConnection, AllData allData);
+
+	/**
+	 * Notifies a listener that a “SimpleProgress” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param simpleProgress
+	 *            The “SimpleProgress” message
+	 */
+	public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress);
+
+	/**
+	 * Notifies a listener that a “StartedCompression” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param startedCompression
+	 *            The “StartedCompression” message
+	 */
+	public void receivedStartedCompression(FcpConnection fcpConnection, StartedCompression startedCompression);
+
+	/**
+	 * Notifies a listener that a “FinishedCompression” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param finishedCompression
+	 *            The “FinishedCompression” message
+	 */
+	public void receivedFinishedCompression(FcpConnection fcpConnection, FinishedCompression finishedCompression);
+
+	/**
+	 * Notifies a listener that an “UnknownPeerNoteType” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param unknownPeerNoteType
+	 *            The “UnknownPeerNoteType” message
+	 */
+	public void receivedUnknownPeerNoteType(FcpConnection fcpConnection, UnknownPeerNoteType unknownPeerNoteType);
+
+	/**
+	 * Notifies a listener that a “UnknownNodeIdentifier” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param unknownNodeIdentifier
+	 *            The “UnknownNodeIdentifier” message
+	 */
+	public void receivedUnknownNodeIdentifier(FcpConnection fcpConnection, UnknownNodeIdentifier unknownNodeIdentifier);
+
+	/**
+	 * Notifies a listener that a “ConfigData” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param configData
+	 *            The “ConfigData” message
+	 */
+	public void receivedConfigData(FcpConnection fcpConnection, ConfigData configData);
+
+	/**
+	 * Notifies a listener that a “GetFailed” message was recevied.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param getFailed
+	 *            The “GetFailed” message
+	 */
+	public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed);
+
+	/**
+	 * Notifies a listener that a “PutFailed” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param putFailed
+	 *            The “PutFailed” message
+	 */
+	public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed);
+
+	/**
+	 * Notifies a listener that an “IdentifierCollision” message was receied.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param identifierCollision
+	 *            The “IdentifierCollision” message
+	 */
+	public void receivedIdentifierCollision(FcpConnection fcpConnection, IdentifierCollision identifierCollision);
+
+	/**
+	 * Notifies a listener that a “PersistentPutDir” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param persistentPutDir
+	 *            The “PersistentPutDir” message
+	 */
+	public void receivedPersistentPutDir(FcpConnection fcpConnection, PersistentPutDir persistentPutDir);
+
+	/**
+	 * Notifies a listener that a “PersistentRequestRemoved” message was
+	 * received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param persistentRequestRemoved
+	 *            The “PersistentRequestRemoved” message
+	 */
+	public void receivedPersistentRequestRemoved(FcpConnection fcpConnection, PersistentRequestRemoved persistentRequestRemoved);
+
+	/**
+	 * Notifies a listener that a “SubscribedUSKUpdate” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that recevied the message
+	 * @param subscribedUSKUpdate
+	 *            The “SubscribedUSKUpdate” message
+	 */
+	public void receivedSubscribedUSKUpdate(FcpConnection fcpConnection, SubscribedUSKUpdate subscribedUSKUpdate);
+
+	/**
+	 * Notifies a listener that a “PluginInfo” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param pluginInfo
+	 *            The “PluginInfo” message
+	 */
+	public void receivedPluginInfo(FcpConnection fcpConnection, PluginInfo pluginInfo);
+
+	/**
+	 * Notifies a listener that an “FCPPluginReply“ message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param fcpPluginReply
+	 *            The “FCPPluginReply” message
+	 */
+	public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply);
+
+	/**
+	 * Notifies a listener that a “PersistentRequestModified” message was
+	 * received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param persistentRequestModified
+	 *            The “PersistentRequestModified” message
+	 */
+	public void receivedPersistentRequestModified(FcpConnection fcpConnection, PersistentRequestModified persistentRequestModified);
+
+	/**
+	 * Notifies a listener that a “PutSuccessful” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param putSuccessful
+	 *            The “PutSuccessful” message
+	 */
+	public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful);
+
+	/**
+	 * Notifies a listener that a “PutFetchable” message was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param putFetchable
+	 *            The “PutFetchable” message
+	 */
+	public void receivedPutFetchable(FcpConnection fcpConnection, PutFetchable putFetchable);
+
+	/**
+	 * Notifies a listener that a feed was sent to a peer.
+	 *
+	 * @param source
+	 *            The connection that received the message
+	 * @param sentFeed
+	 *            The “SentFeed” message
+	 */
+	public void receivedSentFeed(FcpConnection source, SentFeed sentFeed);
+
+	/**
+	 * Notifies a listener that a bookmark was updated.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param receivedBookmarkFeed
+	 *            The “ReceivedBookmarkFeed” message
+	 */
+	public void receivedBookmarkFeed(FcpConnection fcpConnection, ReceivedBookmarkFeed receivedBookmarkFeed);
+
+	/**
+	 * Notifies a listener that a “ProtocolError” was received.
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param protocolError
+	 *            The “ProtocolError” message
+	 */
+	public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError);
+
+	/**
+	 * Notifies a listener that a message has been received. This method is only
+	 * called if {@link FcpConnection#handleMessage(FcpMessage)} does not
+	 * recognize the message. Should that ever happen, please file a bug report!
+	 *
+	 * @param fcpConnection
+	 *            The connection that received the message
+	 * @param fcpMessage
+	 *            The message that was received
+	 */
+	public void receivedMessage(FcpConnection fcpConnection, FcpMessage fcpMessage);
+
+	/**
+	 * Notifies a listener that a connection was closed. A closed connection can
+	 * be reestablished by calling {@link FcpConnection#connect()} on the same
+	 * object again.
+	 *
+	 * @param fcpConnection
+	 *            The connection that was closed.
+	 * @param throwable
+	 *            The exception that caused the disconnect, or <code>null</code>
+	 *            if there was no exception
+	 */
+	public void connectionClosed(FcpConnection fcpConnection, Throwable throwable);
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpListenerManager.java b/alien/src/net/pterodactylus/fcp/FcpListenerManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpListenerManager.java
@@ -0,0 +1,569 @@
+/*
+ * jFCPlib - FcpListenerManager.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manages FCP listeners and event firing.
+ *
+ * @author David ‘Bombe’ Roden <bombe@pterodactylus.net>
+ */
+public class FcpListenerManager extends AbstractListenerManager<FcpConnection, FcpListener> {
+
+	/**
+	 * Creates a new listener manager.
+	 *
+	 * @param fcpConnection
+	 *            The source FCP connection
+	 */
+	public FcpListenerManager(FcpConnection fcpConnection) {
+		super(fcpConnection);
+	}
+
+	/**
+	 * Notifies listeners that a “NodeHello” message was received.
+	 *
+	 * @see FcpListener#receivedNodeHello(FcpConnection, NodeHello)
+	 * @param nodeHello
+	 *            The “NodeHello” message
+	 */
+	public void fireReceivedNodeHello(NodeHello nodeHello) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedNodeHello(getSource(), nodeHello);
+		}
+	}
+
+	/**
+	 * Notifies listeners that a “CloseConnectionDuplicateClientName” message
+	 * was received.
+	 *
+	 * @see FcpListener#receivedCloseConnectionDuplicateClientName(FcpConnection,
+	 *      CloseConnectionDuplicateClientName)
+	 * @param closeConnectionDuplicateClientName
+	 *            The “CloseConnectionDuplicateClientName” message
+	 */
+	public void fireReceivedCloseConnectionDuplicateClientName(CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedCloseConnectionDuplicateClientName(getSource(), closeConnectionDuplicateClientName);
+		}
+	}
+
+	/**
+	 * Notifies listeners that a “SSKKeypair” message was received.
+	 *
+	 * @see FcpListener#receivedSSKKeypair(FcpConnection, SSKKeypair)
+	 * @param sskKeypair
+	 *            The “SSKKeypair” message
+	 */
+	public void fireReceivedSSKKeypair(SSKKeypair sskKeypair) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedSSKKeypair(getSource(), sskKeypair);
+		}
+	}
+
+	/**
+	 * Notifies listeners that a “Peer” message was received.
+	 *
+	 * @see FcpListener#receivedPeer(FcpConnection, Peer)
+	 * @param peer
+	 *            The “Peer” message
+	 */
+	public void fireReceivedPeer(Peer peer) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPeer(getSource(), peer);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “EndListPeers” message was received.
+	 *
+	 * @see FcpListener#receivedEndListPeers(FcpConnection, EndListPeers)
+	 * @param endListPeers
+	 *            The “EndListPeers” message
+	 */
+	public void fireReceivedEndListPeers(EndListPeers endListPeers) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedEndListPeers(getSource(), endListPeers);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PeerNote” message was received.
+	 *
+	 * @see FcpListener#receivedPeerNote(FcpConnection, PeerNote)
+	 * @param peerNote
+	 *            The “PeerNote” message
+	 */
+	public void fireReceivedPeerNote(PeerNote peerNote) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPeerNote(getSource(), peerNote);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “EndListPeerNotes” message was received.
+	 *
+	 * @see FcpListener#receivedEndListPeerNotes(FcpConnection,
+	 *      EndListPeerNotes)
+	 * @param endListPeerNotes
+	 *            The “EndListPeerNotes” message
+	 */
+	public void fireReceivedEndListPeerNotes(EndListPeerNotes endListPeerNotes) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedEndListPeerNotes(getSource(), endListPeerNotes);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PeerRemoved” message was received.
+	 *
+	 * @see FcpListener#receivedPeerRemoved(FcpConnection, PeerRemoved)
+	 * @param peerRemoved
+	 *            The “PeerRemoved” message
+	 */
+	public void fireReceivedPeerRemoved(PeerRemoved peerRemoved) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPeerRemoved(getSource(), peerRemoved);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “NodeData” message was received.
+	 *
+	 * @see FcpListener#receivedNodeData(FcpConnection, NodeData)
+	 * @param nodeData
+	 *            The “NodeData” message
+	 */
+	public void fireReceivedNodeData(NodeData nodeData) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedNodeData(getSource(), nodeData);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “TestDDAReply” message was received.
+	 *
+	 * @see FcpListener#receivedTestDDAReply(FcpConnection, TestDDAReply)
+	 * @param testDDAReply
+	 *            The “TestDDAReply” message
+	 */
+	public void fireReceivedTestDDAReply(TestDDAReply testDDAReply) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedTestDDAReply(getSource(), testDDAReply);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “TestDDAComplete” message was received.
+	 *
+	 * @see FcpListener#receivedTestDDAComplete(FcpConnection, TestDDAComplete)
+	 * @param testDDAComplete
+	 *            The “TestDDAComplete” message
+	 */
+	public void fireReceivedTestDDAComplete(TestDDAComplete testDDAComplete) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedTestDDAComplete(getSource(), testDDAComplete);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PersistentGet” message was received.
+	 *
+	 * @see FcpListener#receivedPersistentGet(FcpConnection, PersistentGet)
+	 * @param persistentGet
+	 *            The “PersistentGet” message
+	 */
+	public void fireReceivedPersistentGet(PersistentGet persistentGet) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPersistentGet(getSource(), persistentGet);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PersistentPut” message was received.
+	 *
+	 * @see FcpListener#receivedPersistentPut(FcpConnection, PersistentPut)
+	 * @param persistentPut
+	 *            The “PersistentPut” message
+	 */
+	public void fireReceivedPersistentPut(PersistentPut persistentPut) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPersistentPut(getSource(), persistentPut);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “EndListPersistentRequests” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedEndListPersistentRequests(FcpConnection,
+	 *      EndListPersistentRequests)
+	 * @param endListPersistentRequests
+	 *            The “EndListPersistentRequests” message
+	 */
+	public void fireReceivedEndListPersistentRequests(EndListPersistentRequests endListPersistentRequests) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedEndListPersistentRequests(getSource(), endListPersistentRequests);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “URIGenerated” message was received.
+	 *
+	 * @see FcpListener#receivedURIGenerated(FcpConnection, URIGenerated)
+	 * @param uriGenerated
+	 *            The “URIGenerated” message
+	 */
+	public void fireReceivedURIGenerated(URIGenerated uriGenerated) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedURIGenerated(getSource(), uriGenerated);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “DataFound” message was received.
+	 *
+	 * @see FcpListener#receivedDataFound(FcpConnection, DataFound)
+	 * @param dataFound
+	 *            The “DataFound” message
+	 */
+	public void fireReceivedDataFound(DataFound dataFound) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedDataFound(getSource(), dataFound);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “AllData” message was received.
+	 *
+	 * @see FcpListener#receivedAllData(FcpConnection, AllData)
+	 * @param allData
+	 *            The “AllData” message
+	 */
+	public void fireReceivedAllData(AllData allData) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedAllData(getSource(), allData);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “SimpleProgress” message was received.
+	 *
+	 * @see FcpListener#receivedSimpleProgress(FcpConnection, SimpleProgress)
+	 * @param simpleProgress
+	 *            The “SimpleProgress” message
+	 */
+	public void fireReceivedSimpleProgress(SimpleProgress simpleProgress) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedSimpleProgress(getSource(), simpleProgress);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “StartedCompression” message was received.
+	 *
+	 * @see FcpListener#receivedStartedCompression(FcpConnection,
+	 *      StartedCompression)
+	 * @param startedCompression
+	 *            The “StartedCompression” message
+	 */
+	public void fireReceivedStartedCompression(StartedCompression startedCompression) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedStartedCompression(getSource(), startedCompression);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “FinishedCompression” message was received.
+	 *
+	 * @see FcpListener#receivedFinishedCompression(FcpConnection,
+	 *      FinishedCompression)
+	 * @param finishedCompression
+	 *            The “FinishedCompression” message
+	 */
+	public void fireReceivedFinishedCompression(FinishedCompression finishedCompression) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedFinishedCompression(getSource(), finishedCompression);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “UnknownPeerNoteType” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedUnknownPeerNoteType(FcpConnection,
+	 *      UnknownPeerNoteType)
+	 * @param unknownPeerNoteType
+	 *            The “UnknownPeerNoteType” message
+	 */
+	public void fireReceivedUnknownPeerNoteType(UnknownPeerNoteType unknownPeerNoteType) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedUnknownPeerNoteType(getSource(), unknownPeerNoteType);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “UnknownNodeIdentifier” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedUnknownNodeIdentifier(FcpConnection,
+	 *      UnknownNodeIdentifier)
+	 * @param unknownNodeIdentifier
+	 *            The “UnknownNodeIdentifier” message
+	 */
+	public void fireReceivedUnknownNodeIdentifier(UnknownNodeIdentifier unknownNodeIdentifier) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedUnknownNodeIdentifier(getSource(), unknownNodeIdentifier);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “ConfigData” message was received.
+	 *
+	 * @see FcpListener#receivedConfigData(FcpConnection, ConfigData)
+	 * @param configData
+	 *            The “ConfigData” message
+	 */
+	public void fireReceivedConfigData(ConfigData configData) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedConfigData(getSource(), configData);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “GetFailed” message was received.
+	 *
+	 * @see FcpListener#receivedGetFailed(FcpConnection, GetFailed)
+	 * @param getFailed
+	 *            The “GetFailed” message
+	 */
+	public void fireReceivedGetFailed(GetFailed getFailed) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedGetFailed(getSource(), getFailed);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PutFailed” message was received.
+	 *
+	 * @see FcpListener#receivedPutFailed(FcpConnection, PutFailed)
+	 * @param putFailed
+	 *            The “PutFailed” message
+	 */
+	public void fireReceivedPutFailed(PutFailed putFailed) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPutFailed(getSource(), putFailed);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “IdentifierCollision” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedIdentifierCollision(FcpConnection,
+	 *      IdentifierCollision)
+	 * @param identifierCollision
+	 *            The “IdentifierCollision” message
+	 */
+	public void fireReceivedIdentifierCollision(IdentifierCollision identifierCollision) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedIdentifierCollision(getSource(), identifierCollision);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “PersistentPutDir” message was received.
+	 *
+	 * @see FcpListener#receivedPersistentPutDir(FcpConnection,
+	 *      PersistentPutDir)
+	 * @param persistentPutDir
+	 *            The “PersistentPutDir” message
+	 */
+	public void fireReceivedPersistentPutDir(PersistentPutDir persistentPutDir) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPersistentPutDir(getSource(), persistentPutDir);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PersistentRequestRemoved” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedPersistentRequestRemoved(FcpConnection,
+	 *      PersistentRequestRemoved)
+	 * @param persistentRequestRemoved
+	 *            The “PersistentRequestRemoved” message
+	 */
+	public void fireReceivedPersistentRequestRemoved(PersistentRequestRemoved persistentRequestRemoved) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPersistentRequestRemoved(getSource(), persistentRequestRemoved);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “SubscribedUSKUpdate” message was received.
+	 *
+	 * @see FcpListener#receivedSubscribedUSKUpdate(FcpConnection,
+	 *      SubscribedUSKUpdate)
+	 * @param subscribedUSKUpdate
+	 *            The “SubscribedUSKUpdate” message
+	 */
+	public void fireReceivedSubscribedUSKUpdate(SubscribedUSKUpdate subscribedUSKUpdate) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedSubscribedUSKUpdate(getSource(), subscribedUSKUpdate);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PluginInfo” message was received.
+	 *
+	 * @see FcpListener#receivedPluginInfo(FcpConnection, PluginInfo)
+	 * @param pluginInfo
+	 *            The “PluginInfo” message
+	 */
+	public void fireReceivedPluginInfo(PluginInfo pluginInfo) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPluginInfo(getSource(), pluginInfo);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that an “FCPPluginReply” message was received.
+	 *
+	 * @see FcpListener#receivedFCPPluginReply(FcpConnection, FCPPluginReply)
+	 * @param fcpPluginReply
+	 *            The “FCPPluginReply” message
+	 */
+	public void fireReceivedFCPPluginReply(FCPPluginReply fcpPluginReply) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedFCPPluginReply(getSource(), fcpPluginReply);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PersistentRequestModified” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedPersistentRequestModified(FcpConnection,
+	 *      PersistentRequestModified)
+	 * @param persistentRequestModified
+	 *            The “PersistentRequestModified” message
+	 */
+	public void fireReceivedPersistentRequestModified(PersistentRequestModified persistentRequestModified) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPersistentRequestModified(getSource(), persistentRequestModified);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PutSuccessful” message was received.
+	 *
+	 * @see FcpListener#receivedPutSuccessful(FcpConnection, PutSuccessful)
+	 * @param putSuccessful
+	 *            The “PutSuccessful” message
+	 */
+	public void fireReceivedPutSuccessful(PutSuccessful putSuccessful) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPutSuccessful(getSource(), putSuccessful);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “PutFetchable” message was received.
+	 *
+	 * @see FcpListener#receivedPutFetchable(FcpConnection, PutFetchable)
+	 * @param putFetchable
+	 *            The “PutFetchable” message
+	 */
+	public void fireReceivedPutFetchable(PutFetchable putFetchable) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedPutFetchable(getSource(), putFetchable);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “ProtocolError” message was received.
+	 *
+	 * @see FcpListener#receivedProtocolError(FcpConnection, ProtocolError)
+	 * @param protocolError
+	 *            The “ProtocolError” message
+	 */
+	public void fireReceivedProtocolError(ProtocolError protocolError) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedProtocolError(getSource(), protocolError);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “SentFeed” message was received.
+	 *
+	 * @see FcpListener#receivedSentFeed(FcpConnection, SentFeed)
+	 * @param sentFeed
+	 *            The “SentFeed” message.
+	 */
+	public void fireSentFeed(SentFeed sentFeed) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedSentFeed(getSource(), sentFeed);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that a “ReceivedBookmarkFeed” message was
+	 * received.
+	 *
+	 * @see FcpListener#receivedBookmarkFeed(FcpConnection,
+	 *      ReceivedBookmarkFeed)
+	 * @param receivedBookmarkFeed
+	 *            The “ReceivedBookmarkFeed” message
+	 */
+	public void fireReceivedBookmarkFeed(ReceivedBookmarkFeed receivedBookmarkFeed) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedBookmarkFeed(getSource(), receivedBookmarkFeed);
+		}
+	}
+
+	/**
+	 * Notifies all registered listeners that a message has been received.
+	 *
+	 * @see FcpListener#receivedMessage(FcpConnection, FcpMessage)
+	 * @param fcpMessage
+	 *            The message that was received
+	 */
+	public void fireMessageReceived(FcpMessage fcpMessage) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.receivedMessage(getSource(), fcpMessage);
+		}
+	}
+
+	/**
+	 * Notifies all listeners that the connection to the node was closed.
+	 *
+	 * @param throwable
+	 *            The exception that caused the disconnect, or <code>null</code>
+	 *            if there was no exception
+	 * @see FcpListener#connectionClosed(FcpConnection, Throwable)
+	 */
+	public void fireConnectionClosed(Throwable throwable) {
+		for (FcpListener fcpListener : getListeners()) {
+			fcpListener.connectionClosed(getSource(), throwable);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpMessage.java b/alien/src/net/pterodactylus/fcp/FcpMessage.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpMessage.java
@@ -0,0 +1,195 @@
+/*
+ * jFCPlib - FcpMessage.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * An FCP message. FCP messages consist of a name, an arbitrary amount of
+ * “fields” (i.e. key-value pairs), a message end marker, and optional payload
+ * data that follows the marker.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpMessage implements Iterable<String> {
+
+	/** Constant for the linefeed. */
+	private static final String LINEFEED = "\r\n";
+
+	/** The name of the message. */
+	private final String name;
+
+	/** The fields of the message. */
+	private final Map<String, String> fields = new HashMap<String, String>();
+
+	/** The optional payload input stream. */
+	private InputStream payloadInputStream;
+
+	/**
+	 * Creates a new FCP message with the given name.
+	 *
+	 * @param name
+	 *            The name of the FCP message
+	 */
+	public FcpMessage(String name) {
+		this(name, null);
+	}
+
+	/**
+	 * Creates a new FCP message with the given name and the given payload input
+	 * stream. The payload input stream is not read until the message is sent to
+	 * the node using {@link FcpConnection#sendMessage(FcpMessage)}.
+	 *
+	 * @param name
+	 *            The name of the message
+	 * @param payloadInputStream
+	 *            The payload of the message
+	 */
+	public FcpMessage(String name, InputStream payloadInputStream) {
+		this.name = name;
+		this.payloadInputStream = payloadInputStream;
+	}
+
+	/**
+	 * Returns the name of the message.
+	 *
+	 * @return The name of the message
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Checks whether this message has a field with the given name.
+	 *
+	 * @param field
+	 *            The name of the field to check for
+	 * @return <code>true</code> if the message has a field with the given name,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean hasField(String field) {
+		return fields.containsKey(field);
+	}
+
+	/**
+	 * Sets the field with the given name to the given value. If the field
+	 * already exists in this message it is overwritten.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @param value
+	 *            The value of the field
+	 */
+	public void setField(String field, String value) {
+		if ((field == null) || (value == null)) {
+			throw new NullPointerException(((field == null) ? "field " : "value ") + "must not be null");
+		}
+		fields.put(field, value);
+	}
+
+	/**
+	 * Returns the value of the given field.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @return The value of the field, or <code>null</code> if there is no such
+	 *         field
+	 */
+	public String getField(String field) {
+		return fields.get(field);
+	}
+
+	/**
+	 * Returns all fields of this message.
+	 *
+	 * @return All fields of this message
+	 */
+	public Map<String, String> getFields() {
+		return Collections.unmodifiableMap(fields);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public Iterator<String> iterator() {
+		return fields.keySet().iterator();
+	}
+
+	/**
+	 * Sets the payload input stream of the message.
+	 *
+	 * @param payloadInputStream
+	 *            The payload input stream
+	 */
+	public void setPayloadInputStream(InputStream payloadInputStream) {
+		this.payloadInputStream = payloadInputStream;
+	}
+
+	/**
+	 * Writes this message to the given output stream. If the message has a
+	 * payload (i.e. {@link #payloadInputStream} is not <code>null</code>) the
+	 * payload is written to the given output stream after the message as well.
+	 * That means that this method can only be called once because on the second
+	 * invocation the payload input stream could not be read (again).
+	 *
+	 * @param outputStream
+	 *            The output stream to write the message to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void write(OutputStream outputStream) throws IOException {
+		writeLine(outputStream, name);
+		for (Entry<String, String> fieldEntry : fields.entrySet()) {
+			writeLine(outputStream, fieldEntry.getKey() + "=" + fieldEntry.getValue());
+		}
+		writeLine(outputStream, "EndMessage"); // DCI: Shouldn't this be "Data" for messages with trailing data?
+		outputStream.flush();
+		if (payloadInputStream != null) {
+			FcpUtils.copy(payloadInputStream, outputStream);
+			outputStream.flush();
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Writes the given line (followed by {@link #LINEFEED} to the given output
+	 * stream, using UTF-8 as encoding.
+	 *
+	 * @param outputStream
+	 *            The output stream to write to
+	 * @param line
+	 *            The line to write
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private void writeLine(OutputStream outputStream, String line) throws IOException {
+		outputStream.write((line + LINEFEED).getBytes("UTF-8"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FcpUtils.java b/alien/src/net/pterodactylus/fcp/FcpUtils.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FcpUtils.java
@@ -0,0 +1,479 @@
+/*
+ * jFCPlib - FcpUtils.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Helper class with utility methods for the FCP protocol.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpUtils {
+
+	/** Counter for unique identifiers. */
+	private static AtomicLong counter = new AtomicLong();
+
+	/**
+	 * Returns a unique identifier.
+	 *
+	 * @return A unique identifier
+	 */
+	public static String getUniqueIdentifier() {
+		return new StringBuilder().append(System.currentTimeMillis()).append('-').append(counter.getAndIncrement()).toString();
+	}
+
+	/**
+	 * Parses an integer field, separated by ‘;’ and returns the parsed values.
+	 *
+	 * @param field
+	 *            The field to parse
+	 * @return An array with the parsed values
+	 * @throws NumberFormatException
+	 *             if a value can not be converted to a number
+	 */
+	public static int[] decodeMultiIntegerField(String field) throws NumberFormatException {
+		StringTokenizer fieldTokens = new StringTokenizer(field, ";");
+		int[] result = new int[fieldTokens.countTokens()];
+		int counter = 0;
+		while (fieldTokens.hasMoreTokens()) {
+			String fieldToken = fieldTokens.nextToken();
+			result[counter++] = Integer.valueOf(fieldToken);
+		}
+		return result;
+	}
+
+	/**
+	 * Encodes the given integer array into a string, separating the values by
+	 * ‘;’.
+	 *
+	 * @param values
+	 *            The values to encode
+	 * @return The encoded values
+	 */
+	public static String encodeMultiIntegerField(int[] values) {
+		StringBuilder encodedField = new StringBuilder();
+		for (int value : values) {
+			if (encodedField.length() > 0) {
+				encodedField.append(';');
+			}
+			encodedField.append(value);
+		}
+		return encodedField.toString();
+	}
+
+	/**
+	 * Encodes the given string array into a string, separating the values by
+	 * ‘;’.
+	 *
+	 * @param values
+	 *            The values to encode
+	 * @return The encoded values
+	 */
+	public static String encodeMultiStringField(String[] values) {
+		StringBuilder encodedField = new StringBuilder();
+		for (String value : values) {
+			if (encodedField.length() > 0) {
+				encodedField.append(';');
+			}
+			encodedField.append(value);
+		}
+		return encodedField.toString();
+	}
+
+	/**
+	 * Tries to parse the given string into an int, returning <code>-1</code> if
+	 * the string can not be parsed.
+	 *
+	 * @param value
+	 *            The string to parse
+	 * @return The parsed int, or <code>-1</code>
+	 */
+	public static int safeParseInt(String value) {
+		return safeParseInt(value, -1);
+	}
+
+	/**
+	 * Tries to parse the given string into an int, returning
+	 * <code>defaultValue</code> if the string can not be parsed.
+	 *
+	 * @param value
+	 *            The string to parse
+	 * @param defaultValue
+	 *            The value to return if the string can not be parsed.
+	 * @return The parsed int, or <code>defaultValue</code>
+	 */
+	public static int safeParseInt(String value, int defaultValue) {
+		try {
+			return Integer.valueOf(value);
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Tries to parse the given string into an long, returning <code>-1</code>
+	 * if the string can not be parsed.
+	 *
+	 * @param value
+	 *            The string to parse
+	 * @return The parsed long, or <code>-1</code>
+	 */
+	public static long safeParseLong(String value) {
+		return safeParseLong(value, -1);
+	}
+
+	/**
+	 * Tries to parse the given string into an long, returning
+	 * <code>defaultValue</code> if the string can not be parsed.
+	 *
+	 * @param value
+	 *            The string to parse
+	 * @param defaultValue
+	 *            The value to return if the string can not be parsed.
+	 * @return The parsed long, or <code>defaultValue</code>
+	 */
+	public static long safeParseLong(String value, long defaultValue) {
+		try {
+			return Integer.valueOf(value);
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Closes the given socket.
+	 *
+	 * @param socket
+	 *            The socket to close
+	 */
+	public static void close(Socket socket) {
+		if (socket != null) {
+			try {
+				socket.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given Closeable.
+	 *
+	 * @param closeable
+	 *            The Closeable to close
+	 */
+	public static void close(Closeable closeable) {
+		if (closeable != null) {
+			try {
+				closeable.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Copies as many bytes as possible (i.e. until {@link InputStream#read()}
+	 * returns <code>-1</code>) from the source input stream to the destination
+	 * output stream.
+	 *
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void copy(InputStream source, OutputStream destination) throws IOException {
+		copy(source, destination, -1);
+	}
+
+	/**
+	 * Copies <code>length</code> bytes from the source input stream to the
+	 * destination output stream. If <code>length</code> is <code>-1</code> as
+	 * much bytes as possible will be copied (i.e. until
+	 * {@link InputStream#read()} returns <code>-1</code> to signal the end of
+	 * the stream).
+	 *
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @param length
+	 *            The number of bytes to copy
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void copy(InputStream source, OutputStream destination, long length) throws IOException {
+		copy(source, destination, length, 1 << 16);
+	}
+
+	/**
+	 * Copies <code>length</code> bytes from the source input stream to the
+	 * destination output stream. If <code>length</code> is <code>-1</code> as
+	 * much bytes as possible will be copied (i.e. until
+	 * {@link InputStream#read()} returns <code>-1</code> to signal the end of
+	 * the stream).
+	 *
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @param length
+	 *            The number of bytes to copy
+	 * @param bufferSize
+	 *            The buffer size
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void copy(InputStream source, OutputStream destination, long length, int bufferSize) throws IOException {
+            //System.err.println("COPY was called! " +source + " " + length + " " + bufferSize);
+            long count = 0;
+		long remaining = length;
+		byte[] buffer = new byte[bufferSize];
+		int read = 0;
+		while ((remaining == -1) || (remaining > 0)) {
+                    //                    System.err.println("Copy reading: " + (((remaining > bufferSize) || (remaining == -1)) ? bufferSize : (int) remaining));
+			read = source.read(buffer, 0, ((remaining > bufferSize) || (remaining == -1)) ? bufferSize : (int) remaining);
+			if (read == -1) {
+				if (length == -1) {
+                                    //                                    System.err.println("COPY, exited: " + count);
+					return;
+				}
+                                //System.err.println("COPY, raising!");
+				throw new EOFException("stream reached eof");
+			}
+                        //System.err.println("COPY, writing: " + read);
+			destination.write(buffer, 0, read);
+                        //System.err.println("COPY, wrote: " + read);
+                        if (remaining != -1) {
+                            remaining -= read; // DCI: BUGFIX, keep this when cleaning up other crap.
+                        }
+                        count += read;
+		}
+	}
+
+	/**
+	 * This input stream stores the content of another input stream either in a
+	 * file or in memory, depending on the length of the input stream.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	public static class TempInputStream extends InputStream {
+
+		/** The default maximum lenght for in-memory storage. */
+		public static final long MAX_LENGTH_MEMORY = 65536;
+
+		/** The temporary file to read from. */
+		private final File tempFile;
+
+		/** The input stream that reads from the file. */
+		private final InputStream fileInputStream;
+
+		/** The input stream that reads from memory. */
+		private final InputStream memoryInputStream;
+
+		/**
+		 * Creates a new temporary input stream that stores the given input
+		 * stream in a temporary file.
+		 *
+		 * @param originalInputStream
+		 *            The original input stream
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 */
+		public TempInputStream(InputStream originalInputStream) throws IOException {
+			this(originalInputStream, -1);
+		}
+
+		/**
+		 * Creates a new temporary input stream that stores the given input
+		 * stream in memory if it is shorter than {@link #MAX_LENGTH_MEMORY},
+		 * otherwise it is stored in a file.
+		 *
+		 * @param originalInputStream
+		 *            The original input stream
+		 * @param length
+		 *            The length of the input stream
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 */
+		public TempInputStream(InputStream originalInputStream, long length) throws IOException {
+			this(originalInputStream, length, MAX_LENGTH_MEMORY);
+		}
+
+		/**
+		 * Creates a new temporary input stream that stores the given input
+		 * stream in memory if it is shorter than <code>maxMemoryLength</code>,
+		 * otherwise it is stored in a file.
+		 *
+		 * @param originalInputStream
+		 *            The original input stream
+		 * @param length
+		 *            The length of the input stream
+		 * @param maxMemoryLength
+		 *            The maximum length to store in memory
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 */
+		public TempInputStream(InputStream originalInputStream, long length, long maxMemoryLength) throws IOException {
+			if ((length > -1) && (length <= maxMemoryLength)) {
+				ByteArrayOutputStream memoryOutputStream = new ByteArrayOutputStream((int) length);
+				try {
+					FcpUtils.copy(originalInputStream, memoryOutputStream, length, (int) length);
+				} finally {
+					memoryOutputStream.close();
+				}
+				tempFile = null;
+				fileInputStream = null;
+				memoryInputStream = new ByteArrayInputStream(memoryOutputStream.toByteArray());
+			} else {
+				tempFile = File.createTempFile("temp-", ".bin");
+				tempFile.deleteOnExit();
+				FileOutputStream fileOutputStream = null;
+				try {
+					fileOutputStream = new FileOutputStream(tempFile);
+					FcpUtils.copy(originalInputStream, fileOutputStream);
+					fileInputStream = new FileInputStream(tempFile);
+				} finally {
+					FcpUtils.close(fileOutputStream);
+				}
+				memoryInputStream = null;
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public int available() throws IOException {
+			if (memoryInputStream != null) {
+				return memoryInputStream.available();
+			}
+			return fileInputStream.available();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void close() throws IOException {
+			if (memoryInputStream != null) {
+				memoryInputStream.close();
+				return;
+			}
+			tempFile.delete();
+			fileInputStream.close();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public synchronized void mark(int readlimit) {
+			if (memoryInputStream != null) {
+				memoryInputStream.mark(readlimit);
+				return;
+			}
+			fileInputStream.mark(readlimit);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean markSupported() {
+			if (memoryInputStream != null) {
+				return memoryInputStream.markSupported();
+			}
+			return fileInputStream.markSupported();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public int read() throws IOException {
+			if (memoryInputStream != null) {
+				return memoryInputStream.read();
+			}
+			return fileInputStream.read();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public int read(byte[] b) throws IOException {
+			if (memoryInputStream != null) {
+				return memoryInputStream.read(b);
+			}
+			return fileInputStream.read(b);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public int read(byte[] b, int off, int len) throws IOException {
+			if (memoryInputStream != null) {
+				return memoryInputStream.read(b, off, len);
+			}
+			return fileInputStream.read(b, off, len);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public synchronized void reset() throws IOException {
+			if (memoryInputStream != null) {
+				memoryInputStream.reset();
+				return;
+			}
+			fileInputStream.reset();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public long skip(long n) throws IOException {
+			if (memoryInputStream != null) {
+				return memoryInputStream.skip(n);
+			}
+			return fileInputStream.skip(n);
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FileEntry.java b/alien/src/net/pterodactylus/fcp/FileEntry.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FileEntry.java
@@ -0,0 +1,294 @@
+/*
+ * jFCPlib - FileEntry.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Container class for file entry data.
+ *
+ * @see ClientPutComplexDir#addFileEntry(FileEntry)
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public abstract class FileEntry {
+
+	/** The name of the file. */
+	protected final String name;
+
+	/** The upload source of the file. */
+	protected final UploadFrom uploadFrom;
+
+	/**
+	 * Creates a new file entry with the given name and upload source.
+	 *
+	 * @param name
+	 *            The name of the file
+	 * @param uploadFrom
+	 *            The upload source of the file
+	 */
+	protected FileEntry(String name, UploadFrom uploadFrom) {
+		this.name = name;
+		this.uploadFrom = uploadFrom;
+	}
+
+	/**
+	 * Creates a new file entry for a file that should be transmitted to the
+	 * node in the payload of the message.
+	 *
+	 * @param name
+	 *            The name of the file
+	 * @param contentType
+	 *            The content type of the file, or <code>null</code> to let the
+	 *            node auto-detect it
+	 * @param length
+	 *            The length of the file
+	 * @param dataInputStream
+	 *            The input stream of the file
+	 * @return A file entry
+	 */
+	public static FileEntry createDirectFileEntry(String name, String contentType, long length, InputStream dataInputStream) {
+		return new DirectFileEntry(name, contentType, length, dataInputStream);
+	}
+
+	/**
+	 * Creates a new file entry for a file that should be uploaded from disk.
+	 *
+	 * @param name
+	 *            The name of the file
+	 * @param filename
+	 *            The name of the file on disk
+	 * @param contentType
+	 *            The content type of the file, or <code>null</code> to let the
+	 *            node auto-detect it
+	 * @param length
+	 *            The length of the file, or <code>-1</code> to not specify a
+	 *            size
+	 * @return A file entry
+	 */
+	public static FileEntry createDiskFileEntry(String name, String filename, String contentType, long length) {
+		return new DiskFileEntry(name, filename, contentType, length);
+	}
+
+	/**
+	 * Creates a new file entry for a file that redirects to another URI.
+	 *
+	 * @param name
+	 *            The name of the file
+	 * @param targetURI
+	 *            The target URI of the redirect
+	 * @return A file entry
+	 */
+	public static FileEntry createRedirectFileEntry(String name, String targetURI) {
+		return new RedirectFileEntry(name, targetURI);
+	}
+
+	/**
+	 * Returns the fields for this file entry.
+	 *
+	 * @return The fields for this file entry
+	 */
+	abstract Map<String, String> getFields();
+
+	/**
+	 * A file entry for a file that should be transmitted in the payload of the
+	 * {@link ClientPutComplexDir} message.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	static class DirectFileEntry extends FileEntry {
+
+		/** The content type of the data. */
+		private final String contentType;
+
+		/** The length of the data. */
+		private final long length;
+
+		/** The input stream of the data. */
+		private final InputStream inputStream;
+
+		/**
+		 * Creates a new direct file entry with content type auto-detection.
+		 *
+		 * @param name
+		 *            The name of the file
+		 * @param length
+		 *            The length of the file
+		 * @param inputStream
+		 *            The input stream of the file
+		 */
+		public DirectFileEntry(String name, long length, InputStream inputStream) {
+			this(name, null, length, inputStream);
+		}
+
+		/**
+		 * Creates a new direct file entry.
+		 *
+		 * @param name
+		 *            The name of the file
+		 * @param contentType
+		 *            The content type of the file, or <code>null</code> to let
+		 *            the node auto-detect it
+		 * @param length
+		 *            The length of the file
+		 * @param inputStream
+		 *            The input stream of the file
+		 */
+		public DirectFileEntry(String name, String contentType, long length, InputStream inputStream) {
+			super(name, UploadFrom.direct);
+			this.contentType = contentType;
+			this.length = length;
+			this.inputStream = inputStream;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		Map<String, String> getFields() {
+			Map<String, String> fields = new HashMap<String, String>();
+			fields.put("Name", name);
+			fields.put("UploadFrom", String.valueOf(uploadFrom));
+			fields.put("DataLength", String.valueOf(length));
+			if (contentType != null) {
+				fields.put("Metadata.ContentType", contentType);
+			}
+			return fields;
+		}
+
+		/**
+		 * Returns the input stream of the file.
+		 *
+		 * @return The input stream of the file
+		 */
+		InputStream getInputStream() {
+			return inputStream;
+		}
+
+	}
+
+	/**
+	 * A file entry for a file that should be uploaded from the disk.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	static class DiskFileEntry extends FileEntry {
+
+		/** The name of the on-disk file. */
+		private final String filename;
+
+		/** The content type of the file. */
+		private final String contentType;
+
+		/** The length of the file. */
+		private final long length;
+
+		/**
+		 * Creates a new disk file entry.
+		 *
+		 * @param name
+		 *            The name of the file
+		 * @param filename
+		 *            The name of the on-disk file
+		 * @param length
+		 *            The length of the file
+		 */
+		public DiskFileEntry(String name, String filename, long length) {
+			this(name, filename, null, length);
+		}
+
+		/**
+		 * Creates a new disk file entry.
+		 *
+		 * @param name
+		 *            The name of the file
+		 * @param filename
+		 *            The name of the on-disk file
+		 * @param contentType
+		 *            The content type of the file, or <code>null</code> to let
+		 *            the node auto-detect it
+		 * @param length
+		 *            The length of the file
+		 */
+		public DiskFileEntry(String name, String filename, String contentType, long length) {
+			super(name, UploadFrom.disk);
+			this.filename = filename;
+			this.contentType = contentType;
+			this.length = length;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		Map<String, String> getFields() {
+			Map<String, String> fields = new HashMap<String, String>();
+			fields.put("Name", name);
+			fields.put("UploadFrom", String.valueOf(uploadFrom));
+			fields.put("Filename", filename);
+			if (length > -1) {
+				fields.put("DataSize", String.valueOf(length));
+			}
+			if (contentType != null) {
+				fields.put("Metadata.ContentType", contentType);
+			}
+			return fields;
+		}
+
+	}
+
+	/**
+	 * A file entry for a file that redirects to another URI.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	static class RedirectFileEntry extends FileEntry {
+
+		/** The target URI of the redirect. */
+		private String targetURI;
+
+		/**
+		 * Creates a new redirect file entry.
+		 *
+		 * @param name
+		 *            The name of the file
+		 * @param targetURI
+		 *            The target URI of the redirect
+		 */
+		public RedirectFileEntry(String name, String targetURI) {
+			super(name, UploadFrom.redirect);
+			this.targetURI = targetURI;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		Map<String, String> getFields() {
+			Map<String, String> fields = new HashMap<String, String>();
+			fields.put("Name", name);
+			fields.put("UploadFrom", String.valueOf(uploadFrom));
+			fields.put("TargetURI", targetURI);
+			return fields;
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/FinishedCompression.java b/alien/src/net/pterodactylus/fcp/FinishedCompression.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/FinishedCompression.java
@@ -0,0 +1,76 @@
+/*
+ * jFCPlib - FinishedCompression.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “FinishedCompression” message signals the client that the compression of
+ * the request data has been finished.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FinishedCompression extends BaseMessage {
+
+	/**
+	 * Creates a new “FinishedCompression” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The message that was recevied
+	 */
+	FinishedCompression(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the ID of the codec that was used for compression.
+	 *
+	 * @return The ID of the codec that was used for compression
+	 */
+	public int getCodec() {
+		return FcpUtils.safeParseInt(getField("Codec"));
+	}
+
+	/**
+	 * Returns the original size of the data (i.e. before compression).
+	 *
+	 * @return The original size of the data
+	 */
+	public long getOriginalSize() {
+		return FcpUtils.safeParseLong(getField("OriginalSize"));
+	}
+
+	/**
+	 * Returns the compressed size of the data (i.e. after compression).
+	 *
+	 * @return The compressed size of the data
+	 */
+	public long getCompressedSize() {
+		return FcpUtils.safeParseLong(getField("CompressedSize"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GenerateSSK.java b/alien/src/net/pterodactylus/fcp/GenerateSSK.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GenerateSSK.java
@@ -0,0 +1,47 @@
+/*
+ * jFCPlib - GenerateSSK.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “GenerateSSK” message. This message tells the node to generate a new SSK
+ * key pair.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GenerateSSK extends FcpMessage {
+
+	/**
+	 * Creates a new “GenerateSSK” message.
+	 */
+	public GenerateSSK() {
+		this(FcpUtils.getUniqueIdentifier());
+	}
+
+	/**
+	 * Creates a new “GenerateSSK” message with the given client identifier.
+	 *
+	 * @param clientIdentifier
+	 *            The client identifier
+	 */
+	public GenerateSSK(String clientIdentifier) {
+		super("GenerateSSK");
+		setField("Identifier", clientIdentifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GetConfig.java b/alien/src/net/pterodactylus/fcp/GetConfig.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GetConfig.java
@@ -0,0 +1,132 @@
+/*
+ * jFCPlib - GetConfig.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “GetConfig” command tells the node to send its configuration to the
+ * client.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetConfig extends FcpMessage {
+
+	/**
+	 * Creates a new “GetConfig” command.
+	 */
+	public GetConfig() {
+		super("GetConfig");
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the
+	 * current values.
+	 *
+	 * @param withCurrent
+	 *            <code>true</code> to include current values in the result,
+	 *            <code>false</code> otherwise
+	 */
+	public void setWithCurrent(boolean withCurrent) {
+		setField("WithCurrent", String.valueOf(withCurrent));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the
+	 * short descriptions.
+	 *
+	 * @param withShortDescription
+	 *            <code>true</code> to include the short descriptions in the
+	 *            result, <code>false</code> otherwise
+	 */
+	public void setWithShortDescription(boolean withShortDescription) {
+		setField("WithShortDescription", String.valueOf(withShortDescription));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the long
+	 * descriptions.
+	 *
+	 * @param withLongDescription
+	 *            <code>true</code> to include the long descriptions in the
+	 *            result, <code>false</code> otherwise
+	 */
+	public void setWithLongDescription(boolean withLongDescription) {
+		setField("WithLongDescription", String.valueOf(withLongDescription));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the data
+	 * types.
+	 *
+	 * @param withDataTypes
+	 *            <code>true</code> to include the data types in the result,
+	 *            <code>false</code> otherwise
+	 */
+	public void setWithDataTypes(boolean withDataTypes) {
+		setField("WithDataTypes", String.valueOf(withDataTypes));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the
+	 * defaults.
+	 *
+	 * @param setWithDefaults
+	 *            <code>true</code> to include the defaults in the result,
+	 *            <code>false</code> otherwise
+	 */
+	public void setWithDefaults(boolean setWithDefaults) {
+		setField("WithDefaults", String.valueOf(setWithDefaults));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the sort
+	 * order.
+	 *
+	 * @param withSortOrder
+	 *            <code>true</code> to include the sort order in the result,
+	 *            <code>false</code> otherwise
+	 */
+	public void setWithSortOrder(boolean withSortOrder) {
+		setField("WithSortOrder", String.valueOf(withSortOrder));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the
+	 * expert flag.
+	 *
+	 * @param withExpertFlag
+	 *            <code>true</code> to include the expert flag in the result,
+	 *            <code>false</code> otherwise
+	 */
+	public void setWithExpertFlag(boolean withExpertFlag) {
+		setField("WithExpertFlag", String.valueOf(withExpertFlag));
+	}
+
+	/**
+	 * Sets whether the {@link ConfigData} result message shall include the
+	 * force-write flag.
+	 *
+	 * @param withForceWriteFlag
+	 *            <code>true</code> to include the force-write flag in the
+	 *            result, <code>false</code> otherwise
+	 */
+	public void setWithForceWriteFlag(boolean withForceWriteFlag) {
+		setField("WithForceWriteFlag", String.valueOf(withForceWriteFlag));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GetFailed.java b/alien/src/net/pterodactylus/fcp/GetFailed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GetFailed.java
@@ -0,0 +1,210 @@
+/*
+ * jFCPlib - GetFailed.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * The “GetFailed” message signals the client that a {@link ClientGet} request
+ * has failed. This also means that no further progress messages for that
+ * request will be sent.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetFailed extends BaseMessage {
+
+	/**
+	 * Creates a new “GetFailed” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	GetFailed(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the code of the error.
+	 *
+	 * @return The code of the error, or <code>-1</code> if the error code could
+	 *         not be parsed
+	 */
+	public int getCode() {
+		return FcpUtils.safeParseInt(getField("Code"));
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the description of the error code.
+	 *
+	 * @return The description of the error code
+	 */
+	public String getCodeDescription() {
+		return getField("CodeDescription");
+	}
+
+	/**
+	 * Returns the extra description of the error.
+	 *
+	 * @return The extra description of the error
+	 */
+	public String getExtraDescription() {
+		return getField("ExtraDescription");
+	}
+
+	/**
+	 * Returns the short description of the error.
+	 *
+	 * @return The short description of the error
+	 */
+	public String getShortCodeDescription() {
+		return getField("ShortCodeDescription");
+	}
+
+	/**
+	 * Returns the expected data length, if already knows.
+	 *
+	 * @return The expected data length, or <code>-1</code> if the length could
+	 *         not be parsed
+	 */
+	public long getExpectedDataLength() {
+		return FcpUtils.safeParseLong(getField("ExpectedDataLength"));
+	}
+
+	/**
+	 * Returns the expected content type of the request.
+	 *
+	 * @return The expected content type
+	 */
+	public String getExpectedMetadataContentType() {
+		return getField("ExpectedMetadata.ContentType");
+	}
+
+	/**
+	 * Returns whether the expected values (see {@link #getExpectedDataLength()}
+	 * and {@link #getExpectedMetadataContentType()}) have already been
+	 * finalized and can be trusted. If the values have not been finalized that
+	 * can change over time.
+	 *
+	 * @return <code>true</code> if the expected values have already been
+	 *         finalized, <code>false</code> otherwise
+	 */
+	public boolean isFinalizedExpected() {
+		return Boolean.valueOf(getField("FinalizedExpected"));
+	}
+
+	/**
+	 * Returns the URI the request is redirected to (in case of a request for a
+	 * USK). This is returned so that client applications know that the URI of
+	 * the key has updated.
+	 *
+	 * @return The URI the request was redirected to
+	 */
+	public String getRedirectURI() {
+		return getField("RedirectURI");
+	}
+
+	/**
+	 * Returns whether the request failed fatally. If a request fails fatally it
+	 * can never complete, even with inifinite retries.
+	 *
+	 * @return <code>true</code> if the request failed fatally,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isFatal() {
+		return Boolean.valueOf(getField("Fatal"));
+	}
+
+	/**
+	 * Returns a list of complex error codes with the message. Use
+	 * {@link #getComplexErrorDescription(int)} and
+	 * {@link #getComplexErrorCount(int)} to get details.
+	 *
+	 * @return A list of complex error codes
+	 */
+	public int[] getComplexErrorCodes() {
+		Map<String, String> allFields = getFields();
+		List<Integer> errorCodeList = new ArrayList<Integer>();
+		for (Entry<String, String> field : allFields.entrySet()) {
+			String fieldKey = field.getKey();
+			if (fieldKey.startsWith("Errors.")) {
+				int nextDot = fieldKey.indexOf('.', 7);
+				if (nextDot > -1) {
+					int errorCode = FcpUtils.safeParseInt(fieldKey.substring(7, nextDot));
+					if (errorCode != -1) {
+						errorCodeList.add(errorCode);
+					}
+				}
+			}
+		}
+		int[] errorCodes = new int[errorCodeList.size()];
+		int errorIndex = 0;
+		for (int errorCode : errorCodeList) {
+			errorCodes[errorIndex++] = errorCode;
+		}
+		return errorCodes;
+	}
+
+	/**
+	 * Returns the description of the complex error. You should only hand it
+	 * error codes you got from {@link #getComplexErrorCodes()}!
+	 *
+	 * @param errorCode
+	 *            The error code
+	 * @return The description of the complex error
+	 */
+	public String getComplexErrorDescription(int errorCode) {
+		return getField("Errors." + errorCode + ".Description");
+	}
+
+	/**
+	 * Returns the count of the complex error. You should only hand it error
+	 * codes you got from {@link #getComplexErrorCodes()}!
+	 *
+	 * @param errorCode
+	 *            The error code
+	 * @return The count of the complex error, or <code>-1</code> if the count
+	 *         could not be parsed
+	 */
+	public int getComplexErrorCount(int errorCode) {
+		return FcpUtils.safeParseInt(getField("Errors." + errorCode + ".Count"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GetNode.java b/alien/src/net/pterodactylus/fcp/GetNode.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GetNode.java
@@ -0,0 +1,63 @@
+/*
+ * jFCPlib - GetNode.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “GetNode” command returns the darknet or opennet noderef of the node,
+ * optionally including private and volatile data.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetNode extends FcpMessage {
+
+	/**
+	 * Creates a “GetNode” command that returns the darknet noderef of the node.
+	 */
+	public GetNode() {
+		this(null, null, null);
+	}
+
+	/**
+	 * Creates a “GetNode” command that returns the request noderef of the node,
+	 * including private and volatile data, if requested. If any of the Boolean
+	 * parameters are <code>null</code> the parameter is ignored and the node’s
+	 * default value is used.
+	 *
+	 * @param giveOpennetRef
+	 *            <code>true</code> to request the opennet noderef,
+	 *            <code>false</code> for darknet
+	 * @param withPrivate
+	 *            <code>true</code> to include private data in the noderef
+	 * @param withVolatile
+	 *            <code>true</code> to include volatile data in the noderef
+	 */
+	public GetNode(Boolean giveOpennetRef, Boolean withPrivate, Boolean withVolatile) {
+		super("GetNode");
+		if (giveOpennetRef != null) {
+			setField("GiveOpennetRef", String.valueOf(giveOpennetRef));
+		}
+		if (withPrivate != null) {
+			setField("WithPrivate", String.valueOf(withPrivate));
+		}
+		if (withVolatile != null) {
+			setField("WithVolatile", String.valueOf(withVolatile));
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GetPluginInfo.java b/alien/src/net/pterodactylus/fcp/GetPluginInfo.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GetPluginInfo.java
@@ -0,0 +1,54 @@
+/*
+ * jFCPlib - GetPluginInfo.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “GetPluginInfo” message requests information about a plugin from the
+ * node, which will response with a {@link PluginInfo} message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetPluginInfo extends FcpMessage {
+
+	/**
+	 * Creates a new “GetPluginInfo” message.
+	 *
+	 * @param pluginName
+	 *            The name of the plugin
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public GetPluginInfo(String pluginName, String identifier) {
+		super("GetPluginInfo");
+		setField("PluginName", pluginName);
+		setField("Identifier", identifier);
+	}
+
+	/**
+	 * Sets whether detailed information about the plugin is wanted.
+	 *
+	 * @param detailed
+	 *            <code>true</code> to request detailed information about the
+	 *            plugin, <code>false</code> otherwise
+	 */
+	public void setDetailed(boolean detailed) {
+		setField("Detailed", String.valueOf(detailed));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/GetRequestStatus.java b/alien/src/net/pterodactylus/fcp/GetRequestStatus.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/GetRequestStatus.java
@@ -0,0 +1,64 @@
+/*
+ * jFCPlib - GetRequestStatus.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “GetRequestStatus” message is used request status information about a
+ * running request. It is also the only way to trigger a download of a persisted
+ * completed {@link ClientGet} with a return type of {@link ReturnType#direct}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetRequestStatus extends FcpMessage {
+
+	/**
+	 * Creates a new “GetRequestStatus” message.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public GetRequestStatus(String identifier) {
+		super("GetRequestStatus");
+		setField("Identifier", identifier);
+	}
+
+	/**
+	 * Sets whether the request is on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> if the request is on the global queue,
+	 *            <code>false</code> if it is on the client-local queue
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Sets whether the omit the transmission of the request data in a
+	 * {@link AllData} message.
+	 *
+	 * @param onlyData
+	 *            <code>true</code> to skip transmission of data,
+	 *            <code>false</code> to download data
+	 */
+	public void setOnlyData(boolean onlyData) {
+		setField("OnlyData", String.valueOf(onlyData));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/IdentifierCollision.java b/alien/src/net/pterodactylus/fcp/IdentifierCollision.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/IdentifierCollision.java
@@ -0,0 +1,59 @@
+/*
+ * jFCPlib - IdentifierCollision.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “IdentifierCollision” message signals the client that the identifier
+ * chosen for a request is already existing.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class IdentifierCollision extends BaseMessage {
+
+	/**
+	 * Creates a new “IdentifierCollision” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	IdentifierCollision(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ListPeer.java b/alien/src/net/pterodactylus/fcp/ListPeer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ListPeer.java
@@ -0,0 +1,42 @@
+/*
+ * jFCPlib - ListPeer.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ListPeer” request asks the node about the details of a given peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ListPeer extends FcpMessage {
+
+	/**
+	 * Creates a new “ListPeer” request that returns information about the node
+	 * specified by <code>nodeIdentifier</code>. <code>nodeIdentifier</code> can
+	 * be of several formats: The node’s name, its identity, or its IP address
+	 * and port (connection with a ‘:’).
+	 *
+	 * @param nodeIdentifier
+	 *            The identifier of the node to get details about
+	 */
+	public ListPeer(String nodeIdentifier) {
+		super("ListPeer");
+		setField("NodeIdentifier", nodeIdentifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ListPeerNotes.java b/alien/src/net/pterodactylus/fcp/ListPeerNotes.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ListPeerNotes.java
@@ -0,0 +1,41 @@
+/*
+ * jFCPlib - ListPeerNotes.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ListPeerNodes” request tells the node to list all notes that have been
+ * entered for a node. Note that notes are only supported for darknet nodes.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ListPeerNotes extends FcpMessage {
+
+	/**
+	 * Creates a new “ListPeerNotes” request that lists all notes of the
+	 * specified node.
+	 *
+	 * @param nodeIdentifier
+	 *            The identifier of the node
+	 */
+	public ListPeerNotes(String nodeIdentifier) {
+		super("ListPeerNotes");
+		setField("NodeIdentifier", nodeIdentifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ListPeers.java b/alien/src/net/pterodactylus/fcp/ListPeers.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ListPeers.java
@@ -0,0 +1,58 @@
+/*
+ * jFCPlib - ListPeers.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ListPeer” requests asks the node for a list of all peers it has.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ListPeers extends FcpMessage {
+
+	/**
+	 * Creates a new “ListPeers” request that only includes basic data of the
+	 * peers.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public ListPeers(String identifier) {
+		this(identifier, false, false);
+	}
+
+	/**
+	 * Creates a new “ListPeers” request that includes wanted data of the peers.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param withMetadata
+	 *            If <code>true</code> metadata of the peers is included in the
+	 *            reply
+	 * @param withVolatile
+	 *            if <code>true</code> volatile data of the peers is included in
+	 *            the reply
+	 */
+	public ListPeers(String identifier, boolean withMetadata, boolean withVolatile) {
+		super("ListPeers");
+		setField("Identifier", identifier);
+		setField("WithMetadata", String.valueOf(withMetadata));
+		setField("WithVolatile", String.valueOf(withVolatile));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ListPersistentRequests.java b/alien/src/net/pterodactylus/fcp/ListPersistentRequests.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ListPersistentRequests.java
@@ -0,0 +1,38 @@
+/*
+ * jFCPlib - ListPersistentRequests.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Command to tell the node to list all persistent requests from the current
+ * queue, which is either the global queue or the client-local queue, depending
+ * on your {@link WatchGlobal} status.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ListPersistentRequests extends FcpMessage {
+
+	/**
+	 * Creates a new “ListPersistentRequests” command that lists all persistent
+	 * requests in the current queue.
+	 */
+	public ListPersistentRequests() {
+		super("ListPersistentRequests");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ModifyConfig.java b/alien/src/net/pterodactylus/fcp/ModifyConfig.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ModifyConfig.java
@@ -0,0 +1,50 @@
+/*
+ * jFCPlib - ModifyConfig.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ModifyConfig” message is used to change the node’s configuration.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ModifyConfig extends FcpMessage {
+
+	/**
+	 * Creates a new “ModifyConfig” message.
+	 */
+	public ModifyConfig() {
+		super("ModifyConfig");
+	}
+
+	/**
+	 * Sets the option with the given name to the given value.
+	 *
+	 * @param option
+	 *            The name of the option
+	 * @param value
+	 *            The value of the option
+	 */
+	public void setOption(String option, String value) {
+		if (option.indexOf('.') == -1) {
+			throw new IllegalArgumentException("invalid option name");
+		}
+		setField(option, value);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ModifyPeer.java b/alien/src/net/pterodactylus/fcp/ModifyPeer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ModifyPeer.java
@@ -0,0 +1,56 @@
+/*
+ * jFCPlib - ModifyPeer.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ModifyPeer” request lets you modify certain properties of a peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ModifyPeer extends FcpMessage {
+
+	/**
+	 * Creates a new “ModifyPeer” request. All Boolean parameters may be null to
+	 * not influence the current setting.
+	 *
+	 * @param nodeIdentifier
+	 *            The identifier of the node, i.e. name, identity, or IP address
+	 *            and port
+	 * @param allowLocalAddresses
+	 *            Whether to allow local addresses from this node
+	 * @param disabled
+	 *            Whether the node is disabled
+	 * @param listenOnly
+	 *            Whether your node should not try to connect the node
+	 */
+	public ModifyPeer(String nodeIdentifier, Boolean allowLocalAddresses, Boolean disabled, Boolean listenOnly) {
+		super("ModifyPeer");
+		setField("NodeIdentifier", nodeIdentifier);
+		if (allowLocalAddresses != null) {
+			setField("AllowLocalAddresses", String.valueOf(allowLocalAddresses));
+		}
+		if (disabled != null) {
+			setField("IsDisabled", String.valueOf(disabled));
+		}
+		if (listenOnly != null) {
+			setField("IsListenOnly", String.valueOf(listenOnly));
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ModifyPeerNote.java b/alien/src/net/pterodactylus/fcp/ModifyPeerNote.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ModifyPeerNote.java
@@ -0,0 +1,49 @@
+/*
+ * jFCPlib - ModifyPeerNote.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ModifyPeerNote” command modifies a peer note.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ModifyPeerNote extends FcpMessage {
+
+	/**
+	 * Creates a new “ModifyPeerNote” request that changes peer note of the
+	 * given type and node to the given text.
+	 *
+	 * @see PeerNote
+	 * @param nodeIdentifier
+	 *            The identifier of the node, i.e. name, identity, or IP address
+	 *            and port
+	 * @param noteText
+	 *            The base64-encoded text
+	 * @param peerNoteType
+	 *            The type of the note to change, possible values are only
+	 *            {@link PeerNote#TYPE_PRIVATE_PEER_NOTE} at the moment
+	 */
+	public ModifyPeerNote(String nodeIdentifier, String noteText, int peerNoteType) {
+		super("ModifyPeer");
+		setField("NodeIdentifier", nodeIdentifier);
+		setField("NoteText", noteText);
+		setField("PeerNoteType", String.valueOf(peerNoteType));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ModifyPersistentRequest.java b/alien/src/net/pterodactylus/fcp/ModifyPersistentRequest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ModifyPersistentRequest.java
@@ -0,0 +1,65 @@
+/*
+ * jFCPlib - ModifyPersistentRequest.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “ModifyPersistentRequest” is used to modify certain properties of a
+ * persistent request while it is running.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ModifyPersistentRequest extends FcpMessage {
+
+	/**
+	 * Creates a new “ModifyPersistentRequest” that changes the specified
+	 * request.
+	 *
+	 * @param requestIdentifier
+	 *            The identifier of the request
+	 * @param global
+	 *            <code>true</code> if the request is on the global queue,
+	 *            <code>false</code> if it is on the client-local queue
+	 */
+	public ModifyPersistentRequest(String requestIdentifier, boolean global) {
+		super("ModifyPersistentRequest");
+		setField("Identifier", requestIdentifier);
+		setField("Global", String.valueOf(global));
+	}
+
+	/**
+	 * Sets the new client token of the request.
+	 *
+	 * @param newClientToken
+	 *            The new client token of the request
+	 */
+	public void setClientToken(String newClientToken) {
+		setField("ClientToken", newClientToken);
+	}
+
+	/**
+	 * Sets the new priority of the request.
+	 *
+	 * @param newPriority
+	 *            The new priority of the request
+	 */
+	public void setPriority(Priority newPriority) {
+		setField("PriorityClass", String.valueOf(newPriority));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/NodeData.java b/alien/src/net/pterodactylus/fcp/NodeData.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/NodeData.java
@@ -0,0 +1,176 @@
+/*
+ * jFCPlib - NodeData.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “NodeData” contains the noderef of the node, along with additional data.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class NodeData extends BaseMessage {
+
+	/** The noderef of the node. */
+	private final NodeRef nodeRef;
+
+	/**
+	 * Creates a new “NodeData” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	NodeData(FcpMessage receivedMessage) {
+		super(receivedMessage);
+		nodeRef = new NodeRef(receivedMessage);
+	}
+
+	/**
+	 * Returns the noderef of the node.
+	 *
+	 * @return The noderef of the node
+	 */
+	public NodeRef getNodeRef() {
+		return nodeRef;
+	}
+
+	/**
+	 * Returns the last good version, i.e. the oldest version the node will
+	 * connect to.
+	 *
+	 * @return The last good version
+	 */
+	public Version getLastGoodVersion() {
+		return nodeRef.getLastGoodVersion();
+	}
+
+	/**
+	 * Returns the signature of the noderef.
+	 *
+	 * @return The signature of the noderef
+	 */
+	public String getSignature() {
+		return nodeRef.getSignature();
+	}
+
+	/**
+	 * Returns whether the noderef is the opennet noderef of the node
+	 *
+	 * @return <code>true</code> if the noderef is the opennet noderef of the
+	 *         node, <code>false</code> otherwise
+	 */
+	public boolean isOpennet() {
+		return nodeRef.isOpennet();
+	}
+
+	/**
+	 * Returns the identity of the node
+	 *
+	 * @return The identity of the node
+	 */
+	public String getIdentity() {
+		return nodeRef.getIdentity();
+	}
+
+	/**
+	 * Returns the name of the node.
+	 *
+	 * @return The name of the node
+	 */
+	public String getMyName() {
+		return nodeRef.getMyName();
+	}
+
+	/**
+	 * Returns the version of the node.
+	 *
+	 * @return The version of the node
+	 */
+	public Version getVersion() {
+		return nodeRef.getVersion();
+	}
+
+	/**
+	 * Returns IP addresses and port number of the node.
+	 *
+	 * @return The IP addresses and port numbers of the node
+	 */
+	public String getPhysicalUDP() {
+		return nodeRef.getPhysicalUDP();
+	}
+
+	/**
+	 * Returns the ARK of the node.
+	 *
+	 * @return The ARK of the node
+	 */
+	public ARK getARK() {
+		return nodeRef.getARK();
+	}
+
+	/**
+	 * Returns the public key of the node.
+	 *
+	 * @return The public key of the node
+	 */
+	public String getDSAPublicKey() {
+		return nodeRef.getDSAPublicKey();
+	}
+
+	/**
+	 * Returns the private key of the node.
+	 *
+	 * @return The private key of the node
+	 */
+	public String getDSKPrivateKey() {
+		return getField("dsaPrivKey.x");
+	}
+
+	/**
+	 * Returns the DSA group of the node.
+	 *
+	 * @return The DSA group of the node
+	 */
+	public DSAGroup getDSAGroup() {
+		return nodeRef.getDSAGroup();
+	}
+
+	/**
+	 * Returns the negotiation types supported by the node.
+	 *
+	 * @return The node’s supported negotiation types
+	 */
+	public int[] getNegotiationTypes() {
+		return nodeRef.getNegotiationTypes();
+	}
+
+	/**
+	 * Returns one of the volatile fields from the message. The given field name
+	 * is prepended with “volatile.” so if you want to get the value of the
+	 * field with the name “volatile.freeJavaMemory” you only need to specify
+	 * “freeJavaMemory”.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @return The value of the field, or <code>null</code> if there is no such
+	 *         field
+	 */
+	public String getVolatile(String field) {
+		return getField("volatile." + field);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/NodeHello.java b/alien/src/net/pterodactylus/fcp/NodeHello.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/NodeHello.java
@@ -0,0 +1,191 @@
+/*
+ * jFCPlib - NodeHello.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Some convenience methods for parsing a “NodeHello” message from the node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class NodeHello extends BaseMessage {
+
+	/**
+	 * Createa a new “NodeHello” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received FCP message
+	 */
+	NodeHello(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the build of the node. This may not be a number but also a string
+	 * like “@custom@” in case you built the node yourself.
+	 *
+	 * @return The build of the node
+	 */
+	public String getBuild() {
+		return getField("Build");
+	}
+
+	/**
+	 * Returns the build number of the node. This may not be a number but also a
+	 * string like “@custom@” in case you built the node yourself.
+	 *
+	 * @return The build number of the node, or <code>-1</code> if the build
+	 *         number could not be determined
+	 */
+	public int getBuildNumber() {
+		return FcpUtils.safeParseInt(getBuild());
+	}
+
+	/**
+	 * Returns the number of compression codecs.
+	 *
+	 * @return The number of compression codecs
+	 */
+	public String getCompressionCodecs() {
+		return getField("CompressionCodecs");
+	}
+
+	/**
+	 * Returns the number of compression codecs.
+	 *
+	 * @return The number of compression codecs, or <code>-1</code> if the
+	 *         number of compression codecs could not be determined
+	 */
+	public int getCompressionCodecsNumber() {
+		return FcpUtils.safeParseInt(getCompressionCodecs());
+	}
+
+	/**
+	 * Returns the unique connection identifier.
+	 *
+	 * @return The connection identifier
+	 */
+	public String getConnectionIdentifier() {
+		return getField("ConnectionIdentifier");
+	}
+
+	/**
+	 * Returns the build of the external library file.
+	 *
+	 * @return The build of the external library file
+	 */
+	public String getExtBuild() {
+		return getField("ExtBuild");
+	}
+
+	/**
+	 * Returns the build number of the external library file.
+	 *
+	 * @return The build number of the external library file, or <code>-1</code>
+	 *         if the build number could not be determined
+	 */
+	public int getExtBuildNumber() {
+		return FcpUtils.safeParseInt(getExtBuild());
+	}
+
+	/**
+	 * Returns the revision of the external library file.
+	 *
+	 * @return The revision of the external library file
+	 */
+	public String getExtRevision() {
+		return getField("ExtRevision");
+	}
+
+	/**
+	 * Returns the revision number of the external library file.
+	 *
+	 * @return The revision number of the external library file, or
+	 *         <code>-1</code> if the revision number could not be determined
+	 */
+	public int getExtRevisionNumber() {
+		return FcpUtils.safeParseInt(getExtRevision());
+	}
+
+	/**
+	 * Returns the FCP version the node speaks.
+	 *
+	 * @return The FCP version the node speaks
+	 */
+	public String getFCPVersion() {
+		return getField("FCPVersion");
+	}
+
+	/**
+	 * Returns the make of the node, e.g. “Fred” (freenet reference
+	 * implementation).
+	 *
+	 * @return The make of the node
+	 */
+	public String getNode() {
+		return getField("Node");
+	}
+
+	/**
+	 * Returns the language of the node as 2-letter code, e.g. “en” or “de”.
+	 *
+	 * @return The language of the node
+	 */
+	public String getNodeLanguage() {
+		return getField("NodeLanguage");
+	}
+
+	/**
+	 * Returns the revision of the node.
+	 *
+	 * @return The revision of the node
+	 */
+	public String getRevision() {
+		return getField("Revision");
+	}
+
+	/**
+	 * Returns the revision number of the node.
+	 *
+	 * @return The revision number of the node, or <code>-1</code> if the
+	 *         revision number coult not be determined
+	 */
+	public int getRevisionNumber() {
+		return FcpUtils.safeParseInt(getRevision());
+	}
+
+	/**
+	 * Returns whether the node is currently is testnet mode.
+	 *
+	 * @return <code>true</code> if the node is currently in testnet mode,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean getTestnet() {
+		return Boolean.valueOf(getField("Testnet"));
+	}
+
+	/**
+	 * Returns the version of the node.
+	 *
+	 * @return The version of the node
+	 */
+	public String getVersion() {
+		return getField("Version");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/NodeRef.java b/alien/src/net/pterodactylus/fcp/NodeRef.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/NodeRef.java
@@ -0,0 +1,353 @@
+/*
+ * jFCPlib - NodeRef.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A reference for a node. The noderef contains all data that is necessary to
+ * establish a trusted and secure connection to the node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class NodeRef {
+
+	/** The identity of the node. */
+	private String identity;
+
+	/** Whether the node is an opennet peer. */
+	private boolean opennet;
+
+	/** The name of the node. */
+	private String name;
+
+	/** The location of the node. */
+	private double location;
+
+	/** The IP addresses and ports of the node. */
+	private String physicalUDP;
+
+	/** The ARK of the node. */
+	private ARK ark;
+
+	/** The public DSA key of the node. */
+	private String dsaPublicKey;
+
+	/** The DSA group of the node. */
+	private DSAGroup dsaGroup;
+
+	/** The node’s supported negotiation types. */
+	private int[] negotiationTypes;
+
+	/** The version of the node. */
+	private Version version;
+
+	/** The oldest version the node will connect to. */
+	private Version lastGoodVersion;
+
+	/** Whether the node is a testnet node. */
+	private boolean testnet;
+
+	/** The signature of the reference. */
+	private String signature;
+
+	/**
+	 * Creates a new, empty noderef.
+	 */
+	public NodeRef() {
+		/* intentionally left blank. */
+	}
+
+	/**
+	 * Creates a new noderef that is initialized with fields from the given
+	 * message.
+	 *
+	 * @param fromMessage
+	 *            The message to get initial values for the noderef from
+	 */
+	public NodeRef(FcpMessage fromMessage) {
+		identity = fromMessage.getField("identity");
+		opennet = Boolean.valueOf(fromMessage.getField("opennet"));
+		name = fromMessage.getField("myName");
+		if (fromMessage.hasField("location")) {
+			location = Double.valueOf(fromMessage.getField("location"));
+		}
+		physicalUDP = fromMessage.getField("physical.udp");
+		ark = new ARK(fromMessage.getField("ark.pubURI"), fromMessage.getField("ark.privURI"), fromMessage.getField("ark.number"));
+		dsaPublicKey = fromMessage.getField("dsaPubKey.y");
+		dsaGroup = new DSAGroup(fromMessage.getField("dsaGroup.b"), fromMessage.getField("dsaGroup.p"), fromMessage.getField("dsaGroup.q"));
+		negotiationTypes = FcpUtils.decodeMultiIntegerField(fromMessage.getField("auth.negTypes"));
+		version = new Version(fromMessage.getField("version"));
+		lastGoodVersion = new Version(fromMessage.getField("lastGoodVersion"));
+		testnet = Boolean.valueOf(fromMessage.getField("testnet"));
+		signature = fromMessage.getField("sig");
+	}
+
+	/**
+	 * Returns the identity of the node.
+	 *
+	 * @return The identity of the node
+	 */
+	public String getIdentity() {
+		return identity;
+	}
+
+	/**
+	 * Sets the identity of the node.
+	 *
+	 * @param identity
+	 *            The identity of the node
+	 */
+	public void setIdentity(String identity) {
+		this.identity = identity;
+	}
+
+	/**
+	 * Returns whether the node is an opennet peer.
+	 *
+	 * @return <code>true</code> if the node is an opennet peer,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isOpennet() {
+		return opennet;
+	}
+
+	/**
+	 * Sets whether the node is an opennet peer.
+	 *
+	 * @param opennet
+	 *            <code>true</code> if the node is an opennet peer,
+	 *            <code>false</code> otherwise
+	 */
+	public void setOpennet(boolean opennet) {
+		this.opennet = opennet;
+	}
+
+	/**
+	 * Returns the name of the node. If the node is an opennet peer, it will not
+	 * have a name!
+	 *
+	 * @return The name of the node, or <code>null</code> if the node is an
+	 *         opennet peer
+	 */
+	public String getMyName() {
+		return name;
+	}
+
+	/**
+	 * Sets the name of the peer.
+	 *
+	 * @param name
+	 *            The name of the peer
+	 */
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * Returns the location of the node.
+	 *
+	 * @return The location of the node
+	 */
+	public double getLocation() {
+		return location;
+	}
+
+	/**
+	 * Sets the location of the node
+	 *
+	 * @param location
+	 *            The location of the node
+	 */
+	public void setLocation(double location) {
+		this.location = location;
+	}
+
+	/**
+	 * Returns the IP addresses and port numbers of the node.
+	 *
+	 * @return The IP addresses and port numbers of the node
+	 */
+	public String getPhysicalUDP() {
+		return physicalUDP;
+	}
+
+	/**
+	 * Sets the IP addresses and port numbers of the node.
+	 *
+	 * @param physicalUDP
+	 *            The IP addresses and port numbers of the node
+	 */
+	public void setPhysicalUDP(String physicalUDP) {
+		this.physicalUDP = physicalUDP;
+	}
+
+	/**
+	 * Returns the ARK of the node.
+	 *
+	 * @return The ARK of the node
+	 */
+	public ARK getARK() {
+		return ark;
+	}
+
+	/**
+	 * Sets the ARK of the node.
+	 *
+	 * @param ark
+	 *            The ARK of the node
+	 */
+	public void setARK(ARK ark) {
+		this.ark = ark;
+	}
+
+	/**
+	 * Returns the public DSA key of the node.
+	 *
+	 * @return The public DSA key of the node
+	 */
+	public String getDSAPublicKey() {
+		return dsaPublicKey;
+	}
+
+	/**
+	 * Sets the public DSA key of the node.
+	 *
+	 * @param dsaPublicKey
+	 *            The public DSA key of the node
+	 */
+	public void setDSAPublicKey(String dsaPublicKey) {
+		this.dsaPublicKey = dsaPublicKey;
+	}
+
+	/**
+	 * Returns the DSA group of the node.
+	 *
+	 * @return The DSA group of the node
+	 */
+	public DSAGroup getDSAGroup() {
+		return dsaGroup;
+	}
+
+	/**
+	 * Sets the DSA group of the node.
+	 *
+	 * @param dsaGroup
+	 *            The DSA group of the node
+	 */
+	public void setDSAGroup(DSAGroup dsaGroup) {
+		this.dsaGroup = dsaGroup;
+	}
+
+	/**
+	 * Returns the negotiation types supported by the node.
+	 *
+	 * @return The node’s supported negotiation types
+	 */
+	public int[] getNegotiationTypes() {
+		return negotiationTypes;
+	}
+
+	/**
+	 * Sets the negotiation types supported by the node.
+	 *
+	 * @param negotiationTypes
+	 *            The node’s supported negotiation types
+	 */
+	public void setNegotiationTypes(int[] negotiationTypes) {
+		this.negotiationTypes = negotiationTypes;
+	}
+
+	/**
+	 * Returns the version of the node.
+	 *
+	 * @return The version of the node
+	 */
+	public Version getVersion() {
+		return version;
+	}
+
+	/**
+	 * Sets the version of the node.
+	 *
+	 * @param version
+	 *            The version of the node
+	 */
+	public void setVersion(Version version) {
+		this.version = version;
+	}
+
+	/**
+	 * Returns the last good version of the node.
+	 *
+	 * @return The oldest version the node will connect to
+	 */
+	public Version getLastGoodVersion() {
+		return lastGoodVersion;
+	}
+
+	/**
+	 * Sets the last good version of the node.
+	 *
+	 * @param lastGoodVersion
+	 *            The oldest version the node will connect to
+	 */
+	public void setLastGoodVersion(Version lastGoodVersion) {
+		this.lastGoodVersion = lastGoodVersion;
+	}
+
+	/**
+	 * Returns whether the node is a testnet node.
+	 *
+	 * @return <code>true</code> if the node is a testnet node,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isTestnet() {
+		return testnet;
+	}
+
+	/**
+	 * Sets whether this node is a testnet node.
+	 *
+	 * @param testnet
+	 *            <code>true</code> if the node is a testnet node,
+	 *            <code>false</code> otherwise
+	 */
+	public void setTestnet(boolean testnet) {
+		this.testnet = testnet;
+	}
+
+	/**
+	 * Returns the signature of the noderef.
+	 *
+	 * @return The signature of the noderef
+	 */
+	public String getSignature() {
+		return signature;
+	}
+
+	/**
+	 * Sets the signature of the noderef.
+	 *
+	 * @param signature
+	 *            The signature of the noderef
+	 */
+	public void setSignature(String signature) {
+		this.signature = signature;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Peer.java b/alien/src/net/pterodactylus/fcp/Peer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Peer.java
@@ -0,0 +1,258 @@
+/*
+ * jFCPlib - Peer.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * The “Peer” reply by the node contains information about a peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class Peer extends BaseMessage {
+
+	/**
+	 * Creates a new “Peer” reply from the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	Peer(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns a collection of fields as a node reference.
+	 *
+	 * @return The node reference contained within this message
+	 */
+	public NodeRef getNodeRef() {
+		NodeRef nodeRef = new NodeRef();
+		nodeRef.setARK(getARK());
+		nodeRef.setDSAGroup(getDSAGroup());
+		nodeRef.setDSAPublicKey(getDSAPublicKey());
+		nodeRef.setIdentity(getIdentity());
+		nodeRef.setLastGoodVersion(getLastGoodVersion());
+		nodeRef.setLocation(getLocation());
+		nodeRef.setName(getMyName());
+		nodeRef.setNegotiationTypes(getNegotiationTypes());
+		nodeRef.setOpennet(isOpennet());
+		nodeRef.setPhysicalUDP(getPhysicalUDP());
+		nodeRef.setVersion(getVersion());
+		return nodeRef;
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the “physical.udp” line from the message. It contains all IP
+	 * addresses and port numbers of the peer.
+	 *
+	 * @return The IP addresses and port numbers of the peer
+	 */
+	public String getPhysicalUDP() {
+		return getField("physical.udp");
+	}
+
+	/**
+	 * Returns whether the listed peer is an opennet peer.
+	 *
+	 * @return <code>true</code> if the peer is an opennet peer,
+	 *         <code>false</code> if the peer is a darknet peer
+	 */
+	public boolean isOpennet() {
+		return Boolean.valueOf(getField("opennet"));
+	}
+
+	/**
+	 * Returns whether this peer is a seed.
+	 *
+	 * @return <code>true</code> if the peer is a seed, <code>false</code>
+	 *         otherwise
+	 */
+	public boolean isSeed() {
+		return Boolean.valueOf(getField("seed"));
+	}
+
+	/**
+	 * Returns the “y” part of the peer’s public DSA key.
+	 *
+	 * @return The public DSA key
+	 */
+	public String getDSAPublicKey() {
+		return getField("dsaPubKey.y");
+	}
+
+	/**
+	 * Returns the DSA group of the peer.
+	 *
+	 * @return The DSA group of the peer
+	 */
+	public DSAGroup getDSAGroup() {
+		return new DSAGroup(getField("dsaGroup.g"), getField("dsaGroup.p"), getField("dsaGroup.q"));
+	}
+
+	/**
+	 * Returns the last good version of the peer, i.e. the oldest version the
+	 * peer will connect to.
+	 *
+	 * @return The last good version of the peer
+	 */
+	public Version getLastGoodVersion() {
+		return new Version(getField("lastGoodVersion"));
+	}
+
+	/**
+	 * Returns the ARK of the peer.
+	 *
+	 * @return The ARK of the peer
+	 */
+	public ARK getARK() {
+		return new ARK(getField("ark.pubURI"), getField("ark.number"));
+	}
+
+	/**
+	 * Returns the identity of the peer.
+	 *
+	 * @return The identity of the peer
+	 */
+	public String getIdentity() {
+		return getField("identity");
+	}
+
+	/**
+	 * Returns the name of the peer. If the peer is not a darknet peer it will
+	 * have no name.
+	 *
+	 * @return The name of the peer, or <code>null</code> if the peer is an
+	 *         opennet peer
+	 */
+	public String getMyName() {
+		return getField("myName");
+	}
+
+	/**
+	 * Returns the location of the peer.
+	 *
+	 * @return The location of the peer
+	 * @throws NumberFormatException
+	 *             if the field can not be parsed
+	 */
+	public double getLocation() throws NumberFormatException {
+		return Double.valueOf(getField("location"));
+	}
+
+	/**
+	 * Returns whether the peer is a testnet node.
+	 *
+	 * @return <code>true</code> if the peer is a testnet node,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isTestnet() {
+		return Boolean.valueOf("testnet");
+	}
+
+	/**
+	 * Returns the version of the peer.
+	 *
+	 * @return The version of the peer
+	 */
+	public Version getVersion() {
+		return new Version(getField("version"));
+	}
+
+	/**
+	 * Returns the negotiation types the peer supports.
+	 *
+	 * @return The supported negotiation types
+	 */
+	public int[] getNegotiationTypes() {
+		return FcpUtils.decodeMultiIntegerField(getField("auth.negTypes"));
+	}
+
+	/**
+	 * Returns all volatile fields from the message.
+	 *
+	 * @return All volatile files
+	 */
+	public Map<String, String> getVolatileFields() {
+		Map<String, String> volatileFields = new HashMap<String, String>();
+		for (Entry<String, String> field : getFields().entrySet()) {
+			if (field.getKey().startsWith("volatile.")) {
+				volatileFields.put(field.getKey(), field.getValue());
+			}
+		}
+		return Collections.unmodifiableMap(volatileFields);
+	}
+
+	/**
+	 * Returns one of the volatile fields from the message. The given field name
+	 * is prepended with “volatile.” so if you want to get the value of the
+	 * field with the name “volatile.status” you only need to specify “status”.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @return The value of the field, or <code>null</code> if there is no such
+	 *         field
+	 */
+	public String getVolatile(String field) {
+		return getField("volatile." + field);
+	}
+
+	/**
+	 * Returns all metadata fields from the message.
+	 *
+	 * @return All volatile files
+	 */
+	public Map<String, String> getMetadataFields() {
+		Map<String, String> metadataFields = new HashMap<String, String>();
+		for (Entry<String, String> field : getFields().entrySet()) {
+			if (field.getKey().startsWith("metadata.")) {
+				metadataFields.put(field.getKey(), field.getValue());
+			}
+		}
+		return Collections.unmodifiableMap(metadataFields);
+	}
+
+	/**
+	 * Returns one of the metadata fields from the message. The given field name
+	 * is prepended with “metadata.” so if you want to get the value of the
+	 * field with the name “metadata.timeLastRoutable” you only need to specify
+	 * “timeLastRoutable”.
+	 *
+	 * @param field
+	 *            The name of the field
+	 * @return The value of the field, or <code>null</code> if there is no such
+	 *         field
+	 */
+	public String getMetadata(String field) {
+		return getField("metadata." + field);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PeerNote.java b/alien/src/net/pterodactylus/fcp/PeerNote.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PeerNote.java
@@ -0,0 +1,70 @@
+/*
+ * jFCPlib - PeerNote.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PeerNote” message contains a private note that has been entered for a
+ * darknet peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PeerNote extends BaseMessage {
+
+	/** The type for base64 encoded peer notes. */
+	public static final int TYPE_PRIVATE_PEER_NOTE = 1;
+
+	/**
+	 * Creates a “PeerNote” message that wraps the recevied message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PeerNote(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the node this note belongs to.
+	 *
+	 * @return The note’s node’s identifier
+	 */
+	public String getNodeIdentifier() {
+		return getField("NodeIdentifier");
+	}
+
+	/**
+	 * Returns the base64-encoded note text.
+	 *
+	 * @return The note text
+	 */
+	public String getNoteText() {
+		return getField("NoteText");
+	}
+
+	/**
+	 * Returns the type of the peer note.
+	 *
+	 * @return The type of the peer note, or <code>-1</code> if the type can not
+	 *         be parsed
+	 */
+	public int getPeerNoteType() {
+		return FcpUtils.safeParseInt(getField("PeerNoteType"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PeerRemoved.java b/alien/src/net/pterodactylus/fcp/PeerRemoved.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PeerRemoved.java
@@ -0,0 +1,56 @@
+/*
+ * jFCPlib - PeerRemoved.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “PeerRemoved” message is sent by the node when a peer has been removed.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PeerRemoved extends BaseMessage {
+
+	/**
+	 * Creates a new “PeerRemoved” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PeerRemoved(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identity of the removed peer.
+	 *
+	 * @return The identity of the removed peer
+	 */
+	public String getIdentity() {
+		return getField("Identity");
+	}
+
+	/**
+	 * Returns the node identifier of the removed peer.
+	 *
+	 * @return The node identifier of the removed peer
+	 */
+	public String getNodeIdentifier() {
+		return getField("NodeIdentifier");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Persistence.java b/alien/src/net/pterodactylus/fcp/Persistence.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Persistence.java
@@ -0,0 +1,46 @@
+/*
+ * jFCPlib - Persistence.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Convenience class for persistence values.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public enum Persistence {
+
+	/**
+	 * Connection persistence. A request with this persistence will die as soon
+	 * as the connection goes down.
+	 */
+	connection,
+
+	/**
+	 * Reboot persistence. A request with this persistence will live until the
+	 * node is restarted.
+	 */
+	reboot,
+
+	/** Forever persistence. A request with this persistence will live forever. */
+	forever,
+
+	/** Unknown persistence. */
+	unknown;
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PersistentGet.java b/alien/src/net/pterodactylus/fcp/PersistentGet.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PersistentGet.java
@@ -0,0 +1,153 @@
+/*
+ * jFCPlib - PersistentGet.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PersistentGet” message is sent to the client to inform it about a
+ * persistent download, either in the client-local queue or in the global queue.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PersistentGet extends BaseMessage {
+
+	/**
+	 * Creates a new “PersistentGet” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PersistentGet(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the URI of the request.
+	 *
+	 * @return The URI of the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns the verbosity of the request.
+	 *
+	 * @return The verbosity of the request
+	 */
+	public Verbosity getVerbosity() {
+		return Verbosity.valueOf(getField("Verbosity"));
+	}
+
+	/**
+	 * Returns the return type of the request.
+	 *
+	 * @return The return type of the request
+	 */
+	public ReturnType getReturnType() {
+		try {
+			return ReturnType.valueOf(getField("ReturnType"));
+		} catch (IllegalArgumentException iae1) {
+			return ReturnType.unknown;
+		}
+	}
+
+	/**
+	 * Returns the name of the file the data is downloaded to. This field will
+	 * only be set if {@link #getReturnType()} is {@link ReturnType#disk}.
+	 *
+	 * @return The name of the file the data is downloaded to
+	 */
+	public String getFilename() {
+		return getField("Filename");
+	}
+
+	/**
+	 * Returns the name of the temporary file. This field will only be set if
+	 * {@link #getReturnType()} is {@link ReturnType#disk}.
+	 *
+	 * @return The name of the temporary file
+	 */
+	public String getTempFilename() {
+		return getField("TempFilename");
+	}
+
+	/**
+	 * Returns the client token of the request.
+	 *
+	 * @return The client token of the request
+	 */
+	public String getClientToken() {
+		return getField("ClientToken");
+	}
+
+	/**
+	 * Returns the priority of the request.
+	 *
+	 * @return The priority of the request
+	 */
+	public Priority getPriority() {
+		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
+	}
+
+	/**
+	 * Returns the persistence of the request.
+	 *
+	 * @return The persistence of the request, or {@link Persistence#unknown} if
+	 *         the persistence could not be parsed
+	 */
+	public Persistence getPersistence() {
+		try {
+			return Persistence.valueOf(getField("Persistence"));
+		} catch (IllegalArgumentException iae1) {
+			return Persistence.unknown;
+		}
+	}
+
+	/**
+	 * Returns whether this request is on the global queue or on the
+	 * client-local queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if the request is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the maximum number of retries for a failed block.
+	 *
+	 * @return The maximum number of retries for a failed block, <code>-1</code>
+	 *         for endless retries, <code>-2</code> if the number could not be
+	 *         parsed
+	 */
+	public int getMaxRetries() {
+		return FcpUtils.safeParseInt(getField("MaxRetries"), -2);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PersistentPut.java b/alien/src/net/pterodactylus/fcp/PersistentPut.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PersistentPut.java
@@ -0,0 +1,163 @@
+/*
+ * jFCPlib - PersistentPut.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “PersistentPut” message notifies a client about a persistent
+ * {@link ClientPut} request.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PersistentPut extends BaseMessage {
+
+	/**
+	 * Creates a new “PersistentPut” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PersistentPut(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the client token of the request.
+	 *
+	 * @return The client token of the request
+	 */
+	public String getClientToken() {
+		return getField("ClientToken");
+	}
+
+	/**
+	 * Returns the data length of the request.
+	 *
+	 * @return The data length of the request, or <code>-1</code> if the length
+	 *         could not be parsed
+	 */
+	public long getDataLength() {
+		return FcpUtils.safeParseLong(getField("DataLength"));
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the maximum number of retries for failed blocks. When
+	 * <code>-1</code> is returned each block is tried forever.
+	 *
+	 * @return The maximum number of retries for failed blocks, or
+	 *         <code>-1</code> for unlimited retries, or <code>-2</code> if the
+	 *         number of retries could not be parsed
+	 */
+	public int getMaxRetries() {
+		return FcpUtils.safeParseInt(getField("MaxRetries"));
+	}
+
+	/**
+	 * Returns the content type of the data.
+	 *
+	 * @return The content type
+	 */
+	public String getMetadataContentType() {
+		return getField("Metadata.ContentType");
+	}
+
+	/**
+	 * Returns the persistence of the request.
+	 *
+	 * @return The persistence of the request
+	 */
+	public Persistence getPersistence() {
+		return Persistence.valueOf(getField("Persistence"));
+	}
+
+	/**
+	 * Returns the priority of the request.
+	 *
+	 * @return The priority of the request, or {@link Priority#unknown} if the
+	 *         priority could not be parsed
+	 */
+	public Priority getPriority() {
+		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
+	}
+
+	/**
+	 * Returns whether this request has started.
+	 *
+	 * @return <code>true</code> if the request has started, <code>false</code>
+	 *         otherwise
+	 */
+	public boolean isStarted() {
+		return Boolean.valueOf(getField("Started"));
+	}
+
+	/**
+	 * Returns the target filename of the request.
+	 *
+	 * @return The target filename of the request
+	 */
+	public String getTargetFilename() {
+		return getField("TargetFilename");
+	}
+
+	/**
+	 * Returns the upload source of the request.
+	 *
+	 * @return The upload source of the request
+	 */
+	public UploadFrom getUploadFrom() {
+		return UploadFrom.valueOf(getField("UploadFrom"));
+	}
+
+	/**
+	 * Returns the target URI of the request.
+	 *
+	 * @return The target URI of the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns the verbosity of the request.
+	 *
+	 * @return The verbosity of the request
+	 */
+	public Verbosity getVerbosity() {
+		return Verbosity.valueOf(getField("Verbosity"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PersistentPutDir.java b/alien/src/net/pterodactylus/fcp/PersistentPutDir.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PersistentPutDir.java
@@ -0,0 +1,170 @@
+/*
+ * jFCPlib - PersistentPutDir.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “PersistentPutDir” is the response to a {@link ClientPutDiskDir} message.
+ * It is also sent as a possible response to a {@link ListPersistentRequests}
+ * message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PersistentPutDir extends BaseMessage {
+
+	/**
+	 * Creates a new “PersistentPutDir” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PersistentPutDir(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the URI of the request.
+	 *
+	 * @return The URI of the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns the verbosity of the request.
+	 *
+	 * @return The verbosity of the request
+	 */
+	public Verbosity getVerbosity() {
+		return Verbosity.valueOf(getField("Verbosity"));
+	}
+
+	/**
+	 * Returns the priority of the request.
+	 *
+	 * @return The priority of the request
+	 */
+	public Priority getPriority() {
+		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the maximum number of retries for failed blocks.
+	 *
+	 * @return The maximum number of retries, or <code>-1</code> for endless
+	 *         retries, or <code>-2</code> if the number could not be parsed
+	 */
+	public int getMaxRetries() {
+		return FcpUtils.safeParseInt(getField("MaxRetries"), -2);
+	}
+
+	/**
+	 * Returns the number of files in the request.
+	 *
+	 * @return The number of files in the request
+	 */
+	public int getFileCount() {
+		int fileCount = -1;
+		while (getField("Files." + ++fileCount + ".UploadFrom") != null) {
+			/* do nothing. */
+		}
+		return fileCount;
+	}
+
+	/**
+	 * Returns the name of the file at the given index. The index is counted
+	 * from <code>0</code>.
+	 *
+	 * @param fileIndex
+	 *            The index of the file
+	 * @return The name of the file at the given index
+	 */
+	public String getFileName(int fileIndex) {
+		return getField("Files." + fileIndex + ".Name");
+	}
+
+	/**
+	 * Returns the length of the file at the given index. The index is counted
+	 * from <code>0</code>.
+	 *
+	 * @param fileIndex
+	 *            The index of the file
+	 * @return The length of the file at the given index
+	 */
+	public long getFileDataLength(int fileIndex) {
+		return FcpUtils.safeParseLong(getField("Files." + fileIndex + ".DataLength"));
+	}
+
+	/**
+	 * Returns the upload source of the file at the given index. The index is
+	 * counted from <code>0</code>.
+	 *
+	 * @param fileIndex
+	 *            The index of the file
+	 * @return The upload source of the file at the given index
+	 */
+	public UploadFrom getFileUploadFrom(int fileIndex) {
+		return UploadFrom.valueOf(getField("Files." + fileIndex + ".UploadFrom"));
+	}
+
+	/**
+	 * Returns the content type of the file at the given index. The index is
+	 * counted from <code>0</code>.
+	 *
+	 * @param fileIndex
+	 *            The index of the file
+	 * @return The content type of the file at the given index
+	 */
+	public String getFileMetadataContentType(int fileIndex) {
+		return getField("Files." + fileIndex + ".Metadata.ContentType");
+	}
+
+	/**
+	 * Returns the filename of the file at the given index. This value is only
+	 * returned if {@link #getFileUploadFrom(int)} is returning
+	 * {@link UploadFrom#disk}. The index is counted from <code>0</code>.
+	 *
+	 * @param fileIndex
+	 *            The index of the file
+	 * @return The filename of the file at the given index
+	 */
+	public String getFileFilename(int fileIndex) {
+		return getField("Files." + fileIndex + ".Filename");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PersistentRequestModified.java b/alien/src/net/pterodactylus/fcp/PersistentRequestModified.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PersistentRequestModified.java
@@ -0,0 +1,79 @@
+/*
+ * jFCPlib - PersistentRequestModified.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PersistentRequestModified” message is a reply to
+ * {@link ModifyPersistentRequest}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PersistentRequestModified extends BaseMessage {
+
+	/**
+	 * Creates a new “PersistentRequestModified” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PersistentRequestModified(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the changed request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on a client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the client token, if it was changed.
+	 *
+	 * @return The new client token, or <code>null</code> if the client token
+	 *         was not changed
+	 */
+	public String getClientToken() {
+		return getField("ClientToken");
+	}
+
+	/**
+	 * Returns the priority of the request, if it was changed.
+	 *
+	 * @return The new priority of the request, or {@link Priority#unknown} if
+	 *         the priority was not changed
+	 */
+	public Priority getPriority() {
+		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PersistentRequestRemoved.java b/alien/src/net/pterodactylus/fcp/PersistentRequestRemoved.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PersistentRequestRemoved.java
@@ -0,0 +1,60 @@
+/*
+ * jFCPlib - PersistentRequestRemoved.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “PersistentRequestRemoved” message signals that a persistent request was
+ * removed from either the global or the client-local queue.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PersistentRequestRemoved extends BaseMessage {
+
+	/**
+	 * Creates a new “PersistentRequestRemoved” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PersistentRequestRemoved(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request was removed from the global queue.
+	 *
+	 * @return <code>true</code> if the request was removed from the global
+	 *         queue, <code>false</code> if it was removed from the client-local
+	 *         queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PluginInfo.java b/alien/src/net/pterodactylus/fcp/PluginInfo.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PluginInfo.java
@@ -0,0 +1,75 @@
+/*
+ * jFCPlib - PluginInfo.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PluginInfo” message is a reply to the {@link GetPluginInfo} request.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PluginInfo extends BaseMessage {
+
+	/**
+	 * Creates a new “PluginInfo” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PluginInfo(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the name of the plugin.
+	 *
+	 * @return The name of the plugin
+	 */
+	public String getPluginName() {
+		return getField("PluginName");
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the original URI of the plugin.
+	 *
+	 * @return The original URI of the plugin
+	 */
+	public String getOriginalURI() {
+		return getField("OriginalUri");
+	}
+
+	/**
+	 * Returns whether the plugin is started.
+	 *
+	 * @return <code>true</code> if the plugin is started, <code>false</code>
+	 *         otherwise
+	 */
+	public boolean isStarted() {
+		return Boolean.valueOf("Started");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Priority.java b/alien/src/net/pterodactylus/fcp/Priority.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Priority.java
@@ -0,0 +1,60 @@
+/*
+ * jFCPlib - Priority.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The priority classes used by the Freenet node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public enum Priority {
+
+	/** Maximum priority. */
+	maximum,
+
+	/** Priority for interactive request, i.e. FProxy. */
+	interactive,
+
+	/** Priority for splitfile manifests. */
+	immediateSplitfile,
+
+	/** Priority for USK searches. */
+	update,
+
+	/** Priority for splitfile blocks. */
+	bulkSplitfile,
+
+	/** Priority for prefetching blocks. */
+	prefetch,
+
+	/** Minimum priority. */
+	minimum,
+
+	/** Unknown priority. */
+	unknown;
+
+	/**
+	 * @see java.lang.Enum#toString()
+	 */
+	@Override
+	public String toString() {
+		return String.valueOf(ordinal());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ProtocolError.java b/alien/src/net/pterodactylus/fcp/ProtocolError.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ProtocolError.java
@@ -0,0 +1,96 @@
+/*
+ * jFCPlib - ProtocolError.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “ProtocolError” message signals that something has gone really wrong.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ProtocolError extends BaseMessage {
+
+	/**
+	 * Creates a new “ProtocolError” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	ProtocolError(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns whether the causing message had the “Global” flag set.
+	 *
+	 * @return <code>true</code> if the causing message had the “Global” flag
+	 *         set
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the error code.
+	 *
+	 * @return The error code, or <code>-1</code> if the error code could not be
+	 *         parsed
+	 */
+	public int getCode() {
+		return FcpUtils.safeParseInt(getField("Code"));
+	}
+
+	/**
+	 * Returns the description of the error.
+	 *
+	 * @return The description of the error
+	 */
+	public String getCodeDescription() {
+		return getField("CodeDescription");
+	}
+
+	/**
+	 * Returns some extra description of the error.
+	 *
+	 * @return Extra description of the error, or <code>null</code> if there is
+	 *         none
+	 */
+	public String getExtraDescription() {
+		return getField("ExtraDescription");
+	}
+
+	/**
+	 * Returns whether the connection to the node can stay open.
+	 *
+	 * @return <code>true</code> when the connection has to be closed,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isFatal() {
+		return Boolean.valueOf(getField("Fatal"));
+	}
+
+	/**
+	 * The identifier of the causing request, if any.
+	 *
+	 * @return The identifier of the causing request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PutFailed.java b/alien/src/net/pterodactylus/fcp/PutFailed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PutFailed.java
@@ -0,0 +1,176 @@
+/*
+ * jFCPlib - GetFailed.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * The “PutFailed” message signals the client that a {@link ClientPut} request
+ * has failed. This also means that no further progress messages for that
+ * request will be sent.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PutFailed extends BaseMessage {
+
+	/**
+	 * Creates a new “PutFailed” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PutFailed(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the code of the error.
+	 *
+	 * @return The code of the error, or <code>-1</code> if the error code could
+	 *         not be parsed
+	 */
+	public int getCode() {
+		return FcpUtils.safeParseInt(getField("Code"));
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the description of the error code.
+	 *
+	 * @return The description of the error code
+	 */
+	public String getCodeDescription() {
+		return getField("CodeDescription");
+	}
+
+	/**
+	 * Returns the extra description of the error.
+	 *
+	 * @return The extra description of the error
+	 */
+	public String getExtraDescription() {
+		return getField("ExtraDescription");
+	}
+
+	/**
+	 * Returns the short description of the error.
+	 *
+	 * @return The short description of the error
+	 */
+	public String getShortCodeDescription() {
+		return getField("ShortCodeDescription");
+	}
+
+	/**
+	 * Returns the expected URI of the request.
+	 *
+	 * @return The expected URI
+	 */
+	public String getExpectedURI() {
+		return getField("ExpectedURI");
+	}
+
+	/**
+	 * Returns whether the request failed fatally. If a request fails fatally it
+	 * can never complete, even with inifinite retries.
+	 *
+	 * @return <code>true</code> if the request failed fatally,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isFatal() {
+		return Boolean.valueOf(getField("Fatal"));
+	}
+
+	/**
+	 * Returns a list of complex error codes with the message. Use
+	 * {@link #getComplexErrorDescription(int)} and
+	 * {@link #getComplexErrorCount(int)} to get details.
+	 *
+	 * @return A list of complex error codes
+	 */
+	public int[] getComplexErrorCodes() {
+		Map<String, String> allFields = getFields();
+		List<Integer> errorCodeList = new ArrayList<Integer>();
+		for (Entry<String, String> field : allFields.entrySet()) {
+			String fieldKey = field.getKey();
+			if (fieldKey.startsWith("Errors.")) {
+				int nextDot = fieldKey.indexOf('.', 7);
+				if (nextDot > -1) {
+					int errorCode = FcpUtils.safeParseInt(fieldKey.substring(7, nextDot));
+					if (errorCode != -1) {
+						errorCodeList.add(errorCode);
+					}
+				}
+			}
+		}
+		int[] errorCodes = new int[errorCodeList.size()];
+		int errorIndex = 0;
+		for (int errorCode : errorCodeList) {
+			errorCodes[errorIndex++] = errorCode;
+		}
+		return errorCodes;
+	}
+
+	/**
+	 * Returns the description of the complex error. You should only hand it
+	 * error codes you got from {@link #getComplexErrorCodes()}!
+	 *
+	 * @param errorCode
+	 *            The error code
+	 * @return The description of the complex error
+	 */
+	public String getComplexErrorDescription(int errorCode) {
+		return getField("Errors." + errorCode + ".Description");
+	}
+
+	/**
+	 * Returns the count of the complex error. You should only hand it error
+	 * codes you got from {@link #getComplexErrorCodes()}!
+	 *
+	 * @param errorCode
+	 *            The error code
+	 * @return The count of the complex error, or <code>-1</code> if the count
+	 *         could not be parsed
+	 */
+	public int getComplexErrorCount(int errorCode) {
+		return FcpUtils.safeParseInt(getField("Errors." + errorCode + ".Count"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PutFetchable.java b/alien/src/net/pterodactylus/fcp/PutFetchable.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PutFetchable.java
@@ -0,0 +1,68 @@
+/*
+ * jFCPlib - PutFetchable.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PutFetchable” message informs a client that a {@link ClientPut} request
+ * has progressed far enough that the resulting final URI might already be
+ * fetchable.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PutFetchable extends BaseMessage {
+
+	/**
+	 * Creates a new “PutFetchable” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PutFetchable(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the URI of the request.
+	 *
+	 * @return The URI of the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/PutSuccessful.java b/alien/src/net/pterodactylus/fcp/PutSuccessful.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/PutSuccessful.java
@@ -0,0 +1,87 @@
+/*
+ * jFCPlib - PutSuccessful.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “PutSuccessful” message informs a client about a successfully finished
+ * {@link ClientPut} (or similar) request.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PutSuccessful extends BaseMessage {
+
+	/**
+	 * Creates a new “PutSuccessful” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	PutSuccessful(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns whether the request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.valueOf(getField("Global"));
+	}
+
+	/**
+	 * Returns the final URI of the {@link ClientPut} request.
+	 *
+	 * @return The final URI of the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns the time the insert started.
+	 *
+	 * @return The time the insert started (in milliseconds since Jan 1, 1970
+	 *         UTC), or <code>-1</code> if the time could not be parsed
+	 */
+	public long getStartupTime() {
+		return FcpUtils.safeParseLong(getField("StartupTime"));
+	}
+
+	/**
+	 * Returns the time the insert completed.
+	 *
+	 * @return The time the insert completed (in milliseconds since Jan 1, 1970
+	 *         UTC), or <code>-1</code> if the time could not be parsed
+	 */
+	public long getCompletionTime() {
+		return FcpUtils.safeParseLong(getField("CompletionTime"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ReceivedBookmarkFeed.java b/alien/src/net/pterodactylus/fcp/ReceivedBookmarkFeed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ReceivedBookmarkFeed.java
@@ -0,0 +1,78 @@
+/*
+ * jFCPlib - ReceivedBookmarkFeed.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Implementation of the “ReceivedBookmarkFeed” FCP message. This message
+ * notifies an FCP client that an update for a bookmark has been found.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class ReceivedBookmarkFeed extends BaseMessage {
+
+	/**
+	 * Creates a new “ReceivedBookmarkFeed” message.
+	 *
+	 * @param fcpMessage
+	 *            The FCP message to get the fields from
+	 */
+	public ReceivedBookmarkFeed(FcpMessage fcpMessage) {
+		super(fcpMessage);
+	}
+
+	/**
+	 * Returns the name of the bookmark.
+	 *
+	 * @return The bookmark’s name
+	 */
+	public String getBookmarkName() {
+		return getField("Name");
+	}
+
+	/**
+	 * Returns the URI of the updated bookmark.
+	 *
+	 * @return The bookmark’s URI
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns whether the bookmark has an active link image.
+	 *
+	 * @return {@code true} if the bookmark has an active link image, {@code
+	 *         false} otherwise
+	 */
+	public boolean hasActiveLink() {
+		return Boolean.parseBoolean(getField("HasAnActiveLink"));
+	}
+
+	/**
+	 * Returns the description of the bookmark. Note that the description may be
+	 * {@code null} and if it is not, it is base64-encoded!
+	 *
+	 * @return The bookmark’s description, or {@code null} if the bookmark has
+	 *         no description
+	 */
+	public String getDescription() {
+		return getField("Description");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/RemovePeer.java b/alien/src/net/pterodactylus/fcp/RemovePeer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/RemovePeer.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - RemovePeer.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “RemovePeer” command removes a peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class RemovePeer extends FcpMessage {
+
+	/**
+	 * Creates a new “RemovePeer” command that removes the given peer.
+	 *
+	 * @param nodeIdentifier
+	 *            The identifier of the node, i.e. its name, identity, or IP
+	 *            address and port pair
+	 */
+	public RemovePeer(String nodeIdentifier) {
+		super("RemovePeer");
+		setField("NodeIdentifier", nodeIdentifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/RemovePersistentRequest.java b/alien/src/net/pterodactylus/fcp/RemovePersistentRequest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/RemovePersistentRequest.java
@@ -0,0 +1,52 @@
+/*
+ * jFCPlib - RemovePersistentRequest.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “RemovePersistentRequest” message tells the node to remove a persistent
+ * request, cancelling it first (resulting in a {@link GetFailed} or
+ * {@link PutFailed} message), if necessary.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class RemovePersistentRequest extends FcpMessage {
+
+	/**
+	 * Creates a new “RemovePersistentRequest” message.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public RemovePersistentRequest(String identifier) {
+		super("RemovePersistentRequest");
+		setField("Identifier", identifier);
+	}
+
+	/**
+	 * Sets whether the request is on the global queue.
+	 *
+	 * @param global
+	 *            <code>true</code> if the request is on the global queue,
+	 *            <code>false</code> if it is on the client-local queue
+	 */
+	public void setGlobal(boolean global) {
+		setField("Global", String.valueOf(global));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/ReturnType.java b/alien/src/net/pterodactylus/fcp/ReturnType.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/ReturnType.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - ReturnType.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The different return types for {@link ClientGet} requests.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public enum ReturnType {
+
+	/** Don't transfer the data at all. */
+	none,
+
+	/** Transfer the data directly after the message. */
+	direct,
+
+	/** Copy the data to disk. */
+	disk,
+
+	/** Unknown return type. */
+	unknown;
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SSKKeypair.java b/alien/src/net/pterodactylus/fcp/SSKKeypair.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SSKKeypair.java
@@ -0,0 +1,66 @@
+/*
+ * jFCPlib - SSKKeypair.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * An “SSKKeypair” message that is sent as a response to a {@link GenerateSSK}
+ * message.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SSKKeypair extends BaseMessage {
+
+	/**
+	 * Creates a new “SSKKeypair” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	SSKKeypair(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the URI that must be used to insert data.
+	 *
+	 * @return The insert URI
+	 */
+	public String getInsertURI() {
+		return getField("InsertURI");
+	}
+
+	/**
+	 * Returns the URI that must be used to request data.
+	 *
+	 * @return The request URI
+	 */
+	public String getRequestURI() {
+		return getField("RequestURI");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SendBookmarkFeed.java b/alien/src/net/pterodactylus/fcp/SendBookmarkFeed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SendBookmarkFeed.java
@@ -0,0 +1,53 @@
+/*
+ * jFCPlib - SendBookmarkFeed.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “SendBookmarkFeed” command sends a bookmark to a peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SendBookmarkFeed extends AbstractSendFeedMessage {
+
+	/**
+	 * Creates a new “SendBookmarkFeed” command.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param nodeIdentifier
+	 *            The identifier of the peer node
+	 * @param name
+	 *            The name of the bookmark
+	 * @param uri
+	 *            The URI of the bookmark
+	 * @param description
+	 *            The description of the bookmark (may be {@code null})
+	 * @param hasActiveLink
+	 *            {@code true} if the bookmark has an activelink image, {@code
+	 *            false} otherwise
+	 */
+	public SendBookmarkFeed(String identifier, String nodeIdentifier, String name, String uri, String description, boolean hasActiveLink) {
+		super("SendBookmarkFeed", identifier, nodeIdentifier);
+		setField("Name", name);
+		setField("URI", uri);
+		setField("Description", description);
+		setField("HasActiveLink", String.valueOf(hasActiveLink));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SendDownloadFeed.java b/alien/src/net/pterodactylus/fcp/SendDownloadFeed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SendDownloadFeed.java
@@ -0,0 +1,47 @@
+/*
+ * jFCPlib - SendDownloadFeed.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “SendDownloadFeed” command sends information about a download to a peer
+ * node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SendDownloadFeed extends AbstractSendFeedMessage {
+
+	/**
+	 * Creates a new “SendDownloadFeed” to a peer node.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param nodeIdentifier
+	 *            The identifier of the peer node
+	 * @param uri
+	 *            The URI of the download to send
+	 * @param description
+	 *            The description of the download (may be {@code null})
+	 */
+	public SendDownloadFeed(String identifier, String nodeIdentifier, String uri, String description) {
+		super("SendDownloadFeed", identifier, nodeIdentifier);
+		setField("URI", uri);
+		setField("Description", description);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SendTextFeed.java b/alien/src/net/pterodactylus/fcp/SendTextFeed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SendTextFeed.java
@@ -0,0 +1,43 @@
+/*
+ * jFCPlib - SendTextFeed.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “SendTextFeed” command sends an arbitrary text to a peer node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SendTextFeed extends AbstractSendFeedMessage {
+
+	/**
+	 * Creates a new “SendTextFeed” command.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param nodeIdentifier
+	 *            The identifier of the peer node
+	 * @param text
+	 *            The text to send
+	 */
+	public SendTextFeed(String identifier, String nodeIdentifier, String text) {
+		super("SendTextFeed", identifier, nodeIdentifier);
+		setField("Text", text);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SentFeed.java b/alien/src/net/pterodactylus/fcp/SentFeed.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SentFeed.java
@@ -0,0 +1,76 @@
+/*
+ * jFCPlib - SentFeed.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “SentFeed” message signals that a feed was successfully sent to a peer.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SentFeed extends BaseMessage {
+
+	/**
+	 * Creates a new “SentFeed” message from the given FCP message.
+	 *
+	 * @param fcpMessage
+	 *            The FCP message containing the “SentFeed” message
+	 */
+	public SentFeed(FcpMessage fcpMessage) {
+		super(fcpMessage);
+	}
+
+	/**
+	 * Returns the identifier of the sent feed. The identifier of this message
+	 * matches the identifier that was given when a {@link SendBookmarkFeed},
+	 * {@link SendDownloadFeed}, or {@link SendTextFeed} command was created.
+	 *
+	 * @return The send feed’s identifier
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the node status of the peer. The node status is definied in
+	 * {@code freenet.node.PeerManager}.
+	 * <p>
+	 * <ol start="1">
+	 * <li>Connected</li>
+	 * <li>Backed off</li>
+	 * <li>Version too new</li>
+	 * <li>Version too old</li>
+	 * <li>Disconnected</li>
+	 * <li>Never connected</li>
+	 * <li>Disabled</li>
+	 * <li>Bursting</li>
+	 * <li>Listening</li>
+	 * <li>Listening only</li>
+	 * <li>Clock problem</li>
+	 * <li>Connection error</li>
+	 * <li>Disconnecting</li>
+	 * <li>Routing disabled</li>
+	 * </ol>
+	 *
+	 * @return The node’s status
+	 */
+	public int getNodeStatus() {
+		return FcpUtils.safeParseInt(getField("NodeStatus"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Shutdown.java b/alien/src/net/pterodactylus/fcp/Shutdown.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Shutdown.java
@@ -0,0 +1,35 @@
+/*
+ * jFCPlib - Shutdown.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Command that shuts down the node.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class Shutdown extends FcpMessage {
+
+	/**
+	 * Creates a new “Shutdown” message.
+	 */
+	public Shutdown() {
+		super("Shutdown");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SimpleProgress.java b/alien/src/net/pterodactylus/fcp/SimpleProgress.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SimpleProgress.java
@@ -0,0 +1,111 @@
+/*
+ * jFCPlib - SimpleProgress.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “SimpleProgress” message tells the client about the progress of a
+ * {@link ClientGet} or {@link ClientPut} operation.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SimpleProgress extends BaseMessage {
+
+	/**
+	 * Creates a new “SimpleProgress” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	SimpleProgress(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the total number of blocks. This number may increase as long as
+	 * {@link #isFinalizedTotal()} returns <code>false</code>.
+	 *
+	 * @return The total number of blocks
+	 */
+	public int getTotal() {
+		return FcpUtils.safeParseInt(getField("Total"));
+	}
+
+	/**
+	 * Returns the number of blocks that are required to completet the request.
+	 * This number might actually be lower than {@link #getTotal} because of
+	 * redundancy information. This number may also increase as long as
+	 * {@link #isFinalizedTotal()} returns <code>false</code>.
+	 *
+	 * @return The number of required blocks
+	 */
+	public int getRequired() {
+		return FcpUtils.safeParseInt(getField("Required"));
+	}
+
+	/**
+	 * Returns the number of blocks that have failed and run out of retries.
+	 *
+	 * @return The number of failed blocks
+	 */
+	public int getFailed() {
+		return FcpUtils.safeParseInt(getField("Failed"));
+	}
+
+	/**
+	 * Returns the number of fatally failed blocks. A block that failed fatally
+	 * can never be completed, even with infinite retries.
+	 *
+	 * @return The number of fatally failed blocks
+	 */
+	public int getFatallyFailed() {
+		return FcpUtils.safeParseInt(getField("FatallyFailed"));
+	}
+
+	/**
+	 * Returns the number of blocks that have been successfully processed.
+	 *
+	 * @return The number of succeeded blocks
+	 */
+	public int getSucceeded() {
+		return FcpUtils.safeParseInt(getField("Succeeded"));
+	}
+
+	/**
+	 * Returns whether the total number of blocks (see {@link #getTotal()} has
+	 * been finalized. Once the total number of blocks has been finalized for a
+	 * request it will not change any more, and this method of every further
+	 * SimpleProgress message will always return <code>true</code>.
+	 *
+	 * @return <code>true</code> if the number of total blocks has been
+	 *         finalized, <code>false</code> otherwise
+	 */
+	public boolean isFinalizedTotal() {
+		return Boolean.valueOf(getField("FinalizedTotal"));
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/StartedCompression.java b/alien/src/net/pterodactylus/fcp/StartedCompression.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/StartedCompression.java
@@ -0,0 +1,59 @@
+/*
+ * jFCPlib - StartedCompression.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “StartedCompression” message signals the client the compressing for a
+ * request has started.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class StartedCompression extends BaseMessage {
+
+	/**
+	 * Creates a new “StartedCompression” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	StartedCompression(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the number of the codec that is used for compression.
+	 *
+	 * @return The codec used for the compression, or <code>-1</code> if the
+	 *         codec could not be parsed
+	 */
+	public int getCodec() {
+		return FcpUtils.safeParseInt(getField("Codec"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SubscribeFeeds.java b/alien/src/net/pterodactylus/fcp/SubscribeFeeds.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SubscribeFeeds.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - SubscribeFeeds.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “SubscribeFeeds” command tells the node that the client is interested in
+ * receiving the feeds sent by peer nodes.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SubscribeFeeds extends FcpMessage {
+
+	/**
+	 * Creates a new “SubscribeFeeds” command.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public SubscribeFeeds(String identifier) {
+		super("SubscribeFeeds");
+		setField("Identifier", identifier);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SubscribeUSK.java b/alien/src/net/pterodactylus/fcp/SubscribeUSK.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SubscribeUSK.java
@@ -0,0 +1,55 @@
+/*
+ * jFCPlib - SubscribeUSK.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * With a “SubscribeUSK” a client requests to be notified if the edition number
+ * of a USK changes.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SubscribeUSK extends FcpMessage {
+
+	/**
+	 * Creates a new “SubscribeUSK” message.
+	 *
+	 * @param uri
+	 *            The URI to watch for changes
+	 * @param identifier
+	 *            The identifier of the request
+	 */
+	public SubscribeUSK(String uri, String identifier) {
+		super("SubscribeUSK");
+		setField("URI", uri);
+		setField("Identifier", identifier);
+	}
+
+	/**
+	 * Sets whether updates for the USK are actively searched.
+	 *
+	 * @param active
+	 *            <code>true</code> to actively search for newer editions,
+	 *            <code>false</code> to only watch for newer editions that are
+	 *            found from other requests
+	 */
+	public void setActive(boolean active) {
+		setField("DontPoll", String.valueOf(!active));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/SubscribedUSKUpdate.java b/alien/src/net/pterodactylus/fcp/SubscribedUSKUpdate.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/SubscribedUSKUpdate.java
@@ -0,0 +1,70 @@
+/*
+ * jFCPlib - SubscribedUSKUpdate.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “SubscribedUSKUpdate” message is sent each time a new edition of a USK that
+ * was previously subscribed to with {@link SubscribeUSK} was found. Note that
+ * if the new edition that was found is several editions ahead of the currently
+ * last known edition, you will received a SubscribedUSKUpdate for each edition
+ * inbetween as welL!
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class SubscribedUSKUpdate extends BaseMessage {
+
+	/**
+	 * Creates a new “SubscribedUSKUpdate” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	SubscribedUSKUpdate(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the subscription.
+	 *
+	 * @return The identifier of the subscription
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the new edition that was found.
+	 *
+	 * @return The new edition
+	 */
+	public int getEdition() {
+		return FcpUtils.safeParseInt(getField("Edition"));
+	}
+
+	/**
+	 * Returns the complete URI, including the new edition.
+	 *
+	 * @return The complete URI
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/TestDDAComplete.java b/alien/src/net/pterodactylus/fcp/TestDDAComplete.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/TestDDAComplete.java
@@ -0,0 +1,68 @@
+/*
+ * jFCPlib - TestDDAComplete.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “TestDDAComplete” message signals that the node has finished checking
+ * your read and write access to a certain directory.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class TestDDAComplete extends BaseMessage {
+
+	/**
+	 * Creates a new “TestDDAComplete” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	TestDDAComplete(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the directory the authorization is given for.
+	 *
+	 * @return The directory that was tested for read and/or write access
+	 */
+	public String getDirectory() {
+		return getField("Directory");
+	}
+
+	/**
+	 * Returns whether read access to the directory is allowed.
+	 *
+	 * @return <code>true</code> if the client is allowed to read from that
+	 *         directory, <code>false</code> otherwise
+	 */
+	public boolean isReadDirectoryAllowed() {
+		return Boolean.valueOf(getField("ReadDirectoryAllowed"));
+	}
+
+	/**
+	 * Returns whether write access to the directory is allowed.
+	 *
+	 * @return <code>true</code> if the client is allowed to write into that
+	 *         directory, <code>false</code> otherwise
+	 */
+	public boolean isWriteDirectoryAllowed() {
+		return Boolean.valueOf(getField("WriteDirectoryAllowed"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/TestDDAReply.java b/alien/src/net/pterodactylus/fcp/TestDDAReply.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/TestDDAReply.java
@@ -0,0 +1,82 @@
+/*
+ * jFCPlib - TestDDAReply.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “TestDDAReply” is sent as a response to {@link TestDDARequest}. If you
+ * specified that you wanted to read files from that directory
+ * {@link #getReadFilename()} will give you a filename. Similarly, if you
+ * specified that you want to write in the directory {@link #getWriteFilename()}
+ * will give you a filename to write {@link #getContentToWrite()} to.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class TestDDAReply extends BaseMessage {
+
+	/**
+	 * Creates a “TestDDAReply” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	TestDDAReply(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the directory the TestDDRequest was made for.
+	 *
+	 * @return The directory to test
+	 */
+	public String getDirectory() {
+		return getField("Directory");
+	}
+
+	/**
+	 * Returns the filename you have to read to proof your ability to read that
+	 * specific directory.
+	 *
+	 * @return The name of the file to read
+	 */
+	public String getReadFilename() {
+		return getField("ReadFilename");
+	}
+
+	/**
+	 * Returns the filename you have to write to to proof your ability to write
+	 * to that specific directory.
+	 *
+	 * @return The name of the file write to
+	 */
+	public String getWriteFilename() {
+		return getField("WriteFilename");
+	}
+
+	/**
+	 * If you requested a test for writing permissions you have to write the
+	 * return value of this method to the file given by
+	 * {@link #getWriteFilename()}.
+	 *
+	 * @return The content to write to the file
+	 */
+	public String getContentToWrite() {
+		return getField("ContentToWrite");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/TestDDARequest.java b/alien/src/net/pterodactylus/fcp/TestDDARequest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/TestDDARequest.java
@@ -0,0 +1,45 @@
+/*
+ * jFCPlib - TestDDARequest.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “TestDDARequest” initiates a DDA test sequence.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class TestDDARequest extends FcpMessage {
+
+	/**
+	 * Creates a new “TestDDARequest” command that initiates a DDA test.
+	 *
+	 * @param directory
+	 *            The directory you want to access files in
+	 * @param wantReadDirectory
+	 *            <code>true</code> if you want to read files from the directory
+	 * @param wantWriteDirectory
+	 *            <code>true</code> if you want to write files to the directory
+	 */
+	public TestDDARequest(String directory, boolean wantReadDirectory, boolean wantWriteDirectory) {
+		super("TestDDARequest");
+		setField("Directory", directory);
+		setField("WantReadDirectory", String.valueOf(wantReadDirectory));
+		setField("WantWriteDirectory", String.valueOf(wantWriteDirectory));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/TestDDAResponse.java b/alien/src/net/pterodactylus/fcp/TestDDAResponse.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/TestDDAResponse.java
@@ -0,0 +1,63 @@
+/*
+ * jFCPlib - TestDDAResponse.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * A “TestDDAResponse” is sent to let the node know that either created a file
+ * with the content from {@link TestDDAReply#getContentToWrite()} or that you
+ * read the content of the file given by {@link TestDDAReply#getReadFilename()}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class TestDDAResponse extends FcpMessage {
+
+	/**
+	 * Creates a new “TestDDAResponse” message that signals that you created the
+	 * file given by {@link TestDDAReply#getWriteFilename()} and wrote the
+	 * contents given by {@link TestDDAReply#getContentToWrite()} to it.
+	 *
+	 * @param directory
+	 *            The directory from the {@link TestDDARequest} command
+	 */
+	public TestDDAResponse(String directory) {
+		this(directory, null);
+	}
+
+	/**
+	 * Creates a new “TestDDAResponse” message that signals that you created the
+	 * file given by {@link TestDDAReply#getWriteFilename()} with the contents
+	 * given by {@link TestDDAReply#getContentToWrite()} to it (when you
+	 * specified that you want to write to the directory) and/or that you read
+	 * the file given by {@link TestDDAReply#getReadFilename()} (when you
+	 * specified you wanted to read the directory).
+	 *
+	 * @param directory
+	 *            The directory from the {@link TestDDARequest} command
+	 * @param readContent
+	 *            The read content, or <code>null</code> if you did not request
+	 *            read access
+	 */
+	public TestDDAResponse(String directory, String readContent) {
+		super("TestDDAResponse");
+		if (readContent != null) {
+			setField("ReadContent", readContent);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/URIGenerated.java b/alien/src/net/pterodactylus/fcp/URIGenerated.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/URIGenerated.java
@@ -0,0 +1,69 @@
+/*
+ * jFCPlib - URIGenerated.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version.
+ *
+ * This program 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 General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “URIGenerated” message signals the client that an URI was generated for a
+ * {@link ClientPut} (or {@link ClientPutDiskDir} or {@link ClientPutComplexDir}
+ * ) request.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class URIGenerated extends BaseMessage {
+
+	/**
+	 * Creates a new “URIGenerated” message that wraps the received message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	URIGenerated(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the identifier of the request that generated an URI.
+	 *
+	 * @return The identifier of the request
+	 */
+	public String getIdentifier() {
+		return getField("Identifier");
+	}
+
+	/**
+	 * Returns the URI that was generated by the request.
+	 *
+	 * @return The URI that was generated by the request
+	 */
+	public String getURI() {
+		return getField("URI");
+	}
+
+	/**
+	 * Returns whether the request that generated the URI is on the global queue
+	 * or on the client-local queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> if it is on the client-local queue
+	 */
+	public boolean isGlobal() {
+		return Boolean.parseBoolean(getField("Global"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/UnknownNodeIdentifier.java b/alien/src/net/pterodactylus/fcp/UnknownNodeIdentifier.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/UnknownNodeIdentifier.java
@@ -0,0 +1,49 @@
+/*
+ * jFCPlib - UnknownNodeIdentifier.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “UnknownNodeIdentifier” message signals the client that the node
+ * identifier given in a command like {@link ListPeer} is unknown.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class UnknownNodeIdentifier extends BaseMessage {
+
+	/**
+	 * Creates a new “UnknownNodeIdentifier” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	UnknownNodeIdentifier(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the unknown node identifier.
+	 *
+	 * @return The unknown node identifier
+	 */
+	public String getNodeIdentifier() {
+		return getField("NodeIdentifier");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/UnknownPeerNoteType.java b/alien/src/net/pterodactylus/fcp/UnknownPeerNoteType.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/UnknownPeerNoteType.java
@@ -0,0 +1,49 @@
+/*
+ * jFCPlib - UnknownPeerNoteType.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The ”UnknownPeerNoteType” message signals the client that the type of peer
+ * note used in a previous {@link ModifyPeerNote} is unknown.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class UnknownPeerNoteType extends BaseMessage {
+
+	/**
+	 * Creates a new ”UnknownPeerNoteType” message that wraps the received
+	 * message.
+	 *
+	 * @param receivedMessage
+	 *            The received message
+	 */
+	public UnknownPeerNoteType(FcpMessage receivedMessage) {
+		super(receivedMessage);
+	}
+
+	/**
+	 * Returns the type of peer note that is unkown.
+	 *
+	 * @return The unknown peer note type
+	 */
+	public int getPeerNoteType() {
+		return FcpUtils.safeParseInt(getField("PeerNoteType"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/UploadFrom.java b/alien/src/net/pterodactylus/fcp/UploadFrom.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/UploadFrom.java
@@ -0,0 +1,38 @@
+/*
+ * jFCPlib - UploadFrom.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Enumeration for the different values for the “UploadFrom” field in
+ * {@link ClientPut} and {@link ClientGet} requests.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public enum UploadFrom {
+
+	/** Request data follows the request. */
+	direct,
+
+	/** Request data is written to or read from disk. */
+	disk,
+
+	/** Request data is just a redirect. */
+	redirect;
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Verbosity.java b/alien/src/net/pterodactylus/fcp/Verbosity.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Verbosity.java
@@ -0,0 +1,105 @@
+/*
+ * jFCPlib - Verbosity.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * Convenicence class for verbosity handling. This might come in handy with the
+ * {@link ClientPut} and {@link ClientGet} requests. The verbosity is a bit-mask
+ * that can be composed of several bits. {@link #PROGRESS} and
+ * {@link #COMPRESSION} are single bits in that mask and can be combined into a
+ * new verbosity using {@link #add(Verbosity)}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class Verbosity {
+
+	/** Constant for no verbosity at all. */
+	public static final Verbosity NONE = new Verbosity(0);
+
+	/** Constant for progress message verbosity. */
+	public static final Verbosity PROGRESS = new Verbosity(1);
+
+	/** Constant for compression message verbosity. */
+	public static final Verbosity COMPRESSION = new Verbosity(512);
+
+	/** Constant for all events. */
+	public static final Verbosity ALL = new Verbosity(-1);
+
+	/** The verbosity level. */
+	private final int level;
+
+	/**
+	 * Creates a new verbosity with the given level.
+	 *
+	 * @param level
+	 *            The verbosity level
+	 */
+	private Verbosity(int level) {
+		this.level = level;
+	}
+
+	/**
+	 * Adds the given verbosity to this verbosity and returns a verbosity with
+	 * the new value. The value of this verbosity is not changed.
+	 *
+	 * @param verbosity
+	 *            The verbosity to add to this verbosity
+	 * @return The verbosity with the new level.
+	 */
+	public Verbosity add(Verbosity verbosity) {
+		return new Verbosity(level | verbosity.level);
+	}
+
+	/**
+	 * Checks whether this Verbosity contains all bits of the given Verbosity.
+	 *
+	 * @param verbosity
+	 *            The verbosity to check for in this Verbosity
+	 * @return <code>true</code> if and only if all set bits in the given
+	 *         Verbosity are also set in this Verbosity
+	 */
+	public boolean contains(Verbosity verbosity) {
+		return (level & verbosity.level) == verbosity.level;
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return String.valueOf(level);
+	}
+
+	/**
+	 * Parses the given string and creates a Verbosity with the given level.
+	 *
+	 * @param s
+	 *            The string to parse
+	 * @return The parsed verbosity, or {@link #NONE} if the string could not be
+	 *         parsed
+	 */
+	public static Verbosity valueOf(String s) {
+		try {
+			return new Verbosity(Integer.valueOf(s));
+		} catch (NumberFormatException nfe1) {
+			return NONE;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/Version.java b/alien/src/net/pterodactylus/fcp/Version.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/Version.java
@@ -0,0 +1,135 @@
+/*
+ * jFCPlib - Version.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+import java.util.StringTokenizer;
+
+/**
+ * Container for the “lastGoodVersion” field.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class Version {
+
+	/** The name of the node implementation. */
+	private final String nodeName;
+
+	/** The tree version of the node. */
+	private final String treeVersion;
+
+	/** The protocol version of the node. */
+	private final String protocolVersion;
+
+	/** The build number of the node. */
+	private final int buildNumber;
+
+	/**
+	 * Creates a new Version from the given string. The string consists of the
+	 * four required fields node name, tree version, protocol version, and build
+	 * number, separated by a comma.
+	 *
+	 * @param version
+	 *            The version string
+	 * @throws NullPointerException
+	 *             if <code>version</code> is <code>null</code>
+	 * @throws IllegalArgumentException
+	 *             if <code>version</code> is not in the right format
+	 */
+	public Version(String version) {
+		if (version == null) {
+			throw new NullPointerException("version must not be null");
+		}
+		StringTokenizer versionTokens = new StringTokenizer(version, ",");
+		if (versionTokens.countTokens() != 4) {
+			throw new IllegalArgumentException("version must consist of four fields");
+		}
+		this.nodeName = versionTokens.nextToken();
+		this.treeVersion = versionTokens.nextToken();
+		this.protocolVersion = versionTokens.nextToken();
+		try {
+			this.buildNumber = Integer.valueOf(versionTokens.nextToken());
+		} catch (NumberFormatException nfe1) {
+			throw new IllegalArgumentException("last part of version must be numeric", nfe1);
+		}
+	}
+
+	/**
+	 * Creates a new Version from the given parts.
+	 *
+	 * @param nodeName
+	 *            The name of the node implementation
+	 * @param treeVersion
+	 *            The tree version
+	 * @param protocolVersion
+	 *            The protocol version
+	 * @param buildNumber
+	 *            The build number of the node
+	 */
+	public Version(String nodeName, String treeVersion, String protocolVersion, int buildNumber) {
+		this.nodeName = nodeName;
+		this.treeVersion = treeVersion;
+		this.protocolVersion = protocolVersion;
+		this.buildNumber = buildNumber;
+	}
+
+	/**
+	 * Returns the name of the node implementation.
+	 *
+	 * @return The node name
+	 */
+	public String getNodeName() {
+		return nodeName;
+	}
+
+	/**
+	 * The tree version of the node.
+	 *
+	 * @return The tree version of the node
+	 */
+	public String getTreeVersion() {
+		return treeVersion;
+	}
+
+	/**
+	 * The protocol version of the node
+	 *
+	 * @return The protocol version of the node
+	 */
+	public String getProtocolVersion() {
+		return protocolVersion;
+	}
+
+	/**
+	 * The build number of the node.
+	 *
+	 * @return The build number of the node
+	 */
+	public int getBuildNumber() {
+		return buildNumber;
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return nodeName + "," + treeVersion + "," + protocolVersion + "," + buildNumber;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/WatchGlobal.java b/alien/src/net/pterodactylus/fcp/WatchGlobal.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/WatchGlobal.java
@@ -0,0 +1,58 @@
+/*
+ * jFCPlib - WatchGlobal.java - Copyright © 2008 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp;
+
+/**
+ * The “WatchGlobal” messages enables clients to watch the global queue in
+ * addition to the client-local queue.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class WatchGlobal extends FcpMessage {
+
+	/**
+	 * Enables or disables watching the global queue.
+	 *
+	 * @param enabled
+	 *            <code>true</code> to watch the global queue and the
+	 *            client-local queue, <code>false</code> to watch only the
+	 *            client-local queue
+	 */
+	public WatchGlobal(boolean enabled) {
+		this(enabled, Verbosity.ALL);
+	}
+
+	/**
+	 * Enables or disables watching the global queue, optionally masking out
+	 * certain events.
+	 *
+	 * @param enabled
+	 *            <code>true</code> to watch the global queue and the
+	 *            client-local queue, <code>false</code> to watch only the
+	 *            client-local queue
+	 * @param verbosityMask
+	 *            A verbosity mask that determines which events are received
+	 */
+	public WatchGlobal(boolean enabled, Verbosity verbosityMask) {
+		super("WatchGlobal");
+		setField("Enabled", String.valueOf(enabled));
+		setField("VerbosityMask", String.valueOf(verbosityMask));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/FcpClient.java b/alien/src/net/pterodactylus/fcp/highlevel/FcpClient.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/FcpClient.java
@@ -0,0 +1,1172 @@
+/*
+ * jFCPlib - FcpClient.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+import java.util.concurrent.CountDownLatch;
+
+import net.pterodactylus.fcp.AddPeer;
+import net.pterodactylus.fcp.ClientHello;
+import net.pterodactylus.fcp.CloseConnectionDuplicateClientName;
+import net.pterodactylus.fcp.DataFound;
+import net.pterodactylus.fcp.EndListPeerNotes;
+import net.pterodactylus.fcp.EndListPeers;
+import net.pterodactylus.fcp.EndListPersistentRequests;
+import net.pterodactylus.fcp.FCPPluginMessage;
+import net.pterodactylus.fcp.FCPPluginReply;
+import net.pterodactylus.fcp.FcpAdapter;
+import net.pterodactylus.fcp.FcpConnection;
+import net.pterodactylus.fcp.FcpListener;
+import net.pterodactylus.fcp.GenerateSSK;
+import net.pterodactylus.fcp.GetFailed;
+import net.pterodactylus.fcp.GetNode;
+import net.pterodactylus.fcp.ListPeerNotes;
+import net.pterodactylus.fcp.ListPeers;
+import net.pterodactylus.fcp.ListPersistentRequests;
+import net.pterodactylus.fcp.ModifyPeer;
+import net.pterodactylus.fcp.ModifyPeerNote;
+import net.pterodactylus.fcp.NodeData;
+import net.pterodactylus.fcp.NodeHello;
+import net.pterodactylus.fcp.NodeRef;
+import net.pterodactylus.fcp.Peer;
+import net.pterodactylus.fcp.PeerNote;
+import net.pterodactylus.fcp.PeerRemoved;
+import net.pterodactylus.fcp.PersistentGet;
+import net.pterodactylus.fcp.PersistentPut;
+import net.pterodactylus.fcp.ProtocolError;
+import net.pterodactylus.fcp.RemovePeer;
+import net.pterodactylus.fcp.SSKKeypair;
+import net.pterodactylus.fcp.SimpleProgress;
+import net.pterodactylus.fcp.WatchGlobal;
+import net.pterodactylus.util.filter.Filter;
+import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.thread.ObjectWrapper;
+
+/**
+ * High-level FCP client that hides the details of the underlying FCP
+ * implementation.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpClient {
+
+	/** Object used for synchronization. */
+	private final Object syncObject = new Object();
+
+	/** Listener management. */
+	private final FcpClientListenerManager fcpClientListenerManager = new FcpClientListenerManager(this);
+
+	/** The underlying FCP connection. */
+	private final FcpConnection fcpConnection;
+
+	/** The {@link NodeHello} data sent by the node on connection. */
+	private volatile NodeHello nodeHello;
+
+	/** Whether the client is currently connected. */
+	private volatile boolean connected;
+
+	/** The listener for “connection closed” events. */
+	private FcpListener connectionClosedListener;
+
+	/**
+	 * Creates an FCP client with the given name.
+	 *
+	 * @throws UnknownHostException
+	 *             if the hostname “localhost” is unknown
+	 */
+	public FcpClient() throws UnknownHostException {
+		this("localhost");
+	}
+
+	/**
+	 * Creates an FCP client.
+	 *
+	 * @param hostname
+	 *            The hostname of the Freenet node
+	 * @throws UnknownHostException
+	 *             if the given hostname can not be resolved
+	 */
+	public FcpClient(String hostname) throws UnknownHostException {
+		this(hostname, FcpConnection.DEFAULT_PORT);
+	}
+
+	/**
+	 * Creates an FCP client.
+	 *
+	 * @param hostname
+	 *            The hostname of the Freenet node
+	 * @param port
+	 *            The Freenet node’s FCP port
+	 * @throws UnknownHostException
+	 *             if the given hostname can not be resolved
+	 */
+	public FcpClient(String hostname, int port) throws UnknownHostException {
+		this(InetAddress.getByName(hostname), port);
+	}
+
+	/**
+	 * Creates an FCP client.
+	 *
+	 * @param host
+	 *            The host address of the Freenet node
+	 */
+	public FcpClient(InetAddress host) {
+		this(host, FcpConnection.DEFAULT_PORT);
+	}
+
+	/**
+	 * Creates an FCP client.
+	 *
+	 * @param host
+	 *            The host address of the Freenet node
+	 * @param port
+	 *            The Freenet node’s FCP port
+	 */
+	public FcpClient(InetAddress host, int port) {
+		this(new FcpConnection(host, port), false);
+	}
+
+	/**
+	 * Creates a new high-level FCP client that will use the given connection.
+	 * This constructor will assume that the FCP connection is already
+	 * connected.
+	 *
+	 * @param fcpConnection
+	 *            The FCP connection to use
+	 */
+	public FcpClient(FcpConnection fcpConnection) {
+		this(fcpConnection, true);
+	}
+
+	/**
+	 * Creates a new high-level FCP client that will use the given connection.
+	 *
+	 * @param fcpConnection
+	 *            The FCP connection to use
+	 * @param connected
+	 *            The initial status of the FCP connection
+	 */
+	public FcpClient(FcpConnection fcpConnection, boolean connected) {
+		this.fcpConnection = fcpConnection;
+		this.connected = connected;
+		connectionClosedListener = new FcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
+				FcpClient.this.connected = false;
+				fcpClientListenerManager.fireFcpClientDisconnected();
+			}
+		};
+		fcpConnection.addFcpListener(connectionClosedListener);
+	}
+
+	//
+	// LISTENER MANAGEMENT
+	//
+
+	/**
+	 * Adds an FCP listener to the underlying connection.
+	 *
+	 * @param fcpListener
+	 *            The FCP listener to add
+	 */
+	public void addFcpListener(FcpListener fcpListener) {
+		fcpConnection.addFcpListener(fcpListener);
+	}
+
+	/**
+	 * Removes an FCP listener from the underlying connection.
+	 *
+	 * @param fcpListener
+	 *            The FCP listener to remove
+	 */
+	public void removeFcpListener(FcpListener fcpListener) {
+		fcpConnection.removeFcpListener(fcpListener);
+	}
+
+	/**
+	 * Adds an FCP client listener to the list of registered listeners.
+	 *
+	 * @param fcpClientListener
+	 *            The FCP client listener to add
+	 */
+	public void addFcpClientListener(FcpClientListener fcpClientListener) {
+		fcpClientListenerManager.addListener(fcpClientListener);
+	}
+
+	/**
+	 * Removes an FCP client listener from the list of registered listeners.
+	 *
+	 * @param fcpClientListener
+	 *            The FCP client listener to remove
+	 */
+	public void removeFcpClientListener(FcpClientListener fcpClientListener) {
+		fcpClientListenerManager.removeListener(fcpClientListener);
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Returns the {@link NodeHello} object that the node returned when
+	 * connecting.
+	 *
+	 * @return The {@code NodeHello} data container
+	 */
+	public NodeHello getNodeHello() {
+		return nodeHello;
+	}
+
+	/**
+	 * Returns the underlying FCP connection.
+	 *
+	 * @return The underlying FCP connection
+	 */
+	public FcpConnection getConnection() {
+		return fcpConnection;
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Connects the FCP client.
+	 *
+	 * @param name
+	 *            The name of the client
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void connect(final String name) throws IOException, FcpException {
+		checkConnected(false);
+		connected = true;
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.connect();
+				ClientHello clientHello = new ClientHello(name);
+				fcpConnection.sendMessage(clientHello);
+				WatchGlobal watchGlobal = new WatchGlobal(true);
+				fcpConnection.sendMessage(watchGlobal);
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello) {
+				FcpClient.this.nodeHello = nodeHello;
+				completionLatch.countDown();
+			}
+		}.execute();
+	}
+
+	/**
+	 * Disconnects the FCP client.
+	 */
+	public void disconnect() {
+		synchronized (syncObject) {
+			fcpConnection.close();
+			syncObject.notifyAll();
+		}
+	}
+
+	/**
+	 * Returns whether this client is currently connected.
+	 *
+	 * @return {@code true} if the client is currently connected, {@code false}
+	 *         otherwise
+	 */
+	public boolean isConnected() {
+		return connected;
+	}
+
+	/**
+	 * Detaches this client from its underlying FCP connection.
+	 */
+	public void detach() {
+		fcpConnection.removeFcpListener(connectionClosedListener);
+	}
+
+	//
+	// PEER MANAGEMENT
+	//
+
+	/**
+	 * Returns all peers that the node has.
+	 *
+	 * @param withMetadata
+	 *            <code>true</code> to include peer metadata
+	 * @param withVolatile
+	 *            <code>true</code> to include volatile peer data
+	 * @return A set containing the node’s peers
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Peer> getPeers(final boolean withMetadata, final boolean withVolatile) throws IOException, FcpException {
+		final Set<Peer> peers = Collections.synchronizedSet(new HashSet<Peer>());
+		new ExtendedFcpAdapter() {
+
+			/** The ID of the “ListPeers” request. */
+			@SuppressWarnings("synthetic-access")
+			private String identifier = createIdentifier("list-peers");
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new ListPeers(identifier, withMetadata, withVolatile));
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
+				if (peer.getIdentifier().equals(identifier)) {
+					peers.add(peer);
+				}
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers) {
+				if (endListPeers.getIdentifier().equals(identifier)) {
+					completionLatch.countDown();
+				}
+			}
+		}.execute();
+		return peers;
+	}
+
+	/**
+	 * Returns all darknet peers.
+	 *
+	 * @param withMetadata
+	 *            <code>true</code> to include peer metadata
+	 * @param withVolatile
+	 *            <code>true</code> to include volatile peer data
+	 * @return A set containing the node’s darknet peers
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Peer> getDarknetPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
+		Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
+		Collection<Peer> darknetPeers = new HashSet<Peer>();
+		for (Peer peer : allPeers) {
+			if (!peer.isOpennet() && !peer.isSeed()) {
+				darknetPeers.add(peer);
+			}
+		}
+		return darknetPeers;
+	}
+
+	/**
+	 * Returns all opennet peers.
+	 *
+	 * @param withMetadata
+	 *            <code>true</code> to include peer metadata
+	 * @param withVolatile
+	 *            <code>true</code> to include volatile peer data
+	 * @return A set containing the node’s opennet peers
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Peer> getOpennetPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
+		Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
+		Collection<Peer> opennetPeers = new HashSet<Peer>();
+		for (Peer peer : allPeers) {
+			if (peer.isOpennet() && !peer.isSeed()) {
+				opennetPeers.add(peer);
+			}
+		}
+		return opennetPeers;
+	}
+
+	/**
+	 * Returns all seed peers.
+	 *
+	 * @param withMetadata
+	 *            <code>true</code> to include peer metadata
+	 * @param withVolatile
+	 *            <code>true</code> to include volatile peer data
+	 * @return A set containing the node’s seed peers
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Peer> getSeedPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
+		Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
+		Collection<Peer> seedPeers = new HashSet<Peer>();
+		for (Peer peer : allPeers) {
+			if (peer.isSeed()) {
+				seedPeers.add(peer);
+			}
+		}
+		return seedPeers;
+	}
+
+	/**
+	 * Adds the given peer to the node.
+	 *
+	 * @param peer
+	 *            The peer to add
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void addPeer(Peer peer) throws IOException, FcpException {
+		addPeer(peer.getNodeRef());
+	}
+
+	/**
+	 * Adds the peer defined by the noderef to the node.
+	 *
+	 * @param nodeRef
+	 *            The noderef that defines the new peer
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void addPeer(NodeRef nodeRef) throws IOException, FcpException {
+		addPeer(new AddPeer(nodeRef));
+	}
+
+	/**
+	 * Adds a peer, reading the noderef from the given URL.
+	 *
+	 * @param url
+	 *            The URL to read the noderef from
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void addPeer(URL url) throws IOException, FcpException {
+		addPeer(new AddPeer(url));
+	}
+
+	/**
+	 * Adds a peer, reading the noderef of the peer from the given file.
+	 * <strong>Note:</strong> the file to read the noderef from has to reside on
+	 * the same machine as the node!
+	 *
+	 * @param file
+	 *            The name of the file containing the peer’s noderef
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void addPeer(String file) throws IOException, FcpException {
+		addPeer(new AddPeer(file));
+	}
+
+	/**
+	 * Sends the given {@link AddPeer} message to the node. This method should
+	 * not be called directly. Use one of {@link #addPeer(Peer)},
+	 * {@link #addPeer(NodeRef)}, {@link #addPeer(URL)}, or
+	 * {@link #addPeer(String)} instead.
+	 *
+	 * @param addPeer
+	 *            The “AddPeer” message
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	private void addPeer(final AddPeer addPeer) throws IOException, FcpException {
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(addPeer);
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
+				completionLatch.countDown();
+			}
+		}.execute();
+	}
+
+	/**
+	 * Modifies the given peer.
+	 *
+	 * @param peer
+	 *            The peer to modify
+	 * @param allowLocalAddresses
+	 *            <code>true</code> to allow local address, <code>false</code>
+	 *            to not allow local address, <code>null</code> to not change
+	 *            the setting
+	 * @param disabled
+	 *            <code>true</code> to disable the peer, <code>false</code> to
+	 *            enable the peer, <code>null</code> to not change the setting
+	 * @param listenOnly
+	 *            <code>true</code> to enable “listen only” for the peer,
+	 *            <code>false</code> to disable it, <code>null</code> to not
+	 *            change it
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void modifyPeer(final Peer peer, final Boolean allowLocalAddresses, final Boolean disabled, final Boolean listenOnly) throws IOException, FcpException {
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new ModifyPeer(peer.getIdentity(), allowLocalAddresses, disabled, listenOnly));
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
+				completionLatch.countDown();
+			}
+		}.execute();
+	}
+
+	/**
+	 * Removes the given peer.
+	 *
+	 * @param peer
+	 *            The peer to remove
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void removePeer(final Peer peer) throws IOException, FcpException {
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new RemovePeer(peer.getIdentity()));
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved) {
+				completionLatch.countDown();
+			}
+		}.execute();
+	}
+
+	//
+	// PEER NOTES MANAGEMENT
+	//
+
+	/**
+	 * Returns the peer note of the given peer.
+	 *
+	 * @param peer
+	 *            The peer to get the note for
+	 * @return The peer’s note
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public PeerNote getPeerNote(final Peer peer) throws IOException, FcpException {
+		final ObjectWrapper<PeerNote> objectWrapper = new ObjectWrapper<PeerNote>();
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new ListPeerNotes(peer.getIdentity()));
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote) {
+				if (peerNote.getNodeIdentifier().equals(peer.getIdentity())) {
+					objectWrapper.set(peerNote);
+				}
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes) {
+				completionLatch.countDown();
+			}
+		}.execute();
+		return objectWrapper.get();
+	}
+
+	/**
+	 * Replaces the peer note for the given peer.
+	 *
+	 * @param peer
+	 *            The peer
+	 * @param noteText
+	 *            The new base64-encoded note text
+	 * @param noteType
+	 *            The type of the note (currently only <code>1</code> is
+	 *            allowed)
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void modifyPeerNote(final Peer peer, final String noteText, final int noteType) throws IOException, FcpException {
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new ModifyPeerNote(peer.getIdentity(), noteText, noteType));
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPeer(FcpConnection fcpConnection, Peer receivedPeer) {
+				if (receivedPeer.getIdentity().equals(peer.getIdentity())) {
+					completionLatch.countDown();
+				}
+			}
+		}.execute();
+	}
+
+	//
+	// KEY GENERATION
+	//
+
+	/**
+	 * Generates a new SSK key pair.
+	 *
+	 * @return The generated key pair
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public SSKKeypair generateKeyPair() throws IOException, FcpException {
+		final ObjectWrapper<SSKKeypair> sskKeypairWrapper = new ObjectWrapper<SSKKeypair>();
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new GenerateSSK());
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair) {
+				sskKeypairWrapper.set(sskKeypair);
+				completionLatch.countDown();
+			}
+		}.execute();
+		return sskKeypairWrapper.get();
+	}
+
+	//
+	// REQUEST MANAGEMENT
+	//
+
+	/**
+	 * Returns all currently visible persistent get requests.
+	 *
+	 * @param global
+	 *            <code>true</code> to return get requests from the global
+	 *            queue, <code>false</code> to only show requests from the
+	 *            client-local queue
+	 * @return All get requests
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Request> getGetRequests(final boolean global) throws IOException, FcpException {
+		return Filters.filteredCollection(getRequests(global), new Filter<Request>() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			public boolean filterObject(Request request) {
+				return request instanceof GetRequest;
+			}
+		});
+	}
+
+	/**
+	 * Returns all currently visible persistent put requests.
+	 *
+	 * @param global
+	 *            <code>true</code> to return put requests from the global
+	 *            queue, <code>false</code> to only show requests from the
+	 *            client-local queue
+	 * @return All put requests
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Request> getPutRequests(final boolean global) throws IOException, FcpException {
+		return Filters.filteredCollection(getRequests(global), new Filter<Request>() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			public boolean filterObject(Request request) {
+				return request instanceof PutRequest;
+			}
+		});
+	}
+
+	/**
+	 * Returns all currently visible persistent requests.
+	 *
+	 * @param global
+	 *            <code>true</code> to return requests from the global queue,
+	 *            <code>false</code> to only show requests from the client-local
+	 *            queue
+	 * @return All requests
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Collection<Request> getRequests(final boolean global) throws IOException, FcpException {
+		final Map<String, Request> requests = Collections.synchronizedMap(new HashMap<String, Request>());
+		new ExtendedFcpAdapter() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				fcpConnection.sendMessage(new ListPersistentRequests());
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet) {
+				if (!persistentGet.isGlobal() || global) {
+					GetRequest getRequest = new GetRequest(persistentGet);
+					requests.put(persistentGet.getIdentifier(), getRequest);
+				}
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see net.pterodactylus.fcp.FcpAdapter#receivedDataFound(net.pterodactylus.fcp.FcpConnection,
+			 *      net.pterodactylus.fcp.DataFound)
+			 */
+			@Override
+			public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
+				Request getRequest = requests.get(dataFound.getIdentifier());
+				if (getRequest == null) {
+					return;
+				}
+				getRequest.setComplete(true);
+				getRequest.setLength(dataFound.getDataLength());
+				getRequest.setContentType(dataFound.getMetadataContentType());
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see net.pterodactylus.fcp.FcpAdapter#receivedGetFailed(net.pterodactylus.fcp.FcpConnection,
+			 *      net.pterodactylus.fcp.GetFailed)
+			 */
+			@Override
+			public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
+				Request getRequest = requests.get(getFailed.getIdentifier());
+				if (getRequest == null) {
+					return;
+				}
+				getRequest.setComplete(true);
+				getRequest.setFailed(true);
+				getRequest.setFatal(getFailed.isFatal());
+				getRequest.setErrorCode(getFailed.getCode());
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see net.pterodactylus.fcp.FcpAdapter#receivedPersistentPut(net.pterodactylus.fcp.FcpConnection,
+			 *      net.pterodactylus.fcp.PersistentPut)
+			 */
+			@Override
+			public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
+				if (!persistentPut.isGlobal() || global) {
+					PutRequest putRequest = new PutRequest(persistentPut);
+					requests.put(persistentPut.getIdentifier(), putRequest);
+				}
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see net.pterodactylus.fcp.FcpAdapter#receivedSimpleProgress(net.pterodactylus.fcp.FcpConnection,
+			 *      net.pterodactylus.fcp.SimpleProgress)
+			 */
+			@Override
+			public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
+				Request request = requests.get(simpleProgress.getIdentifier());
+				if (request == null) {
+					return;
+				}
+				request.setTotalBlocks(simpleProgress.getTotal());
+				request.setRequiredBlocks(simpleProgress.getRequired());
+				request.setFailedBlocks(simpleProgress.getFailed());
+				request.setFatallyFailedBlocks(simpleProgress.getFatallyFailed());
+				request.setSucceededBlocks(simpleProgress.getSucceeded());
+				request.setFinalizedTotal(simpleProgress.isFinalizedTotal());
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
+				completionLatch.countDown();
+			}
+		}.execute();
+		return requests.values();
+	}
+
+	/**
+	 * Sends a message to a plugin and waits for the response.
+	 *
+	 * @param pluginClass
+	 *            The name of the plugin class
+	 * @param parameters
+	 *            The parameters for the plugin
+	 * @return The responses from the plugin
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public Map<String, String> sendPluginMessage(String pluginClass, Map<String, String> parameters) throws IOException, FcpException {
+		return sendPluginMessage(pluginClass, parameters, 0, null);
+	}
+
+	/**
+	 * Sends a message to a plugin and waits for the response.
+	 *
+	 * @param pluginClass
+	 *            The name of the plugin class
+	 * @param parameters
+	 *            The parameters for the plugin
+	 * @param dataLength
+	 *            The length of the optional data stream, or {@code 0} if there
+	 *            is no optional data stream
+	 * @param dataInputStream
+	 *            The input stream for the payload, or {@code null} if there is
+	 *            no payload
+	 * @return The responses from the plugin
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public Map<String, String> sendPluginMessage(final String pluginClass, final Map<String, String> parameters, final long dataLength, final InputStream dataInputStream) throws IOException, FcpException {
+		final Map<String, String> pluginReplies = Collections.synchronizedMap(new HashMap<String, String>());
+		new ExtendedFcpAdapter() {
+
+			@SuppressWarnings("synthetic-access")
+			private final String identifier = createIdentifier("FCPPluginMessage");
+
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				FCPPluginMessage fcpPluginMessage = new FCPPluginMessage(pluginClass);
+				for (Entry<String, String> parameter : parameters.entrySet()) {
+					fcpPluginMessage.setParameter(parameter.getKey(), parameter.getValue());
+				}
+				fcpPluginMessage.setIdentifier(identifier);
+				if ((dataLength > 0) && (dataInputStream != null)) {
+					fcpPluginMessage.setDataLength(dataLength);
+					fcpPluginMessage.setPayloadInputStream(dataInputStream);
+				}
+				fcpConnection.sendMessage(fcpPluginMessage);
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply) {
+				if (!fcpPluginReply.getIdentifier().equals(identifier)) {
+					return;
+				}
+				pluginReplies.putAll(fcpPluginReply.getReplies());
+				completionLatch.countDown();
+			}
+
+		}.execute();
+		return pluginReplies;
+	}
+
+	//
+	// NODE INFORMATION
+	//
+
+	/**
+	 * Returns information about the node.
+	 *
+	 * @param giveOpennetRef
+	 *            Whether to return the OpenNet reference
+	 * @param withPrivate
+	 *            Whether to return private node data
+	 * @param withVolatile
+	 *            Whether to return volatile node data
+	 * @return Node information
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public NodeData getNodeInformation(final Boolean giveOpennetRef, final Boolean withPrivate, final Boolean withVolatile) throws IOException, FcpException {
+		final ObjectWrapper<NodeData> nodeDataWrapper = new ObjectWrapper<NodeData>();
+		new ExtendedFcpAdapter() {
+
+			@Override
+			@SuppressWarnings("synthetic-access")
+			public void run() throws IOException {
+				GetNode getNodeMessage = new GetNode(giveOpennetRef, withPrivate, withVolatile);
+				fcpConnection.sendMessage(getNodeMessage);
+			}
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData) {
+				nodeDataWrapper.set(nodeData);
+				completionLatch.countDown();
+			}
+		}.execute();
+		return nodeDataWrapper.get();
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Creates a unique request identifier.
+	 *
+	 * @param basename
+	 *            The basename of the request
+	 * @return The created request identifier
+	 */
+	private String createIdentifier(String basename) {
+		return basename + "-" + System.currentTimeMillis() + "-" + (int) (Math.random() * Integer.MAX_VALUE);
+	}
+
+	/**
+	 * Checks whether the connection is in the required state.
+	 *
+	 * @param connected
+	 *            The required connection state
+	 * @throws FcpException
+	 *             if the connection is not in the required state
+	 */
+	private void checkConnected(boolean connected) throws FcpException {
+		if (this.connected != connected) {
+			throw new FcpException("Client is " + (connected ? "not" : "already") + " connected.");
+		}
+	}
+
+	/**
+	 * Tells the client that it is now disconnected. This method is called by
+	 * {@link ExtendedFcpAdapter} only.
+	 */
+	private void setDisconnected() {
+		connected = false;
+	}
+
+	/**
+	 * Implementation of an {@link FcpListener} that can store an
+	 * {@link FcpException} and wait for the arrival of a certain command.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	private abstract class ExtendedFcpAdapter extends FcpAdapter {
+
+		/** The count down latch used to wait for completion. */
+		protected final CountDownLatch completionLatch = new CountDownLatch(1);
+
+		/** The FCP exception, if any. */
+		protected FcpException fcpException;
+
+		/**
+		 * Creates a new extended FCP adapter.
+		 */
+		public ExtendedFcpAdapter() {
+			/* do nothing. */
+		}
+
+		/**
+		 * Executes the FCP commands in {@link #run()}, wrapping the execution
+		 * and catching exceptions.
+		 *
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 * @throws FcpException
+		 *             if an FCP error occurs
+		 */
+		@SuppressWarnings("synthetic-access")
+		public void execute() throws IOException, FcpException {
+			checkConnected(true);
+			fcpConnection.addFcpListener(this);
+			try {
+				run();
+				while (true) {
+					try {
+						completionLatch.await();
+						break;
+					} catch (InterruptedException ie1) {
+						/* ignore, we’ll loop. */
+					}
+				}
+			} catch (IOException ioe1) {
+				setDisconnected();
+				throw ioe1;
+			} finally {
+				fcpConnection.removeFcpListener(this);
+			}
+			if (fcpException != null) {
+				setDisconnected();
+				throw fcpException;
+			}
+		}
+
+		/**
+		 * The FCP commands that actually get executed.
+		 *
+		 * @throws IOException
+		 *             if an I/O error occurs
+		 */
+		public abstract void run() throws IOException;
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
+			fcpException = new FcpException("Connection closed", throwable);
+			completionLatch.countDown();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
+			fcpException = new FcpException("Connection closed, duplicate client name");
+			completionLatch.countDown();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
+			fcpException = new FcpException("Protocol error (" + protocolError.getCode() + ", " + protocolError.getCodeDescription());
+			completionLatch.countDown();
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListener.java b/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListener.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListener.java
@@ -0,0 +1,38 @@
+/*
+ * jFCPlib - FcpClientListener.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import java.util.EventListener;
+
+/**
+ * Listener for {@link FcpClient} events.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public interface FcpClientListener extends EventListener {
+
+	/**
+	 * Notifies a listener that the given FCP client was disconnected.
+	 *
+	 * @param fcpClient
+	 *            The FCP client that was disconnected
+	 */
+	public void fcpClientDisconnected(FcpClient fcpClient);
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListenerManager.java b/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListenerManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/FcpClientListenerManager.java
@@ -0,0 +1,51 @@
+/*
+ * jFCPlib - FcpClientListenerManager.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manages {@link FcpClientListener}s and fires events.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpClientListenerManager extends AbstractListenerManager<FcpClient, FcpClientListener> {
+
+	/**
+	 * Creates a new FCP client listener manager.
+	 *
+	 * @param fcpClient
+	 *            The source FCP client
+	 */
+	public FcpClientListenerManager(FcpClient fcpClient) {
+		super(fcpClient);
+	}
+
+	/**
+	 * Notifies all listeners that the FCP client was disconnected.
+	 *
+	 * @see FcpClientListener#fcpClientDisconnected(FcpClient)
+	 */
+	public void fireFcpClientDisconnected() {
+		for (FcpClientListener fcpClientListener : getListeners()) {
+			fcpClientListener.fcpClientDisconnected(getSource());
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/FcpException.java b/alien/src/net/pterodactylus/fcp/highlevel/FcpException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/FcpException.java
@@ -0,0 +1,67 @@
+/*
+ * jFCPlib - FcpException.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+/**
+ * Exception that signals an error in the FCP protocol.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class FcpException extends Exception {
+
+	/**
+	 * Creates a new FCP exception.
+	 */
+	public FcpException() {
+		super();
+	}
+
+	/**
+	 * Creates a new FCP exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public FcpException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Creates a new FCP exception.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public FcpException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * Creates a new FCP exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public FcpException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/GetRequest.java b/alien/src/net/pterodactylus/fcp/highlevel/GetRequest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/GetRequest.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - GetRequest.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import net.pterodactylus.fcp.PersistentGet;
+
+/**
+ * High-level wrapper around {@link PersistentGet}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class GetRequest extends Request {
+
+	/**
+	 * Creates a new get request.
+	 *
+	 * @param persistentGet
+	 *            The persistent Get request to wrap
+	 */
+	GetRequest(PersistentGet persistentGet) {
+		super(persistentGet.getIdentifier(), persistentGet.getClientToken(), persistentGet.isGlobal());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/PutRequest.java b/alien/src/net/pterodactylus/fcp/highlevel/PutRequest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/PutRequest.java
@@ -0,0 +1,40 @@
+/*
+ * jFCPlib - PutRequest.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import net.pterodactylus.fcp.PersistentPut;
+
+/**
+ * High-level wrapper around a {@link PersistentPut}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class PutRequest extends Request {
+
+	/**
+	 * Creates a new put request.
+	 *
+	 * @param persistentPut
+	 *            The FCP message to wrap
+	 */
+	PutRequest(PersistentPut persistentPut) {
+		super(persistentPut.getIdentifier(), persistentPut.getClientToken(), persistentPut.isGlobal());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/highlevel/Request.java b/alien/src/net/pterodactylus/fcp/highlevel/Request.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/highlevel/Request.java
@@ -0,0 +1,363 @@
+/*
+ * jFCPlib - Request.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.highlevel;
+
+import net.pterodactylus.fcp.PersistentGet;
+import net.pterodactylus.fcp.PersistentPut;
+
+/**
+ * Wrapper class around request responses from the node, such as
+ * {@link PersistentGet} or {@link PersistentPut}.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public abstract class Request {
+
+	/** The identifier of the request. */
+	private final String identifier;
+
+	/** The client token of the request. */
+	private final String clientToken;
+
+	/** Whether the request is on the global queue. */
+	private final boolean global;
+
+	/** Whether the get request is complete. */
+	private boolean complete;
+
+	/** Whether the get request has failed. */
+	private boolean failed;
+
+	/** The data length. */
+	private long length;
+
+	/** The mime type. */
+	private String contentType;
+
+	/** The error code in case of failure. */
+	private int errorCode;
+
+	/** Whether the failure is fatal. */
+	private boolean fatal;
+
+	/** The total number of blocks. */
+	private int totalBlocks;
+
+	/** The required number of blocks. */
+	private int requiredBlocks;
+
+	/** The successfully processed number of blocks. */
+	private int succeededBlocks;
+
+	/** The number of failed blocks. */
+	private int failedBlocks;
+
+	/** The number of fatally failed blocks. */
+	private int fatallyFailedBlocks;
+
+	/** Whether the total number of blocks is finalized. */
+	private boolean finalizedTotal;
+
+	/**
+	 * Creates a new request with the given identifier and client token.
+	 *
+	 * @param identifier
+	 *            The identifier of the request
+	 * @param clientToken
+	 *            The client token of the request
+	 * @param global
+	 *            <code>true</code> if the request is on the global queue,
+	 *            <code>false</code> otherwise
+	 */
+	protected Request(String identifier, String clientToken, boolean global) {
+		this.identifier = identifier;
+		this.clientToken = clientToken;
+		this.global = global;
+	}
+
+	/**
+	 * Returns the identifier of the request.
+	 *
+	 * @return The request’s identifier
+	 */
+	public String getIdentifier() {
+		return identifier;
+	}
+
+	/**
+	 * Returns the client token of the request.
+	 *
+	 * @return The request’s client token
+	 */
+	public String getClientToken() {
+		return clientToken;
+	}
+
+	/**
+	 * Returns whether this request is on the global queue.
+	 *
+	 * @return <code>true</code> if the request is on the global queue,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isGlobal() {
+		return global;
+	}
+
+	/**
+	 * Returns whether this request is complete.
+	 *
+	 * @return <code>true</code> if this request is complete, false otherwise
+	 */
+	public boolean isComplete() {
+		return complete;
+	}
+
+	/**
+	 * Sets whether this request is complete.
+	 *
+	 * @param complete
+	 *            <code>true</code> if this request is complete, false otherwise
+	 */
+	void setComplete(boolean complete) {
+		this.complete = complete;
+	}
+
+	/**
+	 * Returns whether this request has failed. This method should only be
+	 * called if {@link #isComplete()} returns <code>true</code>.
+	 *
+	 * @return <code>true</code> if this request failed, <code>false</code>
+	 *         otherwise
+	 */
+	public boolean hasFailed() {
+		return failed;
+	}
+
+	/**
+	 * Sets whether this request has failed.
+	 *
+	 * @param failed
+	 *            <code>true</code> if this request failed, <code>false</code>
+	 *            otherwise
+	 */
+	void setFailed(boolean failed) {
+		this.failed = failed;
+	}
+
+	/**
+	 * Returns the length of the data.
+	 *
+	 * @return The length of the data
+	 */
+	public long getLength() {
+		return length;
+	}
+
+	/**
+	 * Sets the length of the data.
+	 *
+	 * @param length
+	 *            The length of the data
+	 */
+	void setLength(long length) {
+		this.length = length;
+	}
+
+	/**
+	 * Returns the content type of the data.
+	 *
+	 * @return The content type of the data
+	 */
+	public String getContentType() {
+		return contentType;
+	}
+
+	/**
+	 * Sets the content type of the data.
+	 *
+	 * @param contentType
+	 *            The content type of the data
+	 */
+	void setContentType(String contentType) {
+		this.contentType = contentType;
+	}
+
+	/**
+	 * Returns the error code. This method should only be called if
+	 * {@link #hasFailed()} returns <code>true</code>.
+	 *
+	 * @return The error code
+	 */
+	public int getErrorCode() {
+		return errorCode;
+	}
+
+	/**
+	 * Sets the error code.
+	 *
+	 * @param errorCode
+	 *            The error code
+	 */
+	void setErrorCode(int errorCode) {
+		this.errorCode = errorCode;
+	}
+
+	/**
+	 * Returns whether this request has fatally failed, i.e. repitition will not
+	 * cause the request to succeed.
+	 *
+	 * @return <code>true</code> if this request can not be made succeed by
+	 *         repeating, <code>false</code> otherwise
+	 */
+	public boolean isFatal() {
+		return fatal;
+	}
+
+	/**
+	 * Sets whether this request has fatally failed.
+	 *
+	 * @param fatal
+	 *            <code>true</code> if this request failed fatally,
+	 *            <code>false</code> otherwise
+	 */
+	void setFatal(boolean fatal) {
+		this.fatal = fatal;
+	}
+
+	/**
+	 * Returns the total number of blocks of this request.
+	 *
+	 * @return This request’s total number of blocks
+	 */
+	public int getTotalBlocks() {
+		return totalBlocks;
+	}
+
+	/**
+	 * Sets the total number of blocks of this request.
+	 *
+	 * @param totalBlocks
+	 *            This request’s total number of blocks
+	 */
+	void setTotalBlocks(int totalBlocks) {
+		this.totalBlocks = totalBlocks;
+	}
+
+	/**
+	 * Returns the number of required blocks. Any progress percentages should be
+	 * calculated against this value as 100%. Also, as long as
+	 * {@link #isFinalizedTotal()} returns {@code false} this value might
+	 * change.
+	 *
+	 * @return The number of required blocks
+	 */
+	public int getRequiredBlocks() {
+		return requiredBlocks;
+	}
+
+	/**
+	 * Sets the number of required blocks.
+	 *
+	 * @param requiredBlocks
+	 *            The number of required blocks
+	 */
+	void setRequiredBlocks(int requiredBlocks) {
+		this.requiredBlocks = requiredBlocks;
+	}
+
+	/**
+	 * Returns the number of succeeded blocks.
+	 *
+	 * @return The number of succeeded blocks
+	 */
+	public int getSucceededBlocks() {
+		return succeededBlocks;
+	}
+
+	/**
+	 * Sets the number of succeeded blocks.
+	 *
+	 * @param succeededBlocks
+	 *            The number of succeeded blocks
+	 */
+	void setSucceededBlocks(int succeededBlocks) {
+		this.succeededBlocks = succeededBlocks;
+	}
+
+	/**
+	 * Returns the number of failed blocks. These blocks may be retried untill
+	 * the maximum number of retries has been reached.
+	 *
+	 * @return The number of failed blocks
+	 */
+	public int getFailedBlocks() {
+		return failedBlocks;
+	}
+
+	/**
+	 * Sets the number of failed blocks.
+	 *
+	 * @param failedBlocks
+	 *            The number of failed blocks
+	 */
+	void setFailedBlocks(int failedBlocks) {
+		this.failedBlocks = failedBlocks;
+	}
+
+	/**
+	 * Returns the number of fatally failed blocks.
+	 *
+	 * @return The number of fatally failed blocks
+	 */
+	public int getFatallyFailedBlocks() {
+		return fatallyFailedBlocks;
+	}
+
+	/**
+	 * Sets the number of fatally failed blocks.
+	 *
+	 * @param fatallyFailedBlocks
+	 *            The number of fatally failed blocks
+	 */
+	void setFatallyFailedBlocks(int fatallyFailedBlocks) {
+		this.fatallyFailedBlocks = fatallyFailedBlocks;
+	}
+
+	/**
+	 * Returns whether the number of blocks has been finalized.
+	 *
+	 * @return {@code true} if the number of blocks is finalized, {@code false}
+	 *         otherwise
+	 */
+	public boolean isFinalizedTotal() {
+		return finalizedTotal;
+	}
+
+	/**
+	 * Sets whether the number of blocks has been finalized.
+	 *
+	 * @param finalizedTotal
+	 *            {@code true} if the number of blocks has been finalized,
+	 *            {@code false} otherwise
+	 */
+	void setFinalizedTotal(boolean finalizedTotal) {
+		this.finalizedTotal = finalizedTotal;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/fcp/package-info.java b/alien/src/net/pterodactylus/fcp/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/package-info.java
@@ -0,0 +1,36 @@
+/**
+ * Package that holds all the message types that are used in the communication
+ * with a Freenet Node.
+ * 
+ * <h2>Usage</h2>
+ * 
+ * This library was designed to implement the full range of the Freenet Client
+ * Protocol, Version 2.0. At the moment the library provides a rather low-level
+ * approach, wrapping each FCP message into its own object but some kind of
+ * high-level client that does not require any interfaces to be implemented
+ * will probably provided as well.
+ * 
+ * First, create a connection to the node:
+ * 
+ * <pre>
+ * FcpConnection fcpConnection = new FcpConnection();
+ * </pre>
+ * 
+ * Now implement the {@link net.pterodactylus.fcp.FcpListener} interface
+ * and handle all incoming events.
+ * 
+ * <pre>
+ * public class MyClass implements FcpListener {
+ * 
+ * 	public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
+ * 		…
+ * 	}
+ * 
+ * 	// implement all further methods here
+ * 
+ * }
+ * </pre>
+ */
+
+package net.pterodactylus.fcp;
+
diff --git a/alien/src/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java b/alien/src/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/fcp/plugin/WebOfTrustPlugin.java
@@ -0,0 +1,661 @@
+/*
+ * jFCPlib - WebOfTrustPlugin.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.fcp.plugin;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.fcp.highlevel.FcpClient;
+import net.pterodactylus.fcp.highlevel.FcpException;
+
+/**
+ * Simplifies handling of the web-of-trust plugin.
+ *
+ * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+ */
+public class WebOfTrustPlugin {
+
+	/** The FCP client to use. */
+	private final FcpClient fcpClient;
+
+	/**
+	 * Creates a new web-of-trust plugin wrapper around the given FCP client.
+	 *
+	 * @param fcpClient
+	 *            The FCP client to use for communication with the web-of-trust
+	 *            plugin
+	 */
+	public WebOfTrustPlugin(FcpClient fcpClient) {
+		this.fcpClient = fcpClient;
+	}
+
+	/**
+	 * Creates a new identity.
+	 *
+	 * @param nickname
+	 *            The nickname of the new identity
+	 * @param context
+	 *            The context for the new identity
+	 * @param publishTrustList
+	 *            {@code true} if the new identity should publish its trust list
+	 * @return The new identity
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public OwnIdentity createIdentity(String nickname, String context, boolean publishTrustList) throws IOException, FcpException {
+		return createIdentity(nickname, context, publishTrustList, null, null);
+	}
+
+	/**
+	 * Creates a new identity from the given request and insert URI.
+	 *
+	 * @param nickname
+	 *            The nickname of the new identity
+	 * @param context
+	 *            The context for the new identity
+	 * @param publishTrustList
+	 *            {@code true} if the new identity should publish its trust list
+	 * @param requestUri
+	 *            The request URI of the identity
+	 * @param insertUri
+	 *            The insert URI of the identity
+	 * @return The new identity
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public OwnIdentity createIdentity(String nickname, String context, boolean publishTrustList, String requestUri, String insertUri) throws IOException, FcpException {
+		Map<String, String> parameters = new HashMap<String, String>();
+		parameters.put("Message", "CreateIdentity");
+		parameters.put("Nickname", nickname);
+		parameters.put("Context", context);
+		parameters.put("PublishTrustList", String.valueOf(publishTrustList));
+		if ((requestUri != null) && (insertUri != null)) {
+			parameters.put("RequestURI", requestUri);
+			parameters.put("InsertURI", insertUri);
+		}
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", parameters);
+		if (!replies.get("Message").equals("IdentityCreated")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “IdentityCreated” message!");
+		}
+		String identifier = replies.get("ID");
+		String newRequestUri = replies.get("RequestURI");
+		String newInsertUri = replies.get("InsertURI");
+		return new OwnIdentity(identifier, nickname, newRequestUri, newInsertUri);
+	}
+
+	/**
+	 * Returns all own identities of the web-of-trust plugins. Almost all other
+	 * commands require an {@link OwnIdentity} to return meaningful values.
+	 *
+	 * @return All own identities of the web-of-trust plugin
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Set<OwnIdentity> getOwnIdentites() throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetOwnIdentities"));
+		if (!replies.get("Message").equals("OwnIdentities")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “OwnIdentities” message!");
+		}
+		Set<OwnIdentity> ownIdentities = new HashSet<OwnIdentity>();
+		for (int identityIndex = 1; replies.containsKey("Identity" + identityIndex); identityIndex++) {
+			String identity = replies.get("Identity" + identityIndex);
+			String nickname = replies.get("Nickname" + identityIndex);
+			String requestUri = replies.get("RequestURI" + identityIndex);
+			String insertUri = replies.get("InsertURI" + identityIndex);
+			ownIdentities.add(new OwnIdentity(identity, nickname, requestUri, insertUri));
+		}
+		return ownIdentities;
+	}
+
+	/**
+	 * Returns the trust given to the identity with the given identifier by the
+	 * given own identity.
+	 *
+	 * @param ownIdentity
+	 *            The own identity that is used to calculate trust values
+	 * @param identifier
+	 *            The identifier of the identity whose trust to get
+	 * @return The request identity trust
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public CalculatedTrust getIdentityTrust(OwnIdentity ownIdentity, String identifier) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetIdentity", "TreeOwner", ownIdentity.getIdentifier(), "Identity", identifier));
+		if (!replies.get("Message").equals("Identity")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “Identity” message!");
+		}
+		Byte trust = null;
+		try {
+			trust = Byte.valueOf(replies.get("Trust"));
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		Integer score = null;
+		try {
+			score = Integer.valueOf(replies.get("Score"));
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		Integer rank = null;
+		try {
+			rank = Integer.valueOf(replies.get("Rank"));
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		return new CalculatedTrust(trust, score, rank);
+	}
+
+	/**
+	 * Adds a new identity by its request URI.
+	 *
+	 * @param requestUri
+	 *            The request URI of the identity to add
+	 * @return The added identity
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Identity addIdentity(String requestUri) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "AddIdentity", "RequestURI", requestUri));
+		if (!replies.get("Message").equals("IdentityAdded")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “IdentityAdded” message!");
+		}
+		String identifier = replies.get("ID");
+		String nickname = replies.get("Nickname");
+		return new Identity(identifier, nickname, requestUri);
+	}
+
+	/**
+	 * Returns identities by the given score.
+	 *
+	 * @param ownIdentity
+	 *            The own identity
+	 * @param context
+	 *            The context to get the identities for
+	 * @param positive
+	 *            {@code null} to return neutrally trusted identities, {@code
+	 *            true} to return positively trusted identities, {@code false}
+	 *            for negatively trusted identities
+	 * @return The trusted identites
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Set<Identity> getIdentitesByScore(OwnIdentity ownIdentity, String context, Boolean positive) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetIdentitiesByScore", "TreeOwner", ownIdentity.getIdentifier(), "Context", context, "Selection", ((positive == null) ? "0" : (positive ? "+" : "-"))));
+		if (!replies.get("Message").equals("Identities")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “Identities” message!");
+		}
+		Set<Identity> identities = new HashSet<Identity>();
+		for (int identityIndex = 1; replies.containsKey("Identity" + identityIndex); identityIndex++) {
+			String identifier = replies.get("Identity" + identityIndex);
+			String nickname = replies.get("Nickname" + identityIndex);
+			String requestUri = replies.get("RequestURI" + identityIndex);
+			identities.add(new Identity(identifier, nickname, requestUri));
+		}
+		return identities;
+	}
+
+	/**
+	 * Returns the identities that trust the given identity.
+	 *
+	 * @param identity
+	 *            The identity to get the trusters for
+	 * @param context
+	 *            The context to get the trusters for
+	 * @return The identities and their trust values
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Map<Identity, IdentityTrust> getTrusters(Identity identity, String context) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetTrusters", "Identity", identity.getIdentifier(), "Context", context));
+		if (!replies.get("Message").equals("Identities")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “Identities” message!");
+		}
+		Map<Identity, IdentityTrust> identityTrusts = new HashMap<Identity, IdentityTrust>();
+		for (int identityIndex = 1; replies.containsKey("Identity" + identityIndex); identityIndex++) {
+			String identifier = replies.get("Identity" + identityIndex);
+			String nickname = replies.get("Nickname" + identityIndex);
+			String requestUri = replies.get("RequestURI" + identityIndex);
+			byte trust = Byte.parseByte(replies.get("Value" + identityIndex));
+			String comment = replies.get("Comment" + identityIndex);
+			identityTrusts.put(new Identity(identifier, nickname, requestUri), new IdentityTrust(trust, comment));
+		}
+		return identityTrusts;
+	}
+
+	/**
+	 * Returns the identities that given identity trusts.
+	 *
+	 * @param identity
+	 *            The identity to get the trustees for
+	 * @param context
+	 *            The context to get the trustees for
+	 * @return The identities and their trust values
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public Map<Identity, IdentityTrust> getTrustees(Identity identity, String context) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetTrustees", "Identity", identity.getIdentifier(), "Context", context));
+		if (!replies.get("Message").equals("Identities")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “Identities” message!");
+		}
+		Map<Identity, IdentityTrust> identityTrusts = new HashMap<Identity, IdentityTrust>();
+		for (int identityIndex = 1; replies.containsKey("Identity" + identityIndex); identityIndex++) {
+			String identifier = replies.get("Identity" + identityIndex);
+			String nickname = replies.get("Nickname" + identityIndex);
+			String requestUri = replies.get("RequestURI" + identityIndex);
+			byte trust = Byte.parseByte(replies.get("Value" + identityIndex));
+			String comment = replies.get("Comment" + identityIndex);
+			identityTrusts.put(new Identity(identifier, nickname, requestUri), new IdentityTrust(trust, comment));
+		}
+		return identityTrusts;
+	}
+
+	/**
+	 * Sets the trust given to the given identify by the given own identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity that gives the trust
+	 * @param identity
+	 *            The identity that receives the trust
+	 * @param trust
+	 *            The trust value (ranging from {@code -100} to {@code 100}
+	 * @param comment
+	 *            The comment for setting the trust
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void setTrust(OwnIdentity ownIdentity, Identity identity, byte trust, String comment) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "SetTrust", "Truster", ownIdentity.getIdentifier(), "Trustee", identity.getIdentifier(), "Value", String.valueOf(trust), "Comment", comment));
+		if (!replies.get("Message").equals("TrustSet")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “TrustSet” message!");
+		}
+	}
+
+	/**
+	 * Adds the given context to the given identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity to add the context to
+	 * @param context
+	 *            The context to add
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void addContext(OwnIdentity ownIdentity, String context) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "AddContext", "Identity", ownIdentity.getIdentifier(), "Context", context));
+		if (!replies.get("Message").equals("ContextAdded")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “ContextAdded” message!");
+		}
+	}
+
+	/**
+	 * Removes the given context from the given identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity to remove the context from
+	 * @param context
+	 *            The context to remove
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void removeContext(OwnIdentity ownIdentity, String context) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "RemoveContext", "Identity", ownIdentity.getIdentifier(), "Context", context));
+		if (!replies.get("Message").equals("ContextRemoved")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “ContextRemoved” message!");
+		}
+	}
+
+	/**
+	 * Sets the given property for the given identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity to set a property for
+	 * @param property
+	 *            The name of the property to set
+	 * @param value
+	 *            The value of the property to set
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void setProperty(OwnIdentity ownIdentity, String property, String value) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "SetProperty", "Identity", ownIdentity.getIdentifier(), "Property", property, "Value", value));
+		if (!replies.get("Message").equals("PropertyAdded")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “PropertyAdded” message!");
+		}
+	}
+
+	/**
+	 * Returns the value of the given property for the given identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity to get a property for
+	 * @param property
+	 *            The name of the property to get
+	 * @return The value of the property
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public String getProperty(OwnIdentity ownIdentity, String property) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "GetProperty", "Identity", ownIdentity.getIdentifier(), "Property", property));
+		if (!replies.get("Message").equals("PropertyValue")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “PropertyValue” message!");
+		}
+		return replies.get("Property");
+	}
+
+	/**
+	 * Removes the given property from the given identity.
+	 *
+	 * @param ownIdentity
+	 *            The identity to remove a property from
+	 * @param property
+	 *            The name of the property to remove
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws FcpException
+	 *             if an FCP error occurs
+	 */
+	public void removeProperty(OwnIdentity ownIdentity, String property) throws IOException, FcpException {
+		Map<String, String> replies = fcpClient.sendPluginMessage("plugins.WoT.WoT", createParameters("Message", "RemoveProperty", "Identity", ownIdentity.getIdentifier(), "Property", property));
+		if (!replies.get("Message").equals("PropertyRemoved")) {
+			throw new FcpException("WebOfTrust Plugin did not reply with “PropertyRemoved” message!");
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Creates a map from each pair of parameters in the given array.
+	 *
+	 * @param parameters
+	 *            The array of parameters
+	 * @return The map created from the array
+	 * @throws ArrayIndexOutOfBoundsException
+	 *             if the given parameter array does not contains an even number
+	 *             of elements
+	 */
+	private Map<String, String> createParameters(String... parameters) throws ArrayIndexOutOfBoundsException {
+		Map<String, String> parameterMap = new HashMap<String, String>();
+		for (int index = 0; index < parameters.length; index += 2) {
+			parameterMap.put(parameters[index], parameters[index + 1]);
+		}
+		return parameterMap;
+	}
+
+	/**
+	 * Wrapper around a web-of-trust identity.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	public static class Identity {
+
+		/** The identity’s identifier. */
+		private final String identifier;
+
+		/** The identity’s nickname. */
+		private final String nickname;
+
+		/** The identity’s request URI. */
+		private final String requestUri;
+
+		/**
+		 * Creates a new identity.
+		 *
+		 * @param identifier
+		 *            The identifies of the identity
+		 * @param nickname
+		 *            The nickname of the identity
+		 * @param requestUri
+		 *            The request URI of the identity
+		 */
+		public Identity(String identifier, String nickname, String requestUri) {
+			this.identifier = identifier;
+			this.nickname = nickname;
+			this.requestUri = requestUri;
+		}
+
+		/**
+		 * Returns the identifier of this identity.
+		 *
+		 * @return This identity’s identifier
+		 */
+		public String getIdentifier() {
+			return identifier;
+		}
+
+		/**
+		 * Returns the nickname of this identity.
+		 *
+		 * @return This identity’s nickname
+		 */
+		public String getNickname() {
+			return nickname;
+		}
+
+		/**
+		 * Returns the request URI of this identity.
+		 *
+		 * @return This identity’s request URI
+		 */
+		public String getRequestUri() {
+			return requestUri;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean equals(Object obj) {
+			if ((obj == null) || (obj.getClass() != this.getClass())) {
+				return false;
+			}
+			Identity identity = (Identity) obj;
+			return identifier.equals(identity.identifier);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public int hashCode() {
+			return identifier.hashCode();
+		}
+
+	}
+
+	/**
+	 * Container for the trust given from one identity to another.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	public static class IdentityTrust {
+
+		/** The trust given to the identity. */
+		private final byte trust;
+
+		/** The command for the trust value. */
+		private final String comment;
+
+		/**
+		 * Creates a new identity trust container.
+		 *
+		 * @param trust
+		 *            The trust given to the identity
+		 * @param comment
+		 *            The comment for the trust value
+		 */
+		public IdentityTrust(byte trust, String comment) {
+			this.trust = trust;
+			this.comment = comment;
+		}
+
+		/**
+		 * Returns the trust value given to the identity.
+		 *
+		 * @return The trust value
+		 */
+		public byte getTrust() {
+			return trust;
+		}
+
+		/**
+		 * Returns the comment for the trust value.
+		 *
+		 * @return The comment for the trust value
+		 */
+		public String getComment() {
+			return comment;
+		}
+
+	}
+
+	/**
+	 * Container that stores the trust that is calculated by taking all trustees
+	 * and their trust lists into account.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	public static class CalculatedTrust {
+
+		/** The calculated trust value. */
+		private final Byte trust;
+
+		/** The calculated score value. */
+		private final Integer score;
+
+		/** The calculated rank. */
+		private final Integer rank;
+
+		/**
+		 * Creates a new calculated trust container.
+		 *
+		 * @param trust
+		 *            The calculated trust value
+		 * @param score
+		 *            The calculated score value
+		 * @param rank
+		 *            The calculated rank of the
+		 */
+		public CalculatedTrust(Byte trust, Integer score, Integer rank) {
+			this.trust = trust;
+			this.score = score;
+			this.rank = rank;
+		}
+
+		/**
+		 * Returns the calculated trust value.
+		 *
+		 * @return The calculated trust value, or {@code null} if the trust
+		 *         value is not known
+		 */
+		public Byte getTrust() {
+			return trust;
+		}
+
+		/**
+		 * Returns the calculated score value.
+		 *
+		 * @return The calculated score value, or {@code null} if the score
+		 *         value is not known
+		 */
+		public Integer getScore() {
+			return score;
+		}
+
+		/**
+		 * Returns the calculated rank.
+		 *
+		 * @return The calculated rank, or {@code null} if the rank is not known
+		 */
+		public Integer getRank() {
+			return rank;
+		}
+
+	}
+
+	/**
+	 * Wrapper around a web-of-trust own identity.
+	 *
+	 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
+	 */
+	public static class OwnIdentity extends Identity {
+
+		/** The identity’s insert URI. */
+		private final String insertUri;
+
+		/**
+		 * Creates a new web-of-trust own identity.
+		 *
+		 * @param identifier
+		 *            The identifier of the identity
+		 * @param nickname
+		 *            The nickname of the identity
+		 * @param requestUri
+		 *            The request URI of the identity
+		 * @param insertUri
+		 *            The insert URI of the identity
+		 */
+		public OwnIdentity(String identifier, String nickname, String requestUri, String insertUri) {
+			super(identifier, nickname, requestUri);
+			this.insertUri = insertUri;
+		}
+
+		/**
+		 * Returns the insert URI of this identity.
+		 *
+		 * @return This identity’s insert URI
+		 */
+		public String getInsertUri() {
+			return insertUri;
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/beans/AbstractBean.java b/alien/src/net/pterodactylus/util/beans/AbstractBean.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/beans/AbstractBean.java
@@ -0,0 +1,111 @@
+/*
+ * utils - AbstractBean.java - Copyright © 2008-2010 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.util.beans;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Abstract bean super class that contains property change listener management.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractBean {
+
+	/** Property change listeners. */
+	private final List<PropertyChangeListener> propertyChangeListeners = Collections.synchronizedList(new ArrayList<PropertyChangeListener>());
+
+	/**
+	 * Adds a property change listener.
+	 *
+	 * @param propertyChangeListener
+	 *            The property change listener to add
+	 */
+	public void addPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
+		propertyChangeListeners.add(propertyChangeListener);
+	}
+
+	/**
+	 * Removes a property change listener.
+	 *
+	 * @param propertyChangeListener
+	 *            The property change listener to remove
+	 */
+	public void removePropertyChangeListener(PropertyChangeListener propertyChangeListener) {
+		propertyChangeListeners.remove(propertyChangeListener);
+	}
+
+	/**
+	 * Notifies all listeners that a property has changed.
+	 *
+	 * @param property
+	 *            The name of the property
+	 * @param oldValue
+	 *            The old value of the property
+	 * @param newValue
+	 *            The new value of the property
+	 */
+	protected void firePropertyChange(String property, Object oldValue, Object newValue) {
+		PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(this, property, oldValue, newValue);
+		for (PropertyChangeListener propertyChangeListener : propertyChangeListeners) {
+			propertyChangeListener.propertyChange(propertyChangeEvent);
+		}
+
+	}
+
+	/**
+	 * Fires a property change event if the two values are not equal.
+	 *
+	 * @param propertyName
+	 *            The name of the property
+	 * @param oldValue
+	 *            The old value of the property
+	 * @param newValue
+	 *            The new value of the property
+	 */
+	protected void fireIfPropertyChanged(String propertyName, Object oldValue, Object newValue) {
+		if (!equal(oldValue, newValue)) {
+			firePropertyChange(propertyName, oldValue, newValue);
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Compares the two objects and returns whether they are equal according to
+	 * {@link Object#equals(Object)}. This method takes <code>null</code>
+	 * into account as a valid value for an object.
+	 *
+	 * @param first
+	 *            The first object
+	 * @param second
+	 *            The second object
+	 * @return <code>true</code> if the two objects are equal,
+	 *         <code>false</code> otherwise
+	 */
+	private boolean equal(Object first, Object second) {
+		return ((first == null) && (second == null)) || ((first != null) && first.equals(second)) || second.equals(first);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/AbstractCache.java b/alien/src/net/pterodactylus/util/cache/AbstractCache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/AbstractCache.java
@@ -0,0 +1,58 @@
+/*
+ * utils - AbstractCache.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+/**
+ * Abstract base implementation of a {@link Cache}. All implementations should
+ * extend this base class.
+ *
+ * @param <K>
+ *            The type of the key
+ * @param <V>
+ *            The value of the key
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractCache<K, V> implements Cache<K, V> {
+
+	/** The value retriever. */
+	private final ValueRetriever<K, V> valueRetriever;
+
+	/**
+	 * Creates a new abstract cache.
+	 *
+	 * @param valueRetriever
+	 *            The value retriever
+	 */
+	protected AbstractCache(ValueRetriever<K, V> valueRetriever) {
+		this.valueRetriever = valueRetriever;
+	}
+
+	/**
+	 * Retrieves a value from the value retriever.
+	 *
+	 * @param key
+	 *            The key of the value to retrieve
+	 * @return The value of the key, or {@code null} if there is no value
+	 * @throws CacheException
+	 *             if an error occurs retrieving the value
+	 */
+	protected CacheItem<V> retrieveValue(K key) throws CacheException {
+		return valueRetriever.retrieve(key);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/Cache.java b/alien/src/net/pterodactylus/util/cache/Cache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/Cache.java
@@ -0,0 +1,77 @@
+/*
+ * utils - Cache.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+import java.util.WeakHashMap;
+
+/**
+ * Interface for caches with different strategies.
+ *
+ * @param <K>
+ *            The type of the key
+ * @param <V>
+ *            The type of the value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Cache<K, V> {
+
+	/**
+	 * Checks whether this cache contains a value for the given key. No query to
+	 * the underlying {@link ValueRetriever} will be made! Note that it is legal
+	 * for this method to return {@code true} but for a following
+	 * {@link #get(Object)} to return {@code null} (see {@link WeakHashMap}) for
+	 * a possible explanation).
+	 *
+	 * @param key
+	 *            The key to check for
+	 * @return {@code true} if this cache contains a value for the given key,
+	 *         {@code false} otherwise
+	 */
+	public boolean contains(K key);
+
+	/**
+	 * Returns a value from the cache. If this cache does not contain a value
+	 * for the given key, the underlying {@link ValueRetriever} is asked to
+	 * retrieve the value. This operation may result in a {@link CacheException}
+	 * to be thrown. The returned value is cached if it is non-{@code null}.
+	 *
+	 * @param key
+	 *            The key to get the value for
+	 * @return The value of the key, or {@code null} if there is no value for
+	 *         the key
+	 * @throws CacheException
+	 *             if an error occurs retrieving the value from the underlying
+	 *             {@link ValueRetriever}
+	 */
+	public V get(K key) throws CacheException;
+
+	/**
+	 * Removes all cached values. For non-memory based caches this operation may
+	 * be slow.
+	 */
+	public void clear();
+
+	/**
+	 * Returns the number of currently cached values. For non-memory based
+	 * caches this operation may be slow.
+	 *
+	 * @return The number of cached values
+	 */
+	public int size();
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/CacheException.java b/alien/src/net/pterodactylus/util/cache/CacheException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/CacheException.java
@@ -0,0 +1,66 @@
+/*
+ * utils - CacheException.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+/**
+ * Exception that signals an error in cache management.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CacheException extends Exception {
+
+	/**
+	 * Creates a new cache exception.
+	 */
+	public CacheException() {
+		super();
+	}
+
+	/**
+	 * Creates a new cache exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public CacheException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Creates a new cache exception.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public CacheException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * Creates a new cache exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public CacheException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/CacheItem.java b/alien/src/net/pterodactylus/util/cache/CacheItem.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/CacheItem.java
@@ -0,0 +1,42 @@
+/*
+ * utils - CacheItem.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+/**
+ * Wrapper interface for cached items.
+ *
+ * @param <V>
+ *            The type of the value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface CacheItem<V> {
+
+	/**
+	 * Returns the wrapped item.
+	 *
+	 * @return The wrapped item
+	 */
+	public V getItem();
+
+	/**
+	 * Notifies the item that it is removed from the cache and can free any
+	 * resources it uses.
+	 */
+	public void remove();
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/DefaultCacheItem.java b/alien/src/net/pterodactylus/util/cache/DefaultCacheItem.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/DefaultCacheItem.java
@@ -0,0 +1,59 @@
+/*
+ * utils - DefaultCacheItem.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+/**
+ * Default implementation of a {@link CacheItem} that simply stores a value and
+ * does nothing when {@link CacheItem#remove()} is called.
+ *
+ * @param <V>
+ *            The type of the item to store
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultCacheItem<V> implements CacheItem<V> {
+
+	/** The item to store. */
+	private final V item;
+
+	/**
+	 * Creates a new cache item.
+	 *
+	 * @param item
+	 *            The item to store
+	 */
+	public DefaultCacheItem(V item) {
+		this.item = item;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public V getItem() {
+		return item;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void remove() {
+		/* does nothing. */
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/MemoryCache.java b/alien/src/net/pterodactylus/util/cache/MemoryCache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/MemoryCache.java
@@ -0,0 +1,168 @@
+/*
+ * utils - MemoryCache.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Memory-based {@link Cache} implementation.
+ *
+ * @param <K>
+ *            The type of the key
+ * @param <V>
+ *            The type of the value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MemoryCache<K, V> extends AbstractCache<K, V> {
+
+	/** The logger. */
+	private static Logger logger = Logging.getLogger(MemoryCache.class.getName());
+
+	/** The number of values to cache. */
+	private volatile int cacheSize;
+
+	/** The cache for the values. */
+	private final Map<K, CacheItem<V>> cachedValues = new LinkedHashMap<K, CacheItem<V>>() {
+
+		/**
+		 * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		protected boolean removeEldestEntry(Map.Entry<K, CacheItem<V>> eldest) {
+			if (super.size() > cacheSize) {
+				eldest.getValue().remove();
+				return true;
+			}
+			return false;
+		}
+	};
+
+	/** The lock for cache accesses. */
+	private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
+
+	/**
+	 * Creates a new memory-based cache.
+	 *
+	 * @param valueRetriever
+	 *            The value retriever
+	 */
+	public MemoryCache(ValueRetriever<K, V> valueRetriever) {
+		this(valueRetriever, 50);
+	}
+
+	/**
+	 * Creates a new memory-based cache.
+	 *
+	 * @param valueRetriever
+	 *            The value retriever
+	 * @param cacheSize
+	 *            The number of values to cache
+	 */
+	public MemoryCache(ValueRetriever<K, V> valueRetriever, int cacheSize) {
+		super(valueRetriever);
+		this.cacheSize = cacheSize;
+	}
+
+	/**
+	 * Sets the logger to use.
+	 *
+	 * @param logger
+	 *            The logger to use
+	 */
+	public static void setLogger(Logger logger) {
+		MemoryCache.logger = logger;
+	}
+
+	/**
+	 * @see net.pterodactylus.util.cache.Cache#clear()
+	 */
+	@Override
+	public void clear() {
+		cacheLock.writeLock().lock();
+		try {
+			cachedValues.clear();
+		} finally {
+			cacheLock.writeLock().unlock();
+		}
+	}
+
+	/**
+	 * @see net.pterodactylus.util.cache.Cache#contains(java.lang.Object)
+	 */
+	@Override
+	public boolean contains(K key) {
+		cacheLock.readLock().lock();
+		try {
+			return cachedValues.containsKey(key);
+		} finally {
+			cacheLock.readLock().unlock();
+		}
+	}
+
+	/**
+	 * @see net.pterodactylus.util.cache.Cache#get(java.lang.Object)
+	 */
+	@Override
+	public V get(K key) throws CacheException {
+		cacheLock.readLock().lock();
+		try {
+			if (cachedValues.containsKey(key)) {
+				logger.log(Level.FINE, "Value for Key “%1$s” is in cache.", key);
+				return cachedValues.get(key).getItem();
+			}
+			logger.log(Level.INFO, "Retrieving Value for Key “%1$s”...", key);
+			CacheItem<V> value = retrieveValue(key);
+			if (value != null) {
+				cacheLock.readLock().unlock();
+				cacheLock.writeLock().lock();
+				try {
+					cachedValues.put(key, value);
+				} finally {
+					cacheLock.readLock().lock();
+					cacheLock.writeLock().unlock();
+				}
+			}
+			return (value != null) ? value.getItem() : null;
+		} finally {
+			cacheLock.readLock().unlock();
+			logger.log(Level.FINE, "Retrieved Value for Key “%1$s”.", key);
+		}
+	}
+
+	/**
+	 * @see net.pterodactylus.util.cache.Cache#size()
+	 */
+	@Override
+	public int size() {
+		cacheLock.readLock().lock();
+		try {
+			return cachedValues.size();
+		} finally {
+			cacheLock.readLock().unlock();
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cache/ValueRetriever.java b/alien/src/net/pterodactylus/util/cache/ValueRetriever.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cache/ValueRetriever.java
@@ -0,0 +1,42 @@
+/*
+ * utils - ValueRetriever.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cache;
+
+/**
+ * Interface for objects that can fill a {@link Cache} from arbitrary sources.
+ *
+ * @param <K>
+ *            The type of the key
+ * @param <V>
+ *            The type of the value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ValueRetriever<K, V> {
+
+	/**
+	 * Retrieves the value for the given key.
+	 *
+	 * @param key
+	 *            The key to retrieve the value for
+	 * @return The value of the key, or {@code null} if there is no value
+	 * @throws CacheException
+	 *             if an error occurs retrieving the value
+	 */
+	public CacheItem<V> retrieve(K key) throws CacheException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/cmdline/CommandLine.java b/alien/src/net/pterodactylus/util/cmdline/CommandLine.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cmdline/CommandLine.java
@@ -0,0 +1,222 @@
+/*
+ * utils - CommandLine.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cmdline;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * Command-line parser.
+ * <p>
+ * This parser parses a {@link String} array containing the command-line
+ * parameters into options as defined by
+ * {@link #CommandLine(String[], Option...)}. Include code like the following in
+ * your main startup method:
+ *
+ * <pre>
+ * List<Option> options = new ArrayList<Option>();
+ * options.add(new Option('h', "help"));
+ * options.add(new Option('C', "config-file", true));
+ * CommandLine commandLine = new CommandLine(arguments, options);
+ * </pre>
+ *
+ * After the command-line has been parsed successfully, querying it is quite
+ * simple:
+ *
+ * <pre>
+ * if (commandLine.getOption("h").isPresent()) {
+ * 	showHelp();
+ * 	return;
+ * }
+ * if (commandLine.getOption("C").isPresent()) {
+ * 	String configFile = commandLine.getOption("C").getValue();
+ * }
+ * </pre>
+ *
+ * Additional arguments on the command line can be queried via the
+ * {@link #getArguments()} method. A line like
+ * <code>program -C config.txt file1.txt file2.txt</code> with the constructor
+ * from above and the code below would result in the output below the code:
+ *
+ * <pre>
+ * for (String argument : commandLine.getArguments()) {
+ * 	System.out.println(argument);
+ * }
+ * </pre>
+ *
+ * Output:
+ *
+ * <pre>
+ * file1.txt
+ * file2.txt
+ * </pre>
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CommandLine {
+
+	/** Mapping from short names to options. */
+	private final Map<Character, Option> shortNameOptions = new HashMap<Character, Option>();
+
+	/** Mapping from long names to options. */
+	private final Map<String, Option> longNameOptions = new HashMap<String, Option>();
+
+	/** The remaining arguments. */
+	private List<String> arguments = new ArrayList<String>();
+
+	/**
+	 * Creates a new command line and parses the given command-line arguments
+	 * according to the given options.
+	 *
+	 * @param commandLineArguments
+	 *            The command-line arguments
+	 * @param options
+	 *            The options
+	 * @throws CommandLineException
+	 *             if a command-line argument could not be parsed
+	 */
+	public CommandLine(String[] commandLineArguments, Collection<Option> options) throws CommandLineException {
+		this(commandLineArguments, options.toArray(new Option[options.size()]));
+	}
+
+	/**
+	 * Creates a new command line and parses the given command-line arguments
+	 * according to the given options.
+	 *
+	 * @param commandLineArguments
+	 *            The command-line arguments
+	 * @param options
+	 *            The options
+	 * @throws CommandLineException
+	 *             if a command-line argument could not be parsed
+	 */
+	public CommandLine(String[] commandLineArguments, Option... options) throws CommandLineException {
+		Validation.begin().isNotNull("commandLineArguments", commandLineArguments).check();
+		for (Option option : options) {
+			/* TODO - sanity checks */
+			if (option.getShortName() != 0) {
+				shortNameOptions.put(option.getShortName(), option);
+			}
+			if (option.getLongName() != null) {
+				longNameOptions.put(option.getLongName(), option);
+			}
+		}
+		int argumentCount = commandLineArguments.length;
+		boolean argumentsOnly = false;
+		List<Option> optionsNeedingParameters = new ArrayList<Option>();
+		for (int argumentIndex = 0; argumentIndex < argumentCount; argumentIndex++) {
+			String argument = commandLineArguments[argumentIndex];
+			if (!optionsNeedingParameters.isEmpty()) {
+				Option option = optionsNeedingParameters.remove(0);
+				option.setValue(argument);
+				continue;
+			}
+			if (argumentsOnly) {
+				arguments.add(argument);
+				continue;
+			}
+			if ("--".equals(argument)) {
+				argumentsOnly = true;
+				continue;
+			}
+			if (argument.startsWith("--")) {
+				String longName = argument.substring(2);
+				Option option = longNameOptions.get(longName);
+				if (option == null) {
+					throw new CommandLineException("unknown long name: " + longName);
+				}
+				if (option.needsParameter()) {
+					int equals = longName.indexOf('=');
+					if (equals == -1) {
+						optionsNeedingParameters.add(option);
+					} else {
+						option.setValue(longName.substring(equals + 1));
+					}
+				}
+				option.incrementCounter();
+				continue;
+			}
+			if (argument.startsWith("-")) {
+				String optionChars = argument.substring(1);
+				for (char optionChar : optionChars.toCharArray()) {
+					Option option = shortNameOptions.get(optionChar);
+					if (option == null) {
+						throw new CommandLineException("unknown short name: " + optionChar);
+					}
+					if (option.needsParameter()) {
+						optionsNeedingParameters.add(option);
+					}
+					option.incrementCounter();
+				}
+				continue;
+			}
+			arguments.add(argument);
+		}
+		if (!optionsNeedingParameters.isEmpty()) {
+			throw new CommandLineException("missing value for option " + optionsNeedingParameters.get(0));
+		}
+	}
+
+	/**
+	 * Returns the option with the given name. If there is no option with the
+	 * given short name, <code>null</code> is returned.
+	 *
+	 * @param name
+	 *            The short name of the option
+	 * @return The option, or <code>null</code> if no option could be found
+	 */
+	public Option getOption(char name) {
+		return shortNameOptions.get(name);
+	}
+
+	/**
+	 * Returns the option with the given name. If the name is longer than one
+	 * character and matches an option’s long name, that option is returned. If
+	 * the name is exactly one character long and matches an option’s short
+	 * name, that options is returned. Otherwise <code>null</code> is returned.
+	 *
+	 * @param name
+	 *            The long or short name of the option
+	 * @return The option, or <code>null</code> if no option could be found
+	 */
+	public Option getOption(String name) {
+		Validation.begin().isNotNull("name", name).check();
+		if ((name.length() > 1) && longNameOptions.containsKey(name)) {
+			return longNameOptions.get(name);
+		}
+		if ((name.length() == 1) && (shortNameOptions.containsKey(name.charAt(0)))) {
+			return shortNameOptions.get(name.charAt(0));
+		}
+		return null;
+	}
+
+	/**
+	 * Returns all remaining arguments from the original command-line arguments.
+	 *
+	 * @return The remaining arguments
+	 */
+	public String[] getArguments() {
+		return arguments.toArray(new String[arguments.size()]);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cmdline/CommandLineException.java b/alien/src/net/pterodactylus/util/cmdline/CommandLineException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cmdline/CommandLineException.java
@@ -0,0 +1,66 @@
+/*
+ * utils - CommandLineException.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cmdline;
+
+/**
+ * Exception that signals an error in command-line argument parsing.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CommandLineException extends Exception {
+
+	/**
+	 * Creates a new command-line exception.
+	 */
+	public CommandLineException() {
+		super();
+	}
+
+	/**
+	 * Creates a new command-line exception with the given message.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public CommandLineException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Creates a new command-line exception with the given cause.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public CommandLineException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * Creates a new command-line exception with the given message and cause.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public CommandLineException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cmdline/Option.java b/alien/src/net/pterodactylus/util/cmdline/Option.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cmdline/Option.java
@@ -0,0 +1,153 @@
+/*
+ * utils - Option.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.cmdline;
+
+/**
+ * Container for {@link CommandLine} options and their values.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Option {
+
+	/** The short name. */
+	private final char shortName;
+
+	/** The long name. */
+	private final String longName;
+
+	/** Whether the option needs a parameter. */
+	private final boolean needsParameter;
+
+	/** The value of the parameter. */
+	private String value;
+
+	/** The option counter. */
+	private int counter;
+
+	/**
+	 * Creates a new option that does not require a parameter.
+	 *
+	 * @param shortName
+	 *            The short name of the option (may be <code>\u0000</code>)
+	 * @param longName
+	 *            The long name of the option (may be <code>null</code>)
+	 */
+	public Option(char shortName, String longName) {
+		this(shortName, longName, false);
+	}
+
+	/**
+	 * Creates a new option.
+	 *
+	 * @param shortName
+	 *            The short name of the option (may be <code>\u0000</code>)
+	 * @param longName
+	 *            The long name of the option (may be <code>null</code>)
+	 * @param needsParameter
+	 *            <code>true</code> if the option requires a parameter,
+	 *            <code>false</code> otherwise
+	 */
+	public Option(char shortName, String longName, boolean needsParameter) {
+		this.shortName = shortName;
+		this.longName = longName;
+		this.needsParameter = needsParameter;
+	}
+
+	/**
+	 * Returns the short name of the option.
+	 *
+	 * @return The short name of the option
+	 */
+	public char getShortName() {
+		return shortName;
+	}
+
+	/**
+	 * Returns the long name of the option.
+	 *
+	 * @return The long name of the option
+	 */
+	public String getLongName() {
+		return longName;
+	}
+
+	/**
+	 * Returns whether the option needs a parameter.
+	 *
+	 * @return <code>true</code> if the option requires a parameter,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean needsParameter() {
+		return needsParameter;
+	}
+
+	/**
+	 * Returns the value of the option’s parameter.
+	 *
+	 * @return The value of the parameter, or <code>null</code> if no parameter
+	 *         was set
+	 */
+	public String getValue() {
+		return value;
+	}
+
+	/**
+	 * Sets the value of the option’s parameter.
+	 *
+	 * @param value
+	 *            The value of the parameter
+	 */
+	void setValue(String value) {
+		this.value = value;
+	}
+
+	/**
+	 * Returns the counter of the option.
+	 *
+	 * @return The number of times the option was given on the command line
+	 */
+	public int getCounter() {
+		return counter;
+	}
+
+	/**
+	 * Returns whether this option was present in the command line.
+	 *
+	 * @return <code>true</code> if the option was present, <code>false</code>
+	 *         otherwise
+	 */
+	public boolean isPresent() {
+		return counter > 0;
+	}
+
+	/**
+	 * Increments the option counter.
+	 */
+	void incrementCounter() {
+		counter++;
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return ((shortName != 0) ? ("-" + shortName) : "") + ((longName != null) ? (((shortName != 0) ? ("|") : ("")) + "--" + longName) : ("")) + (needsParameter ? ("=") : (""));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/cmdline/package-info.java b/alien/src/net/pterodactylus/util/cmdline/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/cmdline/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Command-line argument parser.
+ * <p>
+ * See {@link net.pterodactylus.util.cmdline.CommandLine} for more information.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+package net.pterodactylus.util.cmdline;
+
diff --git a/alien/src/net/pterodactylus/util/collection/ArrayMap.java b/alien/src/net/pterodactylus/util/collection/ArrayMap.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/ArrayMap.java
@@ -0,0 +1,288 @@
+/*
+ * utils - ArrayMap.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An {@code ArrayMap} is a {@link Map} implementation that is backed by arrays.
+ * It does not rely on {@link Object#hashCode() object hashes} but solely uses
+ * {@link Object#equals(Object)} to compare objects.
+ *
+ * @param <K>
+ *            The type of the keys
+ * @param <V>
+ *            The type of the values
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ArrayMap<K, V> implements Map<K, V> {
+
+	/** The keys. */
+	private Object[] keys;
+
+	/** The values. */
+	private Object[] values;
+
+	/** The current size. */
+	private int size = 0;
+
+	/**
+	 * Creates a new array map with a default size of 10.
+	 */
+	public ArrayMap() {
+		this(10);
+	}
+
+	/**
+	 * Creates a new array map with the given default size.
+	 *
+	 * @param initialSize
+	 *            The initial size of the array map
+	 */
+	public ArrayMap(int initialSize) {
+		keys = new Object[initialSize];
+		values = new Object[initialSize];
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int size() {
+		return size;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean isEmpty() {
+		return size == 0;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean containsKey(Object key) {
+		return locateKey(key) != -1;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean containsValue(Object value) {
+		return locateValue(value) != -1;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public V get(Object key) {
+		int index = locateKey(key);
+		if (index == -1) {
+			return null;
+		}
+		return (V) values[index];
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public V put(K key, V value) {
+		int index = locateKey(key);
+		if (index == -1) {
+			checkResize();
+			keys[size] = key;
+			values[size] = value;
+			++size;
+			return null;
+		}
+		Object oldValue = values[index];
+		values[index] = value;
+		return (V) oldValue;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public V remove(Object key) {
+		int index = locateKey(key);
+		if (index == -1) {
+			return null;
+		}
+		Object value = values[index];
+		if (index < (size - 1)) {
+			keys[index] = keys[size - 1];
+			values[index] = values[size - 1];
+		}
+		--size;
+		return (V) value;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void putAll(Map<? extends K, ? extends V> map) {
+		for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
+			put(entry.getKey(), entry.getValue());
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void clear() {
+		size = 0;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public Set<K> keySet() {
+		if (size < keys.length) {
+			Object[] temp = new Object[size];
+			System.arraycopy(keys, 0, temp, 0, size);
+			return new HashSet<K>(Arrays.asList((K[]) temp));
+		}
+		return new HashSet<K>(Arrays.asList((K[]) keys));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public Collection<V> values() {
+		if (size < keys.length) {
+			Object[] temp = new Object[size];
+			System.arraycopy(values, 0, temp, 0, size);
+			return Arrays.asList((V[]) temp);
+		}
+		return Arrays.asList((V[]) values);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@SuppressWarnings("unchecked")
+	public Set<Entry<K, V>> entrySet() {
+		Set<Entry<K, V>> entries = new HashSet<Entry<K, V>>();
+		for (int index = 0; index < size; ++index) {
+			final K key = (K) keys[index];
+			final V value = (V) values[index];
+			entries.add(new Entry<K, V>() {
+
+				@Override
+				public K getKey() {
+					return key;
+				}
+
+				@Override
+				public V getValue() {
+					return value;
+				}
+
+				@Override
+				public V setValue(V value) {
+					/* nothing. */
+					return value;
+				}
+			});
+		}
+		return entries;
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Locates the given key in the {@link #keys} array.
+	 *
+	 * @param key
+	 *            The key to locate
+	 * @return The index of the key, or {@code -1} if the key could not be found
+	 */
+	private int locateKey(Object key) {
+		return locateObject(keys, key);
+	}
+
+	/**
+	 * Locates the index of the given value.
+	 *
+	 * @param value
+	 *            The value to locate
+	 * @return The index of the value, or {@code -1} if the value could not be
+	 *         found
+	 */
+	private int locateValue(Object value) {
+		return locateObject(values, value);
+	}
+
+	/**
+	 * Locates an object in the given array of objects.
+	 *
+	 * @param data
+	 *            The array of objects to search
+	 * @param value
+	 *            The object to search
+	 * @return The index of the object, or {@code -1} if the object could not be
+	 *         found
+	 */
+	private int locateObject(Object[] data, Object value) {
+		for (int index = 0; index < size; ++index) {
+			if ((value == null) && (data[index] == null) || ((value != null) && value.equals(data[index]))) {
+				return index;
+			}
+		}
+		return -1;
+	}
+
+	/**
+	 * Checks if the map needs to be resized and resizes it if the current
+	 * {@link #size} equals the current capacity of the {@link #keys} array.
+	 */
+	private void checkResize() {
+		if (size == (keys.length)) {
+			Object[] newKeys = new Object[keys.length * 2];
+			Object[] newValues = new Object[keys.length * 2];
+			System.arraycopy(keys, 0, newKeys, 0, size);
+			System.arraycopy(values, 0, newValues, 0, size);
+			keys = newKeys;
+			values = newValues;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/ComparablePair.java b/alien/src/net/pterodactylus/util/collection/ComparablePair.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/ComparablePair.java
@@ -0,0 +1,55 @@
+/*
+ * utils - ComparablePair.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+/**
+ * Container for two {@link Comparable} objects that are tied together.
+ * Comparisons are done by first comparing the left object and only comparing
+ * the right objects if the left objects are considered the same (i.e.
+ * {@link Comparable#compareTo(Object)} returns {@code 0}).
+ *
+ * @param <S>
+ *            The type of the left value
+ * @param <T>
+ *            The type of the right value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ComparablePair<S extends Comparable<S>, T extends Comparable<T>> extends Pair<S, T> implements Comparable<ComparablePair<S, T>> {
+
+	/**
+	 * Creates a new pair consisting of the two values.
+	 *
+	 * @param left
+	 *            The left value
+	 * @param right
+	 *            The right value
+	 */
+	public ComparablePair(S left, T right) {
+		super(left, right);
+	}
+
+	/**
+	 * @see java.lang.Comparable#compareTo(Object)
+	 */
+	@Override
+	public int compareTo(ComparablePair<S, T> pair) {
+		int leftDifference = left.compareTo(pair.left);
+		return (leftDifference == 0) ? right.compareTo(pair.right) : leftDifference;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/MapWriter.java b/alien/src/net/pterodactylus/util/collection/MapWriter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/MapWriter.java
@@ -0,0 +1,183 @@
+/*
+ * utils - MapWriter.java - Copyright © 2008-2010 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.util.collection;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.number.Hex;
+
+/**
+ * Helper class that emulates the function of
+ * {@link Properties#store(java.io.OutputStream, String)} and
+ * {@link Properties#load(java.io.InputStream)} but does not suffer from the
+ * drawbacks of {@link Properties} (namely the fact that a
+ * <code>Properties</code> can not contain <code>null</code> values).
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MapWriter {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(MapWriter.class.getName());
+
+	/**
+	 * Writes the given map to the given writer.
+	 *
+	 * @param writer
+	 *            The writer to write the map’s content to
+	 * @param map
+	 *            The map to write
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void write(Writer writer, Map<String, String> map) throws IOException {
+		for (Entry<String, String> entry : map.entrySet()) {
+			if (entry.getValue() != null) {
+				writer.write(encode(entry.getKey()));
+				writer.write('=');
+				writer.write(encode(entry.getValue()));
+				writer.write('\n');
+			}
+		}
+	}
+
+	/**
+	 * Reads a map from the given reader. Lines are read from the given reader
+	 * until a line is encountered that does not contain a colon (“:”) or equals
+	 * sign (“=”).
+	 *
+	 * @param reader
+	 *            The reader to read from
+	 * @return The map that was read
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static Map<String, String> read(Reader reader) throws IOException {
+		logger.log(Level.FINE, "MapWriter.read(reader=" + reader + ")");
+		Map<String, String> map = new HashMap<String, String>();
+		BufferedReader bufferedReader = new BufferedReader(reader);
+		try {
+			String line;
+			while ((line = bufferedReader.readLine()) != null) {
+				logger.log(Level.FINEST, "Read line: “" + line + "”");
+				if (line.startsWith("#") || (line.length() == 0)) {
+					continue;
+				}
+				if (line.indexOf('=') == -1) {
+					break;
+				}
+				int split = line.indexOf('=');
+				String key = decode(line.substring(0, split));
+				String value = decode(line.substring(split + 1));
+				map.put(key, value);
+			}
+		} finally {
+			Closer.close(bufferedReader);
+		}
+		return map;
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Encodes the given String by replacing certain “unsafe” characters. CR
+	 * (0x0d) is replaced by “\r”, LF (0x0a) is replaced by “\n”, the backslash
+	 * (‘\’) will be replaced by “\\”, other characters that are either smaller
+	 * than 0x20 or larger than 0x7f or that are ‘:’ or ‘=’ will be replaced by
+	 * their unicode notation (“\u0000” for NUL, 0x00). All other values are
+	 * copied verbatim.
+	 *
+	 * @param value
+	 *            The value to encode
+	 * @return The encoded value
+	 */
+	static String encode(String value) {
+		StringBuilder encodedString = new StringBuilder();
+		for (char character : value.toCharArray()) {
+			if (character == 0x0d) {
+				encodedString.append("\\r");
+			} else if (character == 0x0a) {
+				encodedString.append("\\n");
+			} else if (character == '\\') {
+				encodedString.append("\\\\");
+			} else if ((character < 0x20) || (character == '=') || (character > 0x7f)) {
+				encodedString.append("\\u").append(Hex.toHex(character, 4));
+			} else {
+				encodedString.append(character);
+			}
+		}
+		return encodedString.toString();
+	}
+
+	/**
+	 * Decodes the given value by reversing the changes made by
+	 * {@link #encode(String)}.
+	 *
+	 * @param value
+	 *            The value to decode
+	 * @return The decoded value
+	 */
+	static String decode(String value) {
+		StringBuilder decodedString = new StringBuilder();
+		boolean backslash = false;
+		int hexDigit = 0;
+		char[] hexDigits = new char[4];
+		for (char character : value.toCharArray()) {
+			if (hexDigit > 0) {
+				hexDigits[hexDigit - 1] = character;
+				hexDigit++;
+				if (hexDigit > 4) {
+					decodedString.append((char) Integer.parseInt(new String(hexDigits), 16));
+					hexDigit = 0;
+				}
+			} else if (backslash) {
+				if (character == '\\') {
+					decodedString.append('\\');
+				} else if (character == 'r') {
+					decodedString.append('\r');
+				} else if (character == 'n') {
+					decodedString.append('\n');
+				} else if (character == 'u') {
+					hexDigit = 1;
+				}
+				backslash = false;
+			} else if (character == '\\') {
+				backslash = true;
+				continue;
+			} else {
+				decodedString.append(character);
+			}
+		}
+		return decodedString.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/ObjectMethods.java b/alien/src/net/pterodactylus/util/collection/ObjectMethods.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/ObjectMethods.java
@@ -0,0 +1,52 @@
+/*
+ * utils - ObjectMethods.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+/**
+ * Contains helper methods for implementating {@link Object} methods such as
+ * {@link Object#equals(Object)} and {@link Object#hashCode()} for objects that
+ * may be {@code null}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ObjectMethods {
+
+	/**
+	 * Checks whether the two objects are equal.
+	 *
+	 * @param first
+	 *            The first object
+	 * @param second
+	 *            The second object
+	 * @return {@code true} if the objects are equal (according to
+	 *         {@link Object#equals(Object)}) or both {@code null}
+	 */
+	public static boolean equal(Object first, Object second) {
+		return (first == null) ? (second == null) : first.equals(second);
+	}
+
+	/**
+	 * Calculates the hash code for the given object.
+	 * @param object The object to get the hash code for
+	 * @return The hash code of the object, or
+	 */
+	public static int hashCode(Object object) {
+		return (object == null) ? 0xCF018C37 : object.hashCode();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/Pagination.java b/alien/src/net/pterodactylus/util/collection/Pagination.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/Pagination.java
@@ -0,0 +1,216 @@
+/*
+ * utils - Pagination.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+import java.util.List;
+
+/**
+ * Helper class for lists that need pagination. Setting the page or the page
+ * size will automatically recalculate all other parameters, and the next call
+ * to {@link #getItems()} retrieves all items on the current page.
+ *
+ * @param <T>
+ *            The type of the list elements
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Pagination<T> {
+
+	/** The list to paginate. */
+	private final List<T> list;
+
+	/** The page size. */
+	private int pageSize;
+
+	/** The current page, 0-based. */
+	private int page;
+
+	/** The total number of pages. */
+	private int pageCount;
+
+	/**
+	 * Paginates the given list.
+	 *
+	 * @param list
+	 *            The list to paginate
+	 * @param pageSize
+	 *            The page size
+	 */
+	public Pagination(List<T> list, int pageSize) {
+		this.list = list;
+		this.pageSize = pageSize;
+		pageCount = (list.size() - 1) / pageSize + 1;
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Returns the current page, 0-based.
+	 *
+	 * @return The current page, 0-based
+	 */
+	public int getPage() {
+		return page;
+	}
+
+	/**
+	 * Returns the current page, 1-based.
+	 *
+	 * @return The current page, 1-based
+	 */
+	public int getPageNumber() {
+		return page + 1;
+	}
+
+	/**
+	 * Sets the new page. If the new page is out of range it is silently
+	 * corrected.
+	 *
+	 * @param page
+	 *            The new page number
+	 * @return This pagination helper (for method chaining)
+	 */
+	public Pagination<T> setPage(int page) {
+		if (page < 0) {
+			this.page = 0;
+		} else if (page >= pageCount) {
+			this.page = pageCount - 1;
+		} else {
+			this.page = page;
+		}
+		return this;
+	}
+
+	/**
+	 * Returns the total number of pages.
+	 *
+	 * @return The total number of pages
+	 */
+	public int getPageCount() {
+		return pageCount;
+	}
+
+	/**
+	 * Returns the number of items per page.
+	 *
+	 * @return The number of items per page
+	 */
+	public int getPageSize() {
+		return pageSize;
+	}
+
+	/**
+	 * Sets the page size. The page is adjusted so that the first item on the
+	 * old page is still contained in the new page. A page size of less than 1
+	 * is silently corrected to 1.
+	 *
+	 * @param pageSize
+	 *            The new page size
+	 * @return This pagination helper (for method chaining)
+	 */
+	public Pagination<T> setPageSize(int pageSize) {
+		int newPageSize = (pageSize < 1) ? 1 : pageSize;
+		int index = page * this.pageSize;
+		this.pageSize = newPageSize;
+		pageCount = (list.size() - 1) / newPageSize + 1;
+		page = index / newPageSize;
+		return this;
+	}
+
+	/**
+	 * Returns the number of items on the current page. For all but the last
+	 * page this will equal the page size.
+	 *
+	 * @return The number of items on the current page
+	 */
+	public int getItemCount() {
+		return Math.min(pageSize, list.size() - page * pageSize);
+	}
+
+	/**
+	 * Returns the items on the current page.
+	 *
+	 * @return The items on the current page
+	 */
+	public List<T> getItems() {
+		return list.subList(page * pageSize, page * pageSize + getItemCount());
+	}
+
+	/**
+	 * Returns whether the current page is the first page
+	 *
+	 * @return {@code true} if the current page is the first page, {@code false}
+	 *         otherwise
+	 */
+	public boolean isFirst() {
+		return page == 0;
+	}
+
+	/**
+	 * Returns whether the current page is the last page.
+	 *
+	 * @return {@code true} if the current page is the last page, {@code false}
+	 *         otherwise
+	 */
+	public boolean isLast() {
+		return page == (pageCount - 1);
+	}
+
+	/**
+	 * Returns whether pagination is actually necessary, i.e. if the number of
+	 * pages is greater than 1.
+	 *
+	 * @return {@code true} if there are more than one page in this pagination,
+	 *         {@code false} otherwise
+	 */
+	public boolean isNecessary() {
+		return pageCount > 1;
+	}
+
+	/**
+	 * Returns the index of the previous page. {@link #isFirst()} should be
+	 * called first to determine whether there is a page before the current
+	 * page.
+	 *
+	 * @return The index of the previous page
+	 */
+	public int getPreviousPage() {
+		return page - 1;
+	}
+
+	/**
+	 * Returns the index of the next page. {@link #isLast()} should be called
+	 * first to determine whether there is a page after the current page.
+	 *
+	 * @return The index of the next page
+	 */
+	public int getNextPage() {
+		return page + 1;
+	}
+
+	/**
+	 * Returns the index of the last page.
+	 *
+	 * @return The index of the last page
+	 */
+	public int getLastPage() {
+		return pageCount - 1;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/Pair.java b/alien/src/net/pterodactylus/util/collection/Pair.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/Pair.java
@@ -0,0 +1,101 @@
+/*
+ * utils - Pair.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+/**
+ * Container for two objects that are tied together.
+ *
+ * @param <S>
+ *            The type of the left value
+ * @param <T>
+ *            The type of the right value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Pair<S, T> {
+
+	/** The left value. */
+	protected final S left;
+
+	/** The right value. */
+	protected final T right;
+
+	/**
+	 * Creates a new pair consisting of the two values. None of the values may
+	 * be {@code null}.
+	 *
+	 * @param left
+	 *            The left value
+	 * @param right
+	 *            The right value
+	 */
+	public Pair(S left, T right) {
+		if ((left == null) || (right == null)) {
+			throw new NullPointerException("null is not allowed in a pair");
+		}
+		this.left = left;
+		this.right = right;
+	}
+
+	/**
+	 * Returns the left value.
+	 *
+	 * @return The left value
+	 */
+	public S getLeft() {
+		return left;
+	}
+
+	/**
+	 * Returns the right value.
+	 *
+	 * @return The right value
+	 */
+	public T getRight() {
+		return right;
+	}
+
+	/**
+	 * @see java.lang.Object#equals(java.lang.Object)
+	 */
+	@Override
+	public boolean equals(Object obj) {
+		if ((obj == null) || (obj.getClass() != getClass())) {
+			return false;
+		}
+		Pair<?, ?> pair = (Pair<?, ?>) obj;
+		return left.equals(pair.left) && right.equals(pair.right);
+	}
+
+	/**
+	 * @see java.lang.Object#hashCode()
+	 */
+	@Override
+	public int hashCode() {
+		int leftHashCode = left.hashCode();
+		return ((leftHashCode << 16) | (leftHashCode >>> 16)) ^ ~right.hashCode();
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "<" + left + "," + right + ">";
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/ReverseComparator.java b/alien/src/net/pterodactylus/util/collection/ReverseComparator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/ReverseComparator.java
@@ -0,0 +1,40 @@
+/*
+ * utils - ReverseComparator.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+import java.util.Comparator;
+
+/**
+ * This {@link Comparator} implementation compares to {@link Comparable}s but
+ * reverses the result of the comparison.
+ *
+ * @param <T>
+ *            The type to compare
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReverseComparator<T extends Comparable<T>> implements Comparator<T> {
+
+	/**
+	 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
+	 */
+	@Override
+	public int compare(T o1, T o2) {
+		return -o1.compareTo(o2);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/collection/Triplet.java b/alien/src/net/pterodactylus/util/collection/Triplet.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/collection/Triplet.java
@@ -0,0 +1,115 @@
+/*
+ * utils - Triplet.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.collection;
+
+import net.pterodactylus.util.number.Bits;
+
+/**
+ * 3-tuple.
+ *
+ * @param <T>
+ *            The type of the first element
+ * @param <U>
+ *            The type of the second element
+ * @param <V>
+ *            The type of the third element
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Triplet<T, U, V> {
+
+	/** The first element. */
+	private final T first;
+
+	/** The second element. */
+	private final U second;
+
+	/** The third element. */
+	private final V third;
+
+	/**
+	 * Creates a new triplet.
+	 *
+	 * @param first
+	 *            The first element
+	 * @param second
+	 *            The second element
+	 * @param third
+	 *            The third element
+	 */
+	public Triplet(T first, U second, V third) {
+		this.first = first;
+		this.second = second;
+		this.third = third;
+	}
+
+	/**
+	 * Returns the first element.
+	 *
+	 * @return The first element
+	 */
+	public T getFirst() {
+		return first;
+	}
+
+	/**
+	 * Returns the second element.
+	 *
+	 * @return The second element
+	 */
+	public U getSecond() {
+		return second;
+	}
+
+	/**
+	 * Returns the third element.
+	 *
+	 * @return The third element
+	 */
+	public V getThird() {
+		return third;
+	}
+
+	/**
+	 * @see java.lang.Object#equals(java.lang.Object)
+	 */
+	@Override
+	public boolean equals(Object obj) {
+		if (!(obj instanceof Triplet<?, ?, ?>)) {
+			return false;
+		}
+		Triplet<?, ?, ?> triplet = (Triplet<?, ?, ?>) obj;
+		return ObjectMethods.equal(first, triplet.first) && ObjectMethods.equal(second, triplet.second) && ObjectMethods.equal(third, triplet.third);
+	}
+
+	/**
+	 * @see java.lang.Object#hashCode()
+	 */
+	@Override
+	public int hashCode() {
+		return Bits.rotateLeft(ObjectMethods.hashCode(first), 8) ^ Bits.rotateLeft(ObjectMethods.hashCode(second), 16) ^ Bits.rotateLeft(ObjectMethods.hashCode(third), 24);
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "<" + first + "," + second + "," + third + ">";
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/AbstractValue.java b/alien/src/net/pterodactylus/util/config/AbstractValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/AbstractValue.java
@@ -0,0 +1,49 @@
+/*
+ * utils - AbstractValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * An abstract implementation of a {@link Value}.
+ *
+ * @param <T>
+ *            The type of the wrapped value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractValue<T> implements Value<T> {
+
+	/** The configuration that created this value. */
+	protected final Configuration configuration;
+
+	/** The name of the attribute this is the value for. */
+	protected final String attribute;
+
+	/**
+	 * Creates a new value that reads its values from the given configuration
+	 * backend.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute
+	 */
+	public AbstractValue(Configuration configuration, String attribute) {
+		this.configuration = configuration;
+		this.attribute = attribute;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/AttributeNotFoundException.java b/alien/src/net/pterodactylus/util/config/AttributeNotFoundException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/AttributeNotFoundException.java
@@ -0,0 +1,58 @@
+/*
+ * utils - AttributeNotFoundException.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Exception that will be thrown when a non-existing attribute is requested.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class AttributeNotFoundException extends ConfigurationException {
+
+	/** The requested, non-existing attribute. */
+	private final String attribute;
+
+	/**
+	 * Constructs a new exception.
+	 */
+	public AttributeNotFoundException() {
+		super();
+		attribute = null;
+	}
+
+	/**
+	 * Constructs a new exception with the specified message.
+	 *
+	 * @param attribute
+	 *            The name of the attribute that could not be found
+	 */
+	public AttributeNotFoundException(String attribute) {
+		super("attribute not found: " + attribute);
+		this.attribute = attribute;
+	}
+
+	/**
+	 * Returns the requested, non-existing attribute's name.
+	 *
+	 * @return The name of the attribute that does not exist
+	 */
+	public String getAttribute() {
+		return attribute;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/BooleanValue.java b/alien/src/net/pterodactylus/util/config/BooleanValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/BooleanValue.java
@@ -0,0 +1,85 @@
+/*
+ * utils - BooleanValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * A wrapper around a boolean value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BooleanValue extends AbstractValue<Boolean> {
+
+	/**
+	 * Creates a new boolean value that updates its value to the given attribute
+	 * from the given configuration.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute name
+	 */
+	public BooleanValue(Configuration configuration, String attribute) {
+		super(configuration, attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public Boolean getValue() throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			return ((ExtendedConfigurationBackend) configuration.configurationBackend).getBooleanValue(attribute);
+		}
+		String value = configuration.configurationBackend.getValue(attribute);
+		return Boolean.valueOf(("true".equalsIgnoreCase(value)) || ("yes".equalsIgnoreCase(value)) || ("1".equalsIgnoreCase(value)) || ("on".equalsIgnoreCase(value)));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public Boolean getValue(Boolean defaultValue) {
+		try {
+			if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+				return ((ExtendedConfigurationBackend) configuration.configurationBackend).getBooleanValue(attribute);
+			}
+			String value = configuration.configurationBackend.getValue(attribute);
+			return Boolean.valueOf(("true".equalsIgnoreCase(value)) || ("yes".equalsIgnoreCase(value)) || ("1".equalsIgnoreCase(value)) || ("on".equalsIgnoreCase(value)));
+		} catch (ConfigurationException ce1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(Boolean newValue) throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			((ExtendedConfigurationBackend) configuration.configurationBackend).setBooleanValue(attribute, newValue);
+		}
+		configuration.configurationBackend.putValue(attribute, (newValue != null) ? String.valueOf(newValue) : null);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/CachingConfigurationBackend.java b/alien/src/net/pterodactylus/util/config/CachingConfigurationBackend.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/CachingConfigurationBackend.java
@@ -0,0 +1,89 @@
+/*
+ * utils - CachingConfigurationBackend.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Configuration backend proxy that caches the attribute values it retrieves.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CachingConfigurationBackend implements ConfigurationBackend {
+
+	/** The real configuration backend. */
+	private final ConfigurationBackend realConfigurationBackend;
+
+	/** The cache for the attribute values. */
+	private final Map<String, String> attributeCache = new HashMap<String, String>();
+
+	/**
+	 * Creates a new caching configuration backend that works as a proxy for the
+	 * specified configuration backend and caches all values it retrieves.
+	 *
+	 * @param realConfigurationBackend
+	 *            The configuration backend to proxy
+	 */
+	public CachingConfigurationBackend(ConfigurationBackend realConfigurationBackend) {
+		this.realConfigurationBackend = realConfigurationBackend;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#getValue(java.lang.String)
+	 */
+	@Override
+	public synchronized String getValue(String attribute) throws ConfigurationException {
+		if (attributeCache.containsKey(attribute)) {
+			return attributeCache.get(attribute);
+		}
+		String value = realConfigurationBackend.getValue(attribute);
+		attributeCache.put(attribute, value);
+		return value;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#putValue(java.lang.String,
+	 *      java.lang.String)
+	 */
+	@Override
+	public synchronized void putValue(String attribute, String value) throws ConfigurationException {
+		attributeCache.put(attribute, value);
+		realConfigurationBackend.putValue(attribute, value);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void save() throws ConfigurationException {
+		realConfigurationBackend.save();
+	}
+
+	/**
+	 * Clears the current cache, causing the all further lookups to be repeated.
+	 */
+	public synchronized void clear() {
+		attributeCache.clear();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/Configuration.java b/alien/src/net/pterodactylus/util/config/Configuration.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/Configuration.java
@@ -0,0 +1,151 @@
+/*
+ * utils - Configuration.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A configuration contains all necessary methods to read integral data types
+ * from a {@link ConfigurationBackend}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Configuration {
+
+	/** The backend backing this configuration. */
+	final ConfigurationBackend configurationBackend;
+
+	/** The cache for boolean values. */
+	private final Map<String, BooleanValue> booleanCache = new HashMap<String, BooleanValue>();
+
+	/** The cache for double values. */
+	private final Map<String, DoubleValue> doubleCache = new HashMap<String, DoubleValue>();
+
+	/** The cache for string values. */
+	private final Map<String, StringValue> stringCache = new HashMap<String, StringValue>();
+
+	/** The cache for integer values. */
+	private final Map<String, IntegerValue> integerCache = new HashMap<String, IntegerValue>();
+
+	/** The cache for long values. */
+	private final Map<String, LongValue> longCache = new HashMap<String, LongValue>();
+
+	/**
+	 * Creates a new configuration that operates on the given backend.
+	 *
+	 * @param configurationBackend
+	 *            The backend backing this configuration
+	 */
+	public Configuration(ConfigurationBackend configurationBackend) {
+		this.configurationBackend = configurationBackend;
+	}
+
+	/**
+	 * Returns the boolean value stored at the given attribute.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The boolean value stored at the given attribute
+	 */
+	public Value<Boolean> getBooleanValue(String attribute) {
+		BooleanValue booleanValue = booleanCache.get(attribute);
+		if (booleanValue == null) {
+			booleanValue = new BooleanValue(this, attribute);
+			booleanCache.put(attribute, booleanValue);
+		}
+		return booleanValue;
+	}
+
+	/**
+	 * Returns the double value stored at the given attribute.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The double value stored at the given attribute
+	 */
+	public Value<Double> getDoubleValue(String attribute) {
+		DoubleValue doubleValue = doubleCache.get(attribute);
+		if (doubleValue == null) {
+			doubleValue = new DoubleValue(this, attribute);
+			doubleCache.put(attribute, doubleValue);
+		}
+		return doubleValue;
+	}
+
+	/**
+	 * Returns the integer value stored at the given attribute.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The integer value stored at the given attribute
+	 */
+	public Value<Integer> getIntValue(String attribute) {
+		IntegerValue integerValue = integerCache.get(attribute);
+		if (integerValue == null) {
+			integerValue = new IntegerValue(this, attribute);
+			integerCache.put(attribute, integerValue);
+		}
+		return integerValue;
+	}
+
+	/**
+	 * Returns the long value stored at the given attribute.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The long value stored at the given attribute
+	 */
+	public Value<Long> getLongValue(String attribute) {
+		LongValue longValue = longCache.get(attribute);
+		if (longValue == null) {
+			longValue = new LongValue(this, attribute);
+			longCache.put(attribute, longValue);
+		}
+		return longValue;
+	}
+
+	/**
+	 * Returns the string value stored at the given attribute.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The double value stored at the given attribute
+	 */
+	public Value<String> getStringValue(String attribute) {
+		StringValue stringValue = stringCache.get(attribute);
+		if (stringValue == null) {
+			stringValue = new StringValue(this, attribute);
+			stringCache.put(attribute, stringValue);
+		}
+		return stringValue;
+	}
+
+	/**
+	 * Saves the configuration. This request is usually forwarded to the
+	 * {@link ConfigurationBackend}.
+	 *
+	 * @see ConfigurationBackend#save()
+	 * @throws ConfigurationException
+	 *             if the configuration can not be saved
+	 */
+	public void save() throws ConfigurationException {
+		configurationBackend.save();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ConfigurationBackend.java b/alien/src/net/pterodactylus/util/config/ConfigurationBackend.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ConfigurationBackend.java
@@ -0,0 +1,59 @@
+/*
+ * utils - ConfigurationBackend.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Interface for backends that can read and (optionally) write values at given
+ * attribute names.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ConfigurationBackend {
+
+	/**
+	 * Returns the value of the given attribute from the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The string representation of the value
+	 * @throws ConfigurationException
+	 *             if the attribute could not be found
+	 */
+	public String getValue(String attribute) throws ConfigurationException;
+
+	/**
+	 * Sets the value of the given attribute within the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute to set
+	 * @param value
+	 *            The string representation of the value
+	 * @throws ConfigurationException
+	 *             if the value could not be set
+	 */
+	public void putValue(String attribute, String value) throws ConfigurationException;
+
+	/**
+	 * Saves the configuration.
+	 *
+	 * @throws ConfigurationException
+	 *             if the configuration could not be saved
+	 */
+	public void save() throws ConfigurationException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ConfigurationException.java b/alien/src/net/pterodactylus/util/config/ConfigurationException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ConfigurationException.java
@@ -0,0 +1,68 @@
+/*
+ * utils - ConfigurationException.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Parent exception for all exceptions that can occur during configuration file
+ * parsing.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationException extends Exception {
+
+	/**
+	 * Constructs a new configuration exception.
+	 */
+	public ConfigurationException() {
+		super();
+	}
+
+	/**
+	 * Constructs a new configuration exception with the specified message
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public ConfigurationException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Constructs a new configuration exception with the specified message and
+	 * cause.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public ConfigurationException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * Constructs a new configuration exception with the specified cause.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public ConfigurationException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ConfigurationTest.xml b/alien/src/net/pterodactylus/util/config/ConfigurationTest.xml
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ConfigurationTest.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+	<test>10</test>
+</configuration>
\ No newline at end of file
diff --git a/alien/src/net/pterodactylus/util/config/DoubleValue.java b/alien/src/net/pterodactylus/util/config/DoubleValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/DoubleValue.java
@@ -0,0 +1,95 @@
+/*
+ * utils - DoubleValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * A wrapper around a double value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DoubleValue extends AbstractValue<Double> {
+
+	/**
+	 * Creates a new double value that updates its value to the given attribute
+	 * from the given configuration.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute name
+	 */
+	public DoubleValue(Configuration configuration, String attribute) {
+		super(configuration, attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public Double getValue() throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			return ((ExtendedConfigurationBackend) configuration.configurationBackend).getDoubleValue(attribute);
+		}
+		String value = null;
+		try {
+			value = configuration.configurationBackend.getValue(attribute);
+			double doubleValue = Double.valueOf(value);
+			return doubleValue;
+		} catch (NumberFormatException nfe1) {
+			throw new ValueFormatException("could not parse attribute \"" + value + "\".", nfe1);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public Double getValue(Double defaultValue) {
+		String value = null;
+		try {
+			if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+				return ((ExtendedConfigurationBackend) configuration.configurationBackend).getDoubleValue(attribute);
+			}
+			value = configuration.configurationBackend.getValue(attribute);
+			double doubleValue = Double.valueOf(value);
+			return doubleValue;
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		} catch (ConfigurationException ce1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(Double newValue) throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			((ExtendedConfigurationBackend) configuration.configurationBackend).setDoubleValue(attribute, newValue);
+		}
+		configuration.configurationBackend.putValue(attribute, (newValue != null) ? String.valueOf(newValue) : null);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ExtendedConfigurationBackend.java b/alien/src/net/pterodactylus/util/config/ExtendedConfigurationBackend.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ExtendedConfigurationBackend.java
@@ -0,0 +1,122 @@
+/*
+ * utils - ExtendedConfigurationBackend.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * The extended configuration backend allows a backend to specify ways to
+ * retrieve other types than strings. Defined in this backend are all types are
+ * used in {@link Configuration}, too: {@link Boolean}, {@link Double},
+ * {@link Integer}, {@link Long}, and {@link String}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ExtendedConfigurationBackend extends ConfigurationBackend {
+
+	/**
+	 * Returns the value of the given attribute from the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The stored value
+	 * @throws ConfigurationException
+	 *             if the attribute could not be found
+	 */
+	public Boolean getBooleanValue(String attribute) throws ConfigurationException;
+
+	/**
+	 * Sets the value of the given attribute within the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute to set
+	 * @param value
+	 *            The value to store
+	 * @throws ConfigurationException
+	 *             if the value could not be set
+	 */
+	public void setBooleanValue(String attribute, Boolean value) throws ConfigurationException;
+
+	/**
+	 * Returns the value of the given attribute from the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The stored value
+	 * @throws ConfigurationException
+	 *             if the attribute could not be found
+	 */
+	public Double getDoubleValue(String attribute) throws ConfigurationException;
+
+	/**
+	 * Sets the value of the given attribute within the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute to set
+	 * @param value
+	 *            The value to store
+	 * @throws ConfigurationException
+	 *             if the value could not be set
+	 */
+	public void setDoubleValue(String attribute, Double value) throws ConfigurationException;
+
+	/**
+	 * Returns the value of the given attribute from the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The stored value
+	 * @throws ConfigurationException
+	 *             if the attribute could not be found
+	 */
+	public Integer getIntegerValue(String attribute) throws ConfigurationException;
+
+	/**
+	 * Sets the value of the given attribute within the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute to set
+	 * @param value
+	 *            The value to store
+	 * @throws ConfigurationException
+	 *             if the value could not be set
+	 */
+	public void setIntegerValue(String attribute, Integer value) throws ConfigurationException;
+
+	/**
+	 * Returns the value of the given attribute from the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute
+	 * @return The stored value
+	 * @throws ConfigurationException
+	 *             if the attribute could not be found
+	 */
+	public Long getLongValue(String attribute) throws ConfigurationException;
+
+	/**
+	 * Sets the value of the given attribute within the backend.
+	 *
+	 * @param attribute
+	 *            The name of the attribute to set
+	 * @param value
+	 *            The value to store
+	 * @throws ConfigurationException
+	 *             if the value could not be set
+	 */
+	public void setLongValue(String attribute, Long value) throws ConfigurationException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/IntegerValue.java b/alien/src/net/pterodactylus/util/config/IntegerValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/IntegerValue.java
@@ -0,0 +1,95 @@
+/*
+ * utils - IntegerValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * A wrapper around an integer value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IntegerValue extends AbstractValue<Integer> {
+
+	/**
+	 * Creates a new integer value that updates its value to the given attribute
+	 * from the given configuration.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute name
+	 */
+	public IntegerValue(Configuration configuration, String attribute) {
+		super(configuration, attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public Integer getValue() throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			return ((ExtendedConfigurationBackend) configuration.configurationBackend).getIntegerValue(attribute);
+		}
+		String value = null;
+		try {
+			value = configuration.configurationBackend.getValue(attribute);
+			int integerValue = Integer.valueOf(value);
+			return integerValue;
+		} catch (NumberFormatException nfe1) {
+			throw new ValueFormatException("could not parse attribute \"" + value + "\".", nfe1);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public Integer getValue(Integer defaultValue) {
+		String value = null;
+		try {
+			if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+				return ((ExtendedConfigurationBackend) configuration.configurationBackend).getIntegerValue(attribute);
+			}
+			value = configuration.configurationBackend.getValue(attribute);
+			int integerValue = Integer.valueOf(value);
+			return integerValue;
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		} catch (ConfigurationException ce1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(Integer newValue) throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			((ExtendedConfigurationBackend) configuration.configurationBackend).setIntegerValue(attribute, newValue);
+		}
+		configuration.configurationBackend.putValue(attribute, (newValue != null) ? String.valueOf(newValue) : null);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/LongValue.java b/alien/src/net/pterodactylus/util/config/LongValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/LongValue.java
@@ -0,0 +1,95 @@
+/*
+ * utils - LongValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * A wrapper around a long value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LongValue extends AbstractValue<Long> {
+
+	/**
+	 * Creates a new long value that updates its value to the given attribute
+	 * from the given configuration.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute name
+	 */
+	public LongValue(Configuration configuration, String attribute) {
+		super(configuration, attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public Long getValue() throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			return ((ExtendedConfigurationBackend) configuration.configurationBackend).getLongValue(attribute);
+		}
+		String value = null;
+		try {
+			value = configuration.configurationBackend.getValue(attribute);
+			long longValue = Long.valueOf(value);
+			return longValue;
+		} catch (NumberFormatException nfe1) {
+			throw new ValueFormatException("could not parse attribute \"" + value + "\".", nfe1);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public Long getValue(Long defaultValue) {
+		String value = null;
+		try {
+			if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+				return ((ExtendedConfigurationBackend) configuration.configurationBackend).getLongValue(attribute);
+			}
+			value = configuration.configurationBackend.getValue(attribute);
+			long longValue = Long.valueOf(value);
+			return longValue;
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		} catch (ConfigurationException ce1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(Long newValue) throws ConfigurationException {
+		if (configuration.configurationBackend instanceof ExtendedConfigurationBackend) {
+			((ExtendedConfigurationBackend) configuration.configurationBackend).setLongValue(attribute, newValue);
+		}
+		configuration.configurationBackend.putValue(attribute, (newValue != null) ? String.valueOf(newValue) : null);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/MapConfigurationBackend.java b/alien/src/net/pterodactylus/util/config/MapConfigurationBackend.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/MapConfigurationBackend.java
@@ -0,0 +1,304 @@
+/*
+ * utils - MapConfigurationBackend.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.text.StringEscaper;
+import net.pterodactylus.util.text.TextException;
+
+/**
+ * Configuration backend that is backed by a {@link Map}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MapConfigurationBackend implements ConfigurationBackend {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(MapConfigurationBackend.class);
+
+	/** The backing file, if any. */
+	private final File configurationFile;
+
+	/** The backing map. */
+	private final Map<String, String> values = new HashMap<String, String>();
+
+	/**
+	 * Creates a new configuration backend.
+	 */
+	public MapConfigurationBackend() {
+		this(Collections.<String, String> emptyMap());
+	}
+
+	/**
+	 * Creates a new configuration backend that contains the values from the
+	 * given map.
+	 *
+	 * @param values
+	 *            The initial values
+	 */
+	public MapConfigurationBackend(Map<String, String> values) {
+		configurationFile = null;
+		this.values.putAll(values);
+	}
+
+	/**
+	 * Creates a new configuration backend that loads and stores its
+	 * configuration in the given file.
+	 *
+	 * @param configurationFile
+	 *            The file to store the configuration in
+	 * @throws ConfigurationException
+	 *             if the configuration can not be loaded from the file
+	 */
+	public MapConfigurationBackend(File configurationFile) throws ConfigurationException {
+		this(configurationFile, false);
+	}
+
+	/**
+	 * Creates a new configuration backend from the given file that also
+	 * contains the given values.
+	 *
+	 * @param configurationFile
+	 *            The file to store the configuration in
+	 * @param values
+	 *            Additional initial values
+	 * @throws ConfigurationException
+	 *             if the configuration can not be loaded from the file
+	 */
+	public MapConfigurationBackend(File configurationFile, Map<String, String> values) throws ConfigurationException {
+		this(configurationFile, false, values);
+	}
+
+	/**
+	 * Creates a new configuration backend from the given file.
+	 *
+	 * @param configurationFile
+	 *            The file to store the configuration in
+	 * @param ignoreMissing
+	 *            {@code true} to ignore a missing configuration file when
+	 *            loading the configuration
+	 * @throws ConfigurationException
+	 *             if the configuration can not be loaded from the file
+	 */
+	public MapConfigurationBackend(File configurationFile, boolean ignoreMissing) throws ConfigurationException {
+		this(configurationFile, ignoreMissing, null);
+	}
+
+	/**
+	 * Creates a new configuration backend from the given file that also
+	 * contains the given values.
+	 *
+	 * @param configurationFile
+	 *            The file to store the configuration in
+	 * @param ignoreMissing
+	 *            {@code true} to ignore a missing configuration file when
+	 *            loading the configuration
+	 * @param values
+	 *            Additional initial values
+	 * @throws ConfigurationException
+	 *             if the configuration can not be loaded from the file
+	 */
+	public MapConfigurationBackend(File configurationFile, boolean ignoreMissing, Map<String, String> values) throws ConfigurationException {
+		this.configurationFile = configurationFile;
+		if (configurationFile != null) {
+			loadValues(ignoreMissing);
+		}
+		if (values != null) {
+			this.values.putAll(values);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#getValue(java.lang.String)
+	 */
+	@Override
+	public String getValue(String attribute) {
+		synchronized (values) {
+			return values.get(attribute);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#putValue(java.lang.String,
+	 *      java.lang.String)
+	 */
+	@Override
+	public void putValue(String attribute, String value) throws ConfigurationException {
+		synchronized (values) {
+			values.put(attribute, value);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void save() throws ConfigurationException {
+		synchronized (values) {
+			saveValues();
+		}
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Loads the configuration from the {@link #configurationFile}. If
+	 * {@code ignoreMissing} is {@code false} a {@link ConfigurationException}
+	 * will be thrown if the file does not exist.
+	 *
+	 * @param ignoreMissing
+	 *            {@code true} to ignore a missing configuration file,
+	 *            {@code false} to throw a {@link ConfigurationException}
+	 * @throws ConfigurationException
+	 *             if the file can not be found or read, or the values can not
+	 *             be parsed
+	 */
+	private void loadValues(boolean ignoreMissing) throws ConfigurationException {
+		if (!configurationFile.exists()) {
+			if (!ignoreMissing) {
+				throw new ConfigurationException("Configuration file “" + configurationFile.getName() + "” is missing!");
+			}
+			return;
+		}
+		FileInputStream configurationInputStream = null;
+		InputStreamReader inputStreamReader = null;
+		BufferedReader bufferedReader = null;
+		try {
+			configurationInputStream = new FileInputStream(configurationFile);
+			inputStreamReader = new InputStreamReader(configurationInputStream, "UTF-8");
+			bufferedReader = new BufferedReader(inputStreamReader);
+			Map<String, String> values = new HashMap<String, String>();
+			String line;
+			while ((line = bufferedReader.readLine()) != null) {
+				if (line.startsWith("#") || line.startsWith(";") || (line.trim().length() == 0)) {
+					continue;
+				}
+				int colon = line.indexOf(':');
+				int equals = line.indexOf('=');
+				if ((colon == -1) && (equals == -1)) {
+					throw new ConfigurationException("Line without “:” or “=” found: " + line);
+				}
+				String key;
+				if (colon != -1) {
+					if (equals != -1) {
+						key = line.substring(0, Math.min(colon, equals));
+					} else {
+						key = line.substring(0, colon);
+					}
+				} else {
+					key = line.substring(0, equals);
+				}
+				if (line.substring(key.length() + 1).trim().length() == 0) {
+					values.put(key, null);
+				} else {
+					key = StringEscaper.parseLine(key).get(0);
+					List<String> words = StringEscaper.parseLine(line.substring(key.length() + 1).trim());
+					StringBuilder value = new StringBuilder();
+					for (String word : words) {
+						if (value.length() > 0) {
+							value.append(' ');
+						}
+						value.append(word);
+					}
+					values.put(key, value.toString());
+				}
+			}
+			this.values.putAll(values);
+		} catch (FileNotFoundException fnfe1) {
+			if (!ignoreMissing) {
+				throw new ConfigurationException("Could not find configuration file “" + configurationFile.getName() + "”!", fnfe1);
+			}
+		} catch (UnsupportedEncodingException uee1) {
+			/* impossible, I’d say. */
+			logger.log(Level.SEVERE, "JVM does not support UTF-8!");
+		} catch (IOException ioe1) {
+			throw new ConfigurationException("Could not read configuration from “" + configurationFile.getName() + "”!", ioe1);
+		} catch (TextException te1) {
+			throw new ConfigurationException("Could not parse configuration value!", te1);
+		} finally {
+			Closer.close(bufferedReader);
+			Closer.close(inputStreamReader);
+			Closer.close(configurationInputStream);
+		}
+	}
+
+	/**
+	 * Saves the configuration to the configuration file, if it is not
+	 * {@code null}. If no configuration file has been set, this method simply
+	 * returns.
+	 *
+	 * @throws ConfigurationException
+	 *             if there was an error when writing the configuration
+	 */
+	public void saveValues() throws ConfigurationException {
+		if (configurationFile == null) {
+			return;
+		}
+		FileOutputStream configurationOutputStream = null;
+		OutputStreamWriter outputStreamWriter = null;
+		BufferedWriter bufferedWriter = null;
+		try {
+			configurationOutputStream = new FileOutputStream(configurationFile);
+			outputStreamWriter = new OutputStreamWriter(configurationOutputStream, "UTF-8");
+			bufferedWriter = new BufferedWriter(outputStreamWriter);
+			for (Entry<String, String> value : values.entrySet()) {
+				bufferedWriter.write(StringEscaper.escapeWord(value.getKey()));
+				bufferedWriter.write(": ");
+				bufferedWriter.write(StringEscaper.escapeWord(value.getValue()));
+				bufferedWriter.newLine();
+			}
+		} catch (FileNotFoundException fnfe1) {
+			throw new ConfigurationException("Could not create configuration file “" + configurationFile.getName() + "”!", fnfe1);
+		} catch (UnsupportedEncodingException uee1) {
+			/* impossible, I’d say. */
+			logger.log(Level.SEVERE, "JVM does not support UTF-8!");
+		} catch (IOException ioe1) {
+			throw new ConfigurationException("Could not write to configuration file!", ioe1);
+		} finally {
+			Closer.close(bufferedWriter);
+			Closer.close(outputStreamWriter);
+			Closer.close(configurationOutputStream);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/Parser.java b/alien/src/net/pterodactylus/util/config/Parser.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/Parser.java
@@ -0,0 +1,41 @@
+/*
+ * utils - Parser.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Interface for abstract object parsers. Since you are free to implement
+ * methods
+ *
+ * @param <T>
+ *            The type of the object to parse from a {@link Configuration}
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Parser<T> {
+
+	/**
+	 * Parses an abstract object from a {@link Configuration}.
+	 *
+	 * @param attribute
+	 *            The name of the attribute of the abstract object
+	 * @return The abstract object
+	 * @throws ConfigurationException
+	 *             if a configuration error occured
+	 */
+	public Value<T> parse(String attribute) throws ConfigurationException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ReadOnlyValue.java b/alien/src/net/pterodactylus/util/config/ReadOnlyValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ReadOnlyValue.java
@@ -0,0 +1,72 @@
+/*
+ * utils - ReadOnlyValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Implementation of a read-only value that never changes.
+ *
+ * @param <T>
+ *            The real type of the value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReadOnlyValue<T> implements Value<T> {
+
+	/** The wrapped value. */
+	private final T value;
+
+	/**
+	 * Creates a new read-only value.
+	 *
+	 * @param value
+	 *            The value to wrap
+	 */
+	public ReadOnlyValue(T value) {
+		this.value = value;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public T getValue() throws ConfigurationException {
+		return value;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public T getValue(T defaultValue) {
+		return value;
+	}
+
+	/**
+	 * {@inheritDoc} This method does nothing.
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(T newValue) throws ConfigurationException {
+		/* ignore. */
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/StringValue.java b/alien/src/net/pterodactylus/util/config/StringValue.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/StringValue.java
@@ -0,0 +1,74 @@
+/*
+ * utils - StringValue.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * A wrapper around a string value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StringValue extends AbstractValue<String> {
+
+	/**
+	 * Creates a new string value that updates its value to the given attribute
+	 * from the given configuration.
+	 *
+	 * @param configuration
+	 *            The configuration that created this value
+	 * @param attribute
+	 *            The name of this value’s attribute name
+	 */
+	public StringValue(Configuration configuration, String attribute) {
+		super(configuration, attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue()
+	 */
+	@Override
+	public String getValue() throws ConfigurationException {
+		return configuration.configurationBackend.getValue(attribute);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#getValue(java.lang.Object)
+	 */
+	@Override
+	public String getValue(String defaultValue) {
+		try {
+			return configuration.configurationBackend.getValue(attribute);
+		} catch (ConfigurationException ce1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.Value#setValue(java.lang.Object)
+	 */
+	@Override
+	public void setValue(String newValue) throws ConfigurationException {
+		configuration.configurationBackend.putValue(attribute, newValue);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/Value.java b/alien/src/net/pterodactylus/util/config/Value.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/Value.java
@@ -0,0 +1,62 @@
+/*
+ * utils - Value.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Wrapper for a configuration value. Returning this wrapper instead of the
+ * integral types allows the value to be updated (e.g. when the configuration
+ * file changes) or written back to the underlying configuraiton backend (which
+ * might in turn support persisting itself).
+ *
+ * @param <T>
+ *            The type of the wrapped data
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Value<T> {
+
+	/**
+	 * Returns the wrapped value.
+	 *
+	 * @return The wrapped value
+	 * @throws ConfigurationException
+	 *             if a configuration error occurs
+	 */
+	public T getValue() throws ConfigurationException;
+
+	/**
+	 * Returns the wrapped value, if possible. If the value can not be retrieved
+	 * or parsed, the default value is returned instead.
+	 *
+	 * @param defaultValue
+	 *            The default value to return in case of an error
+	 * @return The wrapped value, or the default value if the wrapped value can
+	 *         not be retrieved or parsed
+	 */
+	public T getValue(T defaultValue);
+
+	/**
+	 * Sets a new wrapped value.
+	 *
+	 * @param newValue
+	 *            The new wrapped value
+	 * @throws ConfigurationException
+	 *             if a configuration error occurs
+	 */
+	public void setValue(T newValue) throws ConfigurationException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/ValueFormatException.java b/alien/src/net/pterodactylus/util/config/ValueFormatException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/ValueFormatException.java
@@ -0,0 +1,67 @@
+/*
+ * utils - ValueFormatException.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+/**
+ * Exception that signals a conversion error if a specified format for a value
+ * is requested.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ValueFormatException extends ConfigurationException {
+
+	/**
+	 * Constructs a new value format exception.
+	 */
+	public ValueFormatException() {
+		super();
+	}
+
+	/**
+	 * Constructs a new value format exception with the given message.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public ValueFormatException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Constructs a new value format exception with the given cause.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public ValueFormatException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * Constructs a new value format exception with the given message and cause.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public ValueFormatException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/XMLConfigurationBackend.java b/alien/src/net/pterodactylus/util/config/XMLConfigurationBackend.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/XMLConfigurationBackend.java
@@ -0,0 +1,245 @@
+/*
+ * utils - XMLConfigurationBackend.java - Copyright © 2007-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.xml.SimpleXML;
+import net.pterodactylus.util.xml.XML;
+
+import org.w3c.dom.Document;
+
+/**
+ * Configuration backend that reads and writes its configuration from/to an XML
+ * file.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class XMLConfigurationBackend implements ConfigurationBackend {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(XMLConfigurationBackend.class.getName());
+
+	/** The node cache. */
+	private final Map<String, SimpleXML> nodeCache = new HashMap<String, SimpleXML>();
+
+	/** The configuration file. */
+	private final File configurationFile;
+
+	/** The last modification time of the configuration file. */
+	private long lastModified;
+
+	/** The root node of the document. */
+	private final SimpleXML rootNode;
+
+	/**
+	 * Creates a new backend backed by the given file.
+	 *
+	 * @param configurationFile
+	 *            The XML file to read the configuration from
+	 * @throws ConfigurationException
+	 *             if the XML can not be read or parsed
+	 */
+	public XMLConfigurationBackend(File configurationFile) throws ConfigurationException {
+		this(configurationFile, false);
+	}
+
+	/**
+	 * Creates a new backend backed by the given file.
+	 *
+	 * @param configurationFile
+	 *            The XML file to read the configuration from
+	 * @param create
+	 *            {@code true} to create a new configuration when loading the
+	 *            configuration from the given file fails, {@code false} to
+	 *            throw a {@link ConfigurationException}
+	 * @throws ConfigurationException
+	 *             if the XML can not be read or parsed
+	 */
+	public XMLConfigurationBackend(File configurationFile, boolean create) throws ConfigurationException {
+		this.configurationFile = configurationFile;
+		rootNode = readConfigurationFile(create);
+	}
+
+	/**
+	 * Reads and parses the configuration file.
+	 *
+	 * @param create
+	 *            {@code true} to create a new configuration if loading the
+	 *            configuration file fails, {@code false} to throw a
+	 *            {@link ConfigurationException}
+	 * @return The created root node
+	 * @throws ConfigurationException
+	 *             if the file can not be read or parsed
+	 */
+	private synchronized SimpleXML readConfigurationFile(boolean create) throws ConfigurationException {
+		FileInputStream configFileInputStream = null;
+		try {
+			configFileInputStream = new FileInputStream(configurationFile);
+			Document configurationDocument = XML.transformToDocument(configFileInputStream);
+			if (configurationDocument == null) {
+				if (!create) {
+					throw new ConfigurationException("can not parse XML document");
+				}
+				configurationDocument = new SimpleXML("config").getDocument();
+			}
+			nodeCache.clear();
+			return SimpleXML.fromDocument(configurationDocument);
+		} catch (FileNotFoundException fnfe1) {
+			if (!create) {
+				throw new ConfigurationException(fnfe1);
+			}
+			return new SimpleXML("config");
+		} finally {
+			Closer.close(configFileInputStream);
+		}
+	}
+
+	/**
+	 * Writes the current document (including changes) back to the
+	 * {@link #configurationFile}.
+	 *
+	 * @throws ConfigurationException
+	 *             if the document could not be written
+	 */
+	private synchronized void writeConfigurationFile() throws ConfigurationException {
+		FileOutputStream configurationFileOutputStream = null;
+		OutputStreamWriter configurationOutputStreamWriter = null;
+		try {
+			configurationFileOutputStream = new FileOutputStream(configurationFile);
+			configurationOutputStreamWriter = new OutputStreamWriter(configurationFileOutputStream, "UTF-8");
+			XML.writeToOutputStream(rootNode.getDocument(), configurationOutputStreamWriter);
+		} catch (IOException ioe1) {
+			throw new ConfigurationException(ioe1.getMessage(), ioe1);
+		} finally {
+			Closer.close(configurationOutputStreamWriter);
+			Closer.close(configurationFileOutputStream);
+		}
+	}
+
+	//
+	// INTERFACE ConfigurationBackend
+	//
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#getValue(java.lang.String)
+	 */
+	@Override
+	public String getValue(String attribute) throws ConfigurationException {
+		if (configurationFile.lastModified() > lastModified) {
+			logger.info("reloading configuration file " + configurationFile.getAbsolutePath());
+			readConfigurationFile(false);
+			lastModified = configurationFile.lastModified();
+		}
+		SimpleXML node = getNode(attribute);
+		String value = node.getValue();
+		logger.log(Level.FINEST, "attribute: “%1$s”, value: “%2$s”", new Object[] { attribute, value });
+		return value;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.config.ConfigurationBackend#putValue(java.lang.String,
+	 *      java.lang.String)
+	 */
+	@Override
+	public void putValue(String attribute, String value) throws ConfigurationException {
+		SimpleXML node = getNode(attribute, true);
+		node.setValue(value);
+		writeConfigurationFile();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void save() throws ConfigurationException {
+		writeConfigurationFile();
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Searches for the node with the given name and returns it. The given
+	 * attribute may contain several nodes, separated by a pipe character (“|”),
+	 * that describe where in the document hierarchy the node can be found.
+	 *
+	 * @param attribute
+	 *            The complete name of the node
+	 * @return The node, if found
+	 * @throws ConfigurationException
+	 *             if the node could not be found
+	 */
+	private SimpleXML getNode(String attribute) throws ConfigurationException {
+		return getNode(attribute, false);
+	}
+
+	/**
+	 * Searches for the node with the given name and returns it. The given
+	 * attribute may contain several nodes, separated by a pipe character (“|”),
+	 * that describe where in the document hierarchy the node can be found.
+	 *
+	 * @param attribute
+	 *            The complete name of the node
+	 * @param create
+	 *            {@code true} to create the node if it doesn’t exist,
+	 *            {@code false} to throw a {@link ConfigurationException} if it
+	 *            doesn’t exist
+	 * @return The node, if found
+	 * @throws ConfigurationException
+	 *             if the node could not be found
+	 */
+	private SimpleXML getNode(String attribute, boolean create) throws ConfigurationException {
+		if (nodeCache.containsKey(attribute)) {
+			return nodeCache.get(attribute);
+		}
+		StringTokenizer attributes = new StringTokenizer(attribute, "|/");
+		SimpleXML node = rootNode;
+		while (attributes.hasMoreTokens()) {
+			String nodeName = attributes.nextToken();
+			if (node.hasNode(nodeName)) {
+				node = node.getNode(nodeName);
+			} else {
+				if (!create) {
+					throw new AttributeNotFoundException(attribute);
+				}
+				node = node.append(nodeName);
+			}
+		}
+		nodeCache.put(attribute, node);
+		return node;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/config/package-info.java b/alien/src/net/pterodactylus/util/config/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/config/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * API for handling application configurations. <h2>Using a Configuration</h2>
+ * Using the Configuration API consists of only three small steps. First, you
+ * need to create a {@link net.pterodactylus.util.config.ConfigurationBackend} for the
+ * configuration you want to access. Using the configuration backend you can
+ * then create the {@link net.pterodactylus.util.config.Configuration} and start reading
+ * integral data types from it. By writing specialized parsers you can handle
+ * more abstract objects (e.g. for databases or message connectors) as well.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+package net.pterodactylus.util.config;
+
diff --git a/alien/src/net/pterodactylus/util/data/DefaultNode.java b/alien/src/net/pterodactylus/util/data/DefaultNode.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/data/DefaultNode.java
@@ -0,0 +1,264 @@
+/*
+ * utils - DefaultNode.java - Copyright © 2009-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.data;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Default implementation of the {@link Node} interface.
+ *
+ * @param <E>
+ *            The type of the element to store
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class DefaultNode<E extends Comparable<E>> implements Node<E> {
+
+	/** The parent node of this node. */
+	private final Node<E> parentNode;
+
+	/** The element contained in this node. */
+	private final E element;
+
+	/** The child nodes of this node. */
+	private final List<Node<E>> children = new ArrayList<Node<E>>();
+
+	/**
+	 * Creates a new root node.
+	 */
+	DefaultNode() {
+		this.parentNode = null;
+		this.element = null;
+	}
+
+	/**
+	 * Creates a new node with the given parent and element.
+	 *
+	 * @param parentNode
+	 *            The parent of this node
+	 * @param element
+	 *            The element of this node
+	 */
+	DefaultNode(Node<E> parentNode, E element) {
+		if ((parentNode == null) || (element == null)) {
+			throw new NullPointerException("null is not allowed as parent or element");
+		}
+		this.parentNode = parentNode;
+		this.element = element;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Node<E> getParent() {
+		return parentNode;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public E getElement() {
+		return element;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Node<E> addChild(E child) {
+		Node<E> childNode = new DefaultNode<E>(this, child);
+		children.add(childNode);
+		return childNode;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int size() {
+		return children.size();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Node<E> getChild(int childIndex) {
+		return children.get(childIndex);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Node<E> getChild(E element) {
+		for (Node<E> childNode : children) {
+			if (childNode.getElement().equals(element)) {
+				return childNode;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean hasChild(Node<E> childNode) {
+		return children.contains(childNode);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean hasChild(E element) {
+		for (Node<E> childNode : children) {
+			if (childNode.getElement().equals(element)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getIndexOfChild(Node<E> childNode) {
+		int childIndex = 0;
+		for (Node<E> node : children) {
+			if (node.equals(childNode)) {
+				return childIndex;
+			}
+			childIndex++;
+		}
+		return -1;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getIndexOfChild(E element) {
+		int childIndex = 0;
+		for (Node<E> node : children) {
+			if (node.getElement().equals(element)) {
+				return childIndex;
+			}
+			childIndex++;
+		}
+		return -1;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeChild(Node<E> childNode) {
+		children.remove(childNode);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeChild(E child) {
+		for (Node<E> childNode : children) {
+			if (child.equals(childNode.getElement())) {
+				children.remove(childNode);
+				break;
+			}
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeChild(int childIndex) {
+		children.remove(childIndex);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeAllChildren() {
+		children.clear();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Iterator<Node<E>> iterator() {
+		return children.iterator();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Node<E> findChild(E element) {
+		for (Node<E> childNode : children) {
+			Node<E> wantedNode = childNode.findChild(element);
+			if (wantedNode != null) {
+				return wantedNode;
+			}
+		}
+		if (this.element.equals(element)) {
+			return this;
+		}
+		return null;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void sortChildren() {
+		Collections.sort(children);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void sortChildren(Comparator<Node<E>> comparator) {
+		Collections.sort(children, comparator);
+	}
+
+	//
+	// INTERFACE Comparable
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int compareTo(Node<E> otherNode) {
+		return element.compareTo(otherNode.getElement());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/data/Node.java b/alien/src/net/pterodactylus/util/data/Node.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/data/Node.java
@@ -0,0 +1,186 @@
+/*
+ * utils - Node.java - Copyright © 2009-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.data;
+
+import java.util.Comparator;
+import java.util.Iterator;
+
+/**
+ * A node that can be stored in a {@link Tree}. A node has exactly one parent
+ * (which is <code>null</code> if the node is the {@link Tree#getRootNode()} of
+ * the tree) and an arbitrary amount of child nodes.
+ *
+ * @param <E>
+ *            The type of the element to store
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Node<E extends Comparable<E>> extends Iterable<Node<E>>, Comparable<Node<E>> {
+
+	/**
+	 * Returns the parent node of the node.
+	 *
+	 * @return The parent node
+	 */
+	public Node<E> getParent();
+
+	/**
+	 * Returns the element that is stored in the node.
+	 *
+	 * @return The node’s element
+	 */
+	public E getElement();
+
+	/**
+	 * Adds an element as a child to this node and returns the created node.
+	 *
+	 * @param child
+	 *            The child node’s element
+	 * @return The created child node
+	 */
+	public Node<E> addChild(E child);
+
+	/**
+	 * Returns the number of children this node has.
+	 *
+	 * @return The number of children
+	 */
+	public int size();
+
+	/**
+	 * Returns the child at the given index.
+	 *
+	 * @param index
+	 *            The index of the child
+	 * @return The child at the given index
+	 */
+	public Node<E> getChild(int index);
+
+	/**
+	 * Returns the direct child node that contains the given element.
+	 *
+	 * @param element
+	 *            The element
+	 * @return The direct child node containing the given element, or
+	 *         <code>null</code> if this node does not have a child node
+	 *         containing the given element
+	 */
+	public Node<E> getChild(E element);
+
+	/**
+	 * Returns whether the given node is a direct child of this node.
+	 *
+	 * @param childNode
+	 *            The child node
+	 * @return <code>true</code> if the given node is a direct child of this
+	 *         node, <code>false</code> otherwise
+	 */
+	public boolean hasChild(Node<E> childNode);
+
+	/**
+	 * Returns whether this node contains a child node containing the given
+	 * element.
+	 *
+	 * @param element
+	 *            The element
+	 * @return <code>true</code> if this node contains a direct child node
+	 *         containing the given element, <code>false</code> otherwise
+	 */
+	public boolean hasChild(E element);
+
+	/**
+	 * Returns the index of the given child node.
+	 *
+	 * @param childNode
+	 *            The child node
+	 * @return The index of the child node, or <code>-1</code> if the child node
+	 *         is not a child node of this node
+	 */
+	public int getIndexOfChild(Node<E> childNode);
+
+	/**
+	 * Returns the index of the child node containing the given element.
+	 *
+	 * @param element
+	 *            The element
+	 * @return The index of the child node, or <code>-1</code> if the child node
+	 *         is not a child node of this node
+	 */
+	public int getIndexOfChild(E element);
+
+	/**
+	 * Remove the given child node from this node. If the given node is not a
+	 * child of this node, nothing happens.
+	 *
+	 * @param childNode
+	 *            The child node to remove
+	 */
+	public void removeChild(Node<E> childNode);
+
+	/**
+	 * Removes the child node that contains the given element. The element in
+	 * the node is checked using {@link Object#equals(Object)}.
+	 *
+	 * @param child
+	 *            The child element to remove
+	 */
+	public void removeChild(E child);
+
+	/**
+	 * Removes the child at the given index.
+	 *
+	 * @param childIndex
+	 *            The index of the child to remove
+	 */
+	public void removeChild(int childIndex);
+
+	/**
+	 * Removes all children from this node.
+	 */
+	public void removeAllChildren();
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Iterator<Node<E>> iterator();
+
+	/**
+	 * Searches this node’s children recursively for a node that contains the
+	 * given element.
+	 *
+	 * @param element
+	 *            The element to search
+	 * @return The node that contains the given element, or <code>null</code> if
+	 *         no node could be found
+	 */
+	public Node<E> findChild(E element);
+
+	/**
+	 * Sorts all children according to their natural order.
+	 */
+	public void sortChildren();
+
+	/**
+	 * Sorts all children with the given comparator.
+	 *
+	 * @param comparator
+	 *            The comparator used to sort the children
+	 */
+	public void sortChildren(Comparator<Node<E>> comparator);
+
+}
diff --git a/alien/src/net/pterodactylus/util/data/Tree.java b/alien/src/net/pterodactylus/util/data/Tree.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/data/Tree.java
@@ -0,0 +1,42 @@
+/*
+ * utils - Tree.java - Copyright © 2009-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.data;
+
+/**
+ * A tree structure in which every node can have an arbitrary amount of
+ * children.
+ *
+ * @param <E>
+ *            The type of the element to store
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Tree<E extends Comparable<E>> {
+
+	/** The root node of the tree. */
+	private final Node<E> rootNode = new DefaultNode<E>();
+
+	/**
+	 * Returns the root node of the tree.
+	 *
+	 * @return The root node of the tree
+	 */
+	public Node<E> getRootNode() {
+		return rootNode;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/AbstractDatabase.java b/alien/src/net/pterodactylus/util/database/AbstractDatabase.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/AbstractDatabase.java
@@ -0,0 +1,441 @@
+/*
+ * utils - AbstractDatabase.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import net.pterodactylus.util.io.Closer;
+
+/**
+ * Abstract implementation of a {@link Database}. This already contains all the
+ * heavy lifting, the only thing that is still left is the actual database
+ * connection. This is done by overriding {@link #getConnection()} to return a
+ * new connection to a database.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractDatabase implements Database {
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * @see net.pterodactylus.util.database.Database#getSingle(net.pterodactylus.util.database.Query,
+	 *      net.pterodactylus.util.database.ObjectCreator)
+	 */
+	@Override
+	public <T> T getSingle(Query query, ObjectCreator<T> objectCreator) throws DatabaseException {
+		return new SingleDatabaseWorker<T>(query, objectCreator).work();
+	}
+
+	/**
+	 * @see net.pterodactylus.util.database.Database#getMultiple(net.pterodactylus.util.database.Query,
+	 *      net.pterodactylus.util.database.ObjectCreator)
+	 */
+	@Override
+	public <T> List<T> getMultiple(Query query, ObjectCreator<T> objectCreator) throws DatabaseException {
+		return new MultipleDatabaseWorker<T>(query, objectCreator).work();
+	}
+
+	/**
+	 * @see net.pterodactylus.util.database.Database#insert(net.pterodactylus.util.database.Query)
+	 */
+	@Override
+	public long insert(Query query) throws DatabaseException {
+		return new InsertDatabaseWorker(query).work();
+	}
+
+	/**
+	 * @see net.pterodactylus.util.database.Database#update(net.pterodactylus.util.database.Query)
+	 */
+	@Override
+	public int update(Query query) throws DatabaseException {
+		return new UpdateDatabaseWorker(query).work();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.database.Database#process(Query, ResultProcessor)
+	 */
+	@Override
+	public void process(Query query, ResultProcessor resultProcessor) throws DatabaseException {
+		new ProcessWorker(query, resultProcessor).work();
+	}
+
+	//
+	// STATIC METHODS
+	//
+
+	/**
+	 * Creates a database from a {@link DataSource}.
+	 *
+	 * @param dataSource
+	 *            The data source to create a database from
+	 * @return The database that uses the given data source
+	 */
+	public static Database fromDataSource(final DataSource dataSource) {
+		return new AbstractDatabase() {
+
+			@Override
+			protected void returnConnection(Connection connection) {
+				Closer.close(connection);
+			}
+
+			@Override
+			protected Connection getConnection() throws SQLException {
+				return dataSource.getConnection();
+			}
+		};
+	}
+
+	//
+	// ABSTRACT METHODS
+	//
+
+	/**
+	 * Creates a new connection to the database.
+	 *
+	 * @return The created connection
+	 * @throws SQLException
+	 *             if a database error occurs
+	 */
+	protected abstract Connection getConnection() throws SQLException;
+
+	/**
+	 * Returns the connection after it has been used. This method can be used to
+	 * simply close the connection, or to return it to a connection pool.
+	 *
+	 * @param connection
+	 *            The connection to return
+	 */
+	protected abstract void returnConnection(Connection connection);
+
+	/**
+	 * Abstract base class for database workers. A database worker executes a
+	 * query and returns a result that depends on the type of execute query.
+	 * <p>
+	 * This database worker contains several {@code run()} methods, each for
+	 * different “stages” of a normal JDBC interaction: {@link #run(Connection)}
+	 * is run with a {@link Connection} from
+	 * {@link AbstractDatabase#getConnection()}, {@link #run(PreparedStatement)}
+	 * with a {@link PreparedStatement} that was generated by
+	 * {@link Query#createStatement(Connection)}, and {@link #run(ResultSet)}
+	 * with a {@link ResultSet} that was obtained by
+	 * {@link PreparedStatement#executeQuery()}. When a resource object was
+	 * obtained by a not-overridden method of this class, the resource will be
+	 * closed after the “deeper” {@code run()} method returns. Each method can
+	 * be overridden if the default behaviour is not appropriate for the
+	 * implementing database worker.
+	 *
+	 * @param <T>
+	 *            The type of the result
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private abstract class AbstractDatabaseWorker<T> {
+
+		/** The query to execute. */
+		protected final Query query;
+
+		/**
+		 * Creates a new abstract database worker that will execute the given
+		 * query.
+		 *
+		 * @param query
+		 *            The query to execute
+		 */
+		protected AbstractDatabaseWorker(Query query) {
+			this.query = query;
+		}
+
+		/**
+		 * Processes the given result set. The default implementation simply
+		 * returns {@code null} here.
+		 *
+		 * @param resultSet
+		 *            The result set to process
+		 * @return The worker’s result
+		 * @throws SQLException
+		 *             if an SQL error occurs
+		 */
+		protected T run(ResultSet resultSet) throws SQLException {
+			return null;
+		}
+
+		/**
+		 * Processes the given prepared statement. The default implementation
+		 * creates a {@link ResultSet} with
+		 * {@link PreparedStatement#executeQuery()} and hands it over to
+		 * {@link #run(ResultSet)}, closing it afterwards.
+		 *
+		 * @param preparedStatement
+		 *            The prepared statement to process
+		 * @return The worker’s result
+		 * @throws SQLException
+		 *             if an SQL error occurs
+		 */
+		protected T run(PreparedStatement preparedStatement) throws SQLException {
+			ResultSet resultSet = null;
+			try {
+				resultSet = preparedStatement.executeQuery();
+				return run(resultSet);
+			} finally {
+				Closer.close(resultSet);
+			}
+		}
+
+		/**
+		 * Processes the given connection. The default implementation uses the
+		 * {@link #query} to {@link Query#createStatement(Connection) create a
+		 * prepared statement} and hand it over to
+		 * {@link #run(PreparedStatement)}, closing the statement afterwards.
+		 *
+		 * @param connection
+		 *            The connection to interact with
+		 * @return The worker’s result
+		 * @throws SQLException
+		 *             if an SQL error occurs
+		 */
+		protected T run(Connection connection) throws SQLException {
+			PreparedStatement preparedStatement = null;
+			try {
+				preparedStatement = query.createStatement(connection);
+				return run(preparedStatement);
+			} finally {
+				Closer.close(preparedStatement);
+			}
+		}
+
+		/**
+		 * Performs the work of this database worker. The default database
+		 * worker retrieves a connection from
+		 * {@link AbstractDatabase#getConnection()} and processes it with
+		 * {@link #run(Connection)},
+		 * {@link AbstractDatabase#returnConnection(Connection) returning} it
+		 * afterwards.
+		 *
+		 * @return The result of the worker
+		 * @throws DatabaseException
+		 *             if a database error occurs
+		 */
+		public T work() throws DatabaseException {
+			Connection connection = null;
+			try {
+				connection = getConnection();
+				return run(connection);
+			} catch (SQLException sqle1) {
+				throw new DatabaseException(sqle1);
+			} finally {
+				returnConnection(connection);
+			}
+		}
+
+	}
+
+	/**
+	 * A database worker that reads a single row from a result and creates an
+	 * object from it.
+	 *
+	 * @param <T>
+	 *            The type of the created object
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class SingleDatabaseWorker<T> extends AbstractDatabaseWorker<T> {
+
+		/** The object creator. */
+		private final ObjectCreator<T> objectCreator;
+
+		/**
+		 * Creates a new database worker that returns a single result.
+		 *
+		 * @param query
+		 *            The query to execute
+		 * @param objectCreator
+		 *            The object creator
+		 */
+		public SingleDatabaseWorker(Query query, ObjectCreator<T> objectCreator) {
+			super(query);
+			this.objectCreator = objectCreator;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		protected T run(ResultSet resultSet) throws SQLException {
+			if (resultSet.next()) {
+				return objectCreator.createObject(resultSet);
+			}
+			return null;
+		}
+
+	}
+
+	/**
+	 * Database worker that returns multiple results of a query.
+	 *
+	 * @param <T>
+	 *            The type of the results
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class MultipleDatabaseWorker<T> extends AbstractDatabaseWorker<List<T>> {
+
+		/** The object creator. */
+		private final ObjectCreator<T> objectCreator;
+
+		/**
+		 * Creates a new database worker that returns multiple results.
+		 *
+		 * @param query
+		 *            The query to execute
+		 * @param objectCreator
+		 *            The object creator
+		 */
+		public MultipleDatabaseWorker(Query query, ObjectCreator<T> objectCreator) {
+			super(query);
+			this.objectCreator = objectCreator;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		protected List<T> run(ResultSet resultSet) throws SQLException {
+			List<T> results = new ArrayList<T>();
+			while (resultSet.next()) {
+				T object = objectCreator.createObject(resultSet);
+				results.add(object);
+			}
+			return results;
+		}
+
+	}
+
+	/**
+	 * A database worker that executes an insert and returns the automatically
+	 * generated ID.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class InsertDatabaseWorker extends AbstractDatabaseWorker<Long> {
+
+		/**
+		 * Creates a new database worker that performs an insert.
+		 *
+		 * @param query
+		 *            The query to execute
+		 */
+		public InsertDatabaseWorker(Query query) {
+			super(query);
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.AbstractDatabase.AbstractDatabaseWorker#run(java.sql.Connection)
+		 */
+		@Override
+		protected Long run(PreparedStatement preparedStatement) throws SQLException {
+			preparedStatement.executeUpdate();
+			ResultSet generatedKeys = null;
+			try {
+				generatedKeys = preparedStatement.getGeneratedKeys();
+				if (generatedKeys.next()) {
+					return generatedKeys.getLong(1);
+				}
+				return -1L;
+			} finally {
+				Closer.close(generatedKeys);
+			}
+		}
+
+	}
+
+	/**
+	 * Database worker that performs an update and returns the number of updated
+	 * or changed rows.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class UpdateDatabaseWorker extends AbstractDatabaseWorker<Integer> {
+
+		/**
+		 * Creates a new database worker that performs an update.
+		 *
+		 * @param query
+		 *            The query to execute
+		 */
+		public UpdateDatabaseWorker(Query query) {
+			super(query);
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.AbstractDatabase.AbstractDatabaseWorker#run(java.sql.PreparedStatement)
+		 */
+		@Override
+		protected Integer run(PreparedStatement preparedStatement) throws SQLException {
+			return preparedStatement.executeUpdate();
+		}
+
+	}
+
+	/**
+	 * A database worker that processes all result rows with an
+	 * {@link ObjectCreator} instance which does not need to return meaningful
+	 * values.
+	 *
+	 * @author <a href="mailto:david.roden@sysart.de">David Roden</a>
+	 */
+	private class ProcessWorker extends AbstractDatabaseWorker<Void> {
+
+		/** The object creator used as result processor. */
+		private final ResultProcessor resultProcessor;
+
+		/**
+		 * Creates a new process worker.
+		 *
+		 * @param query
+		 *            The query to execute
+		 * @param resultProcessor
+		 *            The result processor
+		 */
+		public ProcessWorker(Query query, ResultProcessor resultProcessor) {
+			super(query);
+			this.resultProcessor = resultProcessor;
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.AbstractDatabase.AbstractDatabaseWorker#run(java.sql.ResultSet)
+		 */
+		@Override
+		protected Void run(ResultSet resultSet) throws SQLException {
+			while (resultSet.next()) {
+				resultProcessor.processResult(resultSet);
+			}
+			return null;
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/AndWhereClause.java b/alien/src/net/pterodactylus/util/database/AndWhereClause.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/AndWhereClause.java
@@ -0,0 +1,110 @@
+/*
+ * utils - AndWhereClause.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * {@link WhereClause} implementation that performs an AND conjunction of an
+ * arbitrary amount of where clauses.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class AndWhereClause implements WhereClause {
+
+	/** The list of where clauses. */
+	private final List<WhereClause> whereClauses = new ArrayList<WhereClause>();
+
+	/**
+	 * Creates a new AND where clause.
+	 */
+	public AndWhereClause() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Creates a new AND where clause.
+	 *
+	 * @param whereClauses
+	 *            The where clauses to connect
+	 */
+	public AndWhereClause(WhereClause... whereClauses) {
+		for (WhereClause whereClause : whereClauses) {
+			this.whereClauses.add(whereClause);
+		}
+	}
+
+	/**
+	 * Creates a new AND where clause.
+	 *
+	 * @param whereClauses
+	 *            The where clauses to connect
+	 */
+	public AndWhereClause(Collection<WhereClause> whereClauses) {
+		this.whereClauses.addAll(whereClauses);
+	}
+
+	/**
+	 * Adds the given WHERE clause.
+	 *
+	 * @param whereClause
+	 *            The WHERE clause to add
+	 * @return This WHERE clause (to allow method chaining)
+	 */
+	public AndWhereClause add(WhereClause whereClause) {
+		whereClauses.add(whereClause);
+		return this;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<Parameter<?>> getParameters() {
+		ArrayList<Parameter<?>> parameters = new ArrayList<Parameter<?>>();
+		for (WhereClause whereClause : whereClauses) {
+			parameters.addAll(whereClause.getParameters());
+		}
+		return parameters;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(Writer writer) throws IOException {
+		if (whereClauses.isEmpty()) {
+			writer.write("(1 = 1)");
+		}
+		boolean first = true;
+		for (WhereClause whereClause : whereClauses) {
+			if (!first) {
+				writer.write(" AND ");
+			}
+			writer.write("(");
+			whereClause.render(writer);
+			writer.write(")");
+			first = false;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/CountField.java b/alien/src/net/pterodactylus/util/database/CountField.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/CountField.java
@@ -0,0 +1,35 @@
+/*
+ * utils - CountField.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+/**
+ * Convenience {@link Field} implementation that creates a field that returns
+ * the count of all rows.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CountField extends Field {
+
+	/**
+	 * Creates a new count field.
+	 */
+	public CountField() {
+		super("COUNT(*)");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/DataObject.java b/alien/src/net/pterodactylus/util/database/DataObject.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/DataObject.java
@@ -0,0 +1,336 @@
+/*
+ * utils - DataObject.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.util.database.Parameter.LongParameter;
+import net.pterodactylus.util.database.Query.Type;
+
+/**
+ * A data object represents a data row from a database table. The
+ * {@link DataObject} class is intended to make it easier to read and write
+ * objects from and to a database. In order to achieve this some restrictions
+ * had to be posed upon the data object and its representation in the database.
+ * <ul>
+ * <li>A data object is required to have an ID of the type {@code long}.</li>
+ * <li>Every data object is required to have a corresponding entry in the
+ * database. It is not possible to create a data object in memory first and then
+ * save it to the database.</li>
+ * </ul>
+ *
+ * @param <D>
+ *            The type of the data object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class DataObject<D extends DataObject<D>> {
+
+	/** The data object factory that created this object. */
+	private final DataObjectFactory<D> dataObjectFactory;
+
+	/** The ID of the data object. */
+	private long id;
+
+	/** The properties of this data object. */
+	private final Map<String, Object> properties = new HashMap<String, Object>();
+
+	/** Whether the object has been written to. */
+	private boolean dirty;
+
+	/**
+	 * Creates a new data object.
+	 *
+	 * @param dataObjectFactory
+	 *            The data object factory that create this object
+	 * @param id
+	 *            The ID of the data object
+	 */
+	protected DataObject(DataObjectFactory<D> dataObjectFactory, long id) {
+		this.dataObjectFactory = dataObjectFactory;
+		this.id = id;
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Returns the ID of the data object.
+	 *
+	 * @return The ID of the data object
+	 */
+	public final long getId() {
+		return id;
+	}
+
+	//
+	// PROTECTED METHODS
+	//
+
+	/**
+	 * Sets the property with the given name to the given value.
+	 *
+	 * @param name
+	 *            The name of the property
+	 * @param value
+	 *            The value of the property
+	 */
+	protected void setProperty(String name, Object value) {
+		properties.put(name, value);
+		dirty = true;
+	}
+
+	/**
+	 * Returns the value of the property with the given name.
+	 *
+	 * @param name
+	 *            The name of the property
+	 * @return The value of the property, or {@code null} if the property does
+	 *         not exist
+	 */
+	protected Object getProperty(String name) {
+		return properties.get(name);
+	}
+
+	/**
+	 * Clears the dirty flag.
+	 */
+	protected void clearDirtyFlag() {
+		dirty = false;
+	}
+
+	//
+	// METHODS FOR SUBCLASSES TO OVERRIDE
+	//
+
+	/**
+	 * Retuns the fields that need to be saved when the object has changed.
+	 *
+	 * @return The fields that need to be saved
+	 */
+	protected abstract Set<ValueField> getSaveFields();
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Saves this data object to the database if it is dirty.
+	 *
+	 * @param database
+	 *            The database to store the object to
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public void save(Database database) throws DatabaseException {
+		save(database, false);
+	}
+
+	/**
+	 * Saves this data object to the database if it is dirty, or if the
+	 * {@code force} parameter is {@code true}.
+	 *
+	 * @param database
+	 *            The database to store the object to
+	 * @param force
+	 *            {@code true} to force saving to the database, {@code false} to
+	 *            not save if the object is not dirty
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public void save(Database database, boolean force) throws DatabaseException {
+		if (id != -1 && !dirty && !force) {
+			return;
+		}
+		if (id == -1) {
+			Query query = new Query(Type.INSERT, dataObjectFactory.getTable());
+			for (ValueField saveField : getSaveFields()) {
+				query.addValueField(saveField);
+			}
+			id = database.insert(query);
+			clearDirtyFlag();
+			return;
+		}
+		Query query = new Query(Type.UPDATE, dataObjectFactory.getTable());
+		for (ValueField saveField : getSaveFields()) {
+			query.addValueField(saveField);
+		}
+		query.addWhereClause(new ValueFieldWhereClause(new ValueField(dataObjectFactory.getIdentityColumn(), new LongParameter(id))));
+		database.update(query);
+		clearDirtyFlag();
+	}
+
+	/**
+	 * Deletes this data object from the database. This object should not be
+	 * used afterwards, especially the {@link #save(Database)} methods should
+	 * NOT be called!
+	 *
+	 * @param database
+	 *            The database to delete the object from
+	 * @return {@code true} if the object was deleted, {@code false} otherwise
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public boolean delete(Database database) throws DatabaseException {
+		return DataObject.deleteById(database, dataObjectFactory, getId());
+	}
+
+	//
+	// STATIC METHODS
+	//
+
+	/**
+	 * Loads a data object from the given database by the given ID.
+	 *
+	 * @param database
+	 *            The database to load the object from
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param id
+	 *            The ID to load
+	 * @param <D>
+	 *            The type of the object to load
+	 * @return The loaded object, or {@code null} if there was no object with
+	 *         the given ID
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> D loadById(Database database, DataObjectFactory<D> dataObjectFactory, long id) throws DatabaseException {
+		return loadByWhereClause(database, dataObjectFactory, new ValueFieldWhereClause(new ValueField(dataObjectFactory.getIdentityColumn(), new LongParameter(id))));
+	}
+
+	/**
+	 * Loads the first data object that matches the given where clause from the
+	 * given database.
+	 *
+	 * @param database
+	 *            The database to load the object from
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param whereClause
+	 *            The where clause to match the object
+	 * @param <D>
+	 *            The type of the object to load
+	 * @param orderFields
+	 *            The fields to order the results by
+	 * @return The first object that matched the wher clause, or {@code null} if
+	 *         there was no object that matched
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> D loadByWhereClause(Database database, DataObjectFactory<D> dataObjectFactory, WhereClause whereClause, OrderField... orderFields) throws DatabaseException {
+		Query query = new Query(Type.SELECT, dataObjectFactory.getTable());
+		query.addWhereClause(whereClause);
+		for (OrderField orderField : orderFields) {
+			query.addOrderField(orderField);
+		}
+		return database.getSingle(query, dataObjectFactory);
+	}
+
+	/**
+	 * Loads all data objects that match the given where clause from the given
+	 * database.
+	 *
+	 * @param database
+	 *            The database to load the objects from
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param whereClause
+	 *            The where clause to match the objects
+	 * @param <D>
+	 *            The type of the objects to load
+	 * @param orderFields
+	 *            The order fields to sort the results by
+	 * @return All objects that matched the wher clause, or an empty list if
+	 *         there was no object that matched
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> List<D> loadAllByWhereClause(Database database, DataObjectFactory<D> dataObjectFactory, WhereClause whereClause, OrderField... orderFields) throws DatabaseException {
+		Query query = new Query(Type.SELECT, dataObjectFactory.getTable());
+		query.addWhereClause(whereClause);
+		query.addOrderField(orderFields);
+		return database.getMultiple(query, dataObjectFactory);
+	}
+
+	/**
+	 * Creates a new data object with the given value fields.
+	 *
+	 * @param <D>
+	 *            The type of the data object
+	 * @param database
+	 *            The database to create the object in
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param valueFields
+	 *            The value fields
+	 * @return The created object
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> D create(Database database, DataObjectFactory<D> dataObjectFactory, ValueField... valueFields) throws DatabaseException {
+		Query query = new Query(Type.INSERT, dataObjectFactory.getTable());
+		query.addValueField(valueFields);
+		long id = database.insert(query);
+		return loadById(database, dataObjectFactory, id);
+	}
+
+	/**
+	 * Deletes the data object with the given ID from the database.
+	 *
+	 * @param <D>
+	 *            The type of the data object
+	 * @param database
+	 *            The database to delete the object from
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param id
+	 *            The ID of the object to delete
+	 * @return {@code true} if an object was deleted, {@code false} otherwise
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> boolean deleteById(Database database, DataObjectFactory<D> dataObjectFactory, long id) throws DatabaseException {
+		return deleteByWhereClause(database, dataObjectFactory, new ValueFieldWhereClause(new ValueField(dataObjectFactory.getIdentityColumn(), new LongParameter(id)))) == 1;
+	}
+
+	/**
+	 * Deletes all objects from the database that match the given WHERE clause.
+	 *
+	 * @param <D>
+	 *            The type of the data object
+	 * @param database
+	 *            The database to delete the objects from
+	 * @param dataObjectFactory
+	 *            The data object factory
+	 * @param whereClause
+	 *            The WHERE clause to match for deletion
+	 * @return The number of deleted objects
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public static <D extends DataObject<D>> int deleteByWhereClause(Database database, DataObjectFactory<D> dataObjectFactory, WhereClause whereClause) throws DatabaseException {
+		Query query = new Query(Type.DELETE, dataObjectFactory.getTable());
+		query.addWhereClause(whereClause);
+		return database.update(query);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/DataObjectFactory.java b/alien/src/net/pterodactylus/util/database/DataObjectFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/DataObjectFactory.java
@@ -0,0 +1,45 @@
+/*
+ * utils - DataObjectFactory.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+
+/**
+ * The data object factory knows details about the specifics of how a
+ * {@link DataObject} is persisted to the database.
+ *
+ * @param <D>
+ *            The type of the data object to create
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface DataObjectFactory<D extends DataObject<D>> extends ObjectCreator<D> {
+
+	/**
+	 * Returns the table the data object is stored in.
+	 *
+	 * @return The name of the table
+	 */
+	public String getTable();
+
+	/**
+	 * Returns the name of the column that holds the identity.
+	 *
+	 * @return The name of the identity column
+	 */
+	public String getIdentityColumn();
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/Database.java b/alien/src/net/pterodactylus/util/database/Database.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/Database.java
@@ -0,0 +1,97 @@
+/*
+ * utils - Database.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.util.List;
+
+/**
+ * Interface for the database abstraction. This interface holds methods that
+ * allow defined access to a database.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Database {
+
+	/**
+	 * Returns a single object from a database.
+	 *
+	 * @param <T>
+	 *            The type of the object
+	 * @param query
+	 *            The query to execute
+	 * @param objectCreator
+	 *            The object creator
+	 * @return The created object, or {@code null} if the {@code query} did not
+	 *         yield any results
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public <T> T getSingle(Query query, ObjectCreator<T> objectCreator) throws DatabaseException;
+
+	/**
+	 * Returns multiple query results.
+	 *
+	 * @param <T>
+	 *            The type of the results
+	 * @param query
+	 *            The query to execute
+	 * @param objectCreator
+	 *            The object creator
+	 * @return A list containing the objects from the database
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public <T> List<T> getMultiple(Query query, ObjectCreator<T> objectCreator) throws DatabaseException;
+
+	/**
+	 * Inserts an object into the database and returns the automatically
+	 * generated ID, if any.
+	 *
+	 * @param query
+	 *            The query to execute
+	 * @return The automatically generated ID, or {@code -1} if no ID was
+	 *         generated
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public long insert(Query query) throws DatabaseException;
+
+	/**
+	 * Updates objects in the database.
+	 *
+	 * @param query
+	 *            The query to execute
+	 * @return The number of changed objects in the database
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public int update(Query query) throws DatabaseException;
+
+	/**
+	 * Processes the results of the given query with the given result processor.
+	 *
+	 * @param query
+	 *            The query to execute
+	 * @param resultProcessor
+	 *            The result processor used to process the result set
+	 * @throws DatabaseException
+	 *             if a database error occurs
+	 */
+	public void process(Query query, ResultProcessor resultProcessor) throws DatabaseException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/DatabaseException.java b/alien/src/net/pterodactylus/util/database/DatabaseException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/DatabaseException.java
@@ -0,0 +1,66 @@
+/*
+ * utils - DatabaseException.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+/**
+ * Exception that signals a database error.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DatabaseException extends Exception {
+
+	/**
+	 * Creates a new database exception.
+	 */
+	public DatabaseException() {
+		// TODO Auto-generated constructor stub
+	}
+
+	/**
+	 * Creates a new database exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public DatabaseException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Creates a new database exception.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public DatabaseException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * Creates a new database exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public DatabaseException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/Field.java b/alien/src/net/pterodactylus/util/database/Field.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/Field.java
@@ -0,0 +1,69 @@
+/*
+ * utils - Field.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+/**
+ * A field stores the name of a database column.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Field {
+
+	/** The name of the field. */
+	private final String name;
+
+	/**
+	 * Creates a new field.
+	 *
+	 * @param name
+	 *            The name of the field
+	 */
+	public Field(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * Returns the name of the field.
+	 *
+	 * @return The name of the field
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean equals(Object object) {
+		if (!(object instanceof Field)) {
+			return false;
+		}
+		Field field = (Field) object;
+		return field.name.equals(name);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int hashCode() {
+		return name.hashCode();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/Limit.java b/alien/src/net/pterodactylus/util/database/Limit.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/Limit.java
@@ -0,0 +1,75 @@
+/*
+ * utils - Limit.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+/**
+ * A LIMIT clause limits the results that are returned.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Limit {
+
+	/** The index of the first result to retrieve, 0-based. */
+	private final long start;
+
+	/** The number of results to retrieve. */
+	private final long number;
+
+	/**
+	 * Creates a new limit clause that retrieves the given number of results.
+	 *
+	 * @param number
+	 *            The number of results to retrieve
+	 */
+	public Limit(long number) {
+		this(0, number);
+	}
+
+	/**
+	 * Creates a new limit clause that retrieves the given number of results,
+	 * starting at the given index.
+	 *
+	 * @param start
+	 *            The index of the first result to retrieve
+	 * @param number
+	 *            The number of results to retrieve
+	 */
+	public Limit(long start, long number) {
+		this.start = start;
+		this.number = number;
+	}
+
+	/**
+	 * Returns the index of the first result to retrieve.
+	 *
+	 * @return The index of the first result to retrieve
+	 */
+	public long getStart() {
+		return start;
+	}
+
+	/**
+	 * Returns the number of results to retreive.
+	 *
+	 * @return The number of results to retrieve
+	 */
+	public long getNumber() {
+		return number;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/MapCreator.java b/alien/src/net/pterodactylus/util/database/MapCreator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/MapCreator.java
@@ -0,0 +1,75 @@
+/*
+ * utils - MapCreator.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link ObjectCreator} implementation that creates a {@link Map} from the
+ * current row of a {@link ResultSet}. The display names of the columns are used
+ * as keys, the values are mapped to objects of a fitting type.
+ * <p>
+ * The following {@link Types} are currently mapped:
+ * <ul>
+ * <li>{@link Types#INTEGER} is mapped to {@link Integer}.</li>
+ * <li>{@link Types#BIGINT} is mapped to {@link Long}.</li>
+ * <li>{@link Types#BOOLEAN} is mapped to {@link Boolean}.</li>
+ * <li>{@link Types#VARCHAR} is mapped to {@link String}.</li>
+ * </ul>
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MapCreator implements ObjectCreator<Map<String, Object>> {
+
+	/**
+	 * @see net.pterodactylus.util.database.ObjectCreator#createObject(java.sql.ResultSet)
+	 */
+	@Override
+	public Map<String, Object> createObject(ResultSet resultSet) throws SQLException {
+		Map<String, Object> result = new HashMap<String, Object>();
+		ResultSetMetaData metadata = resultSet.getMetaData();
+		for (int column = 1; column <= metadata.getColumnCount(); column++) {
+			int columnType = metadata.getColumnType(column);
+			String columnLabel = metadata.getColumnLabel(column);
+			if (columnType == Types.INTEGER) {
+				result.put(columnLabel, resultSet.getInt(column));
+			} else if (columnType == Types.BIGINT) {
+				result.put(columnLabel, resultSet.getLong(column));
+			} else if (columnType == Types.DECIMAL) {
+				result.put(columnLabel, resultSet.getDouble(column));
+			} else if (columnType == Types.BOOLEAN) {
+				result.put(columnLabel, resultSet.getBoolean(column));
+			} else if (columnType == Types.VARCHAR) {
+				result.put(columnLabel, resultSet.getString(column));
+			} else if (columnType == Types.LONGVARCHAR) {
+				result.put(columnLabel, resultSet.getString(column));
+			} else if (columnType == Types.DATE) {
+				result.put(columnLabel, resultSet.getDate(column));
+			} else {
+				System.out.println("unknown type (" + columnType + ") for column “" + columnLabel + "”.");
+			}
+		}
+		return result;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/MatchAllWhereClause.java b/alien/src/net/pterodactylus/util/database/MatchAllWhereClause.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/MatchAllWhereClause.java
@@ -0,0 +1,48 @@
+/*
+ * utils - MatchAllWhereClause.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * TODO
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MatchAllWhereClause implements WhereClause {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<Parameter<?>> getParameters() {
+		return Collections.emptyList();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(Writer writer) throws IOException {
+		writer.write("(1 = 1)");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/ObjectCreator.java b/alien/src/net/pterodactylus/util/database/ObjectCreator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/ObjectCreator.java
@@ -0,0 +1,134 @@
+/*
+ * utils - ObjectCreator.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * An <em>object creator</em> is able to create a new object of type {@code T}
+ * from the current row of a {@link ResultSet}.
+ *
+ * @param <T>
+ *            The type of the created object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ObjectCreator<T> {
+
+	/** A default integer creator. */
+	public static final IntegerCreator INTEGER_CREATOR = new IntegerCreator();
+
+	/** A default string creator. */
+	public static final StringCreator STRING_CREATOR = new StringCreator();
+
+	/**
+	 * Creates a new object from the current row in the given result set.
+	 *
+	 * @param resultSet
+	 *            The result set to create a new object from
+	 * @return {@code null} if the result set is not on a valid row, i.e. if the
+	 *         result set is empty or there are no more rows left in it
+	 * @throws SQLException
+	 *             if an SQL error occurs
+	 */
+	public T createObject(ResultSet resultSet) throws SQLException;
+
+	/**
+	 * Creates a single integer from a result set. When the column contained the
+	 * SQL NULL value, {@code null} is returned instead of {@code 0}; this
+	 * differs from {@link ResultSet#getInt(int)}.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class IntegerCreator implements ObjectCreator<Integer> {
+
+		/** The index of the value. */
+		private final int index;
+
+		/**
+		 * Creates a new object creator that returns the integer value of the
+		 * first column of the result set.
+		 */
+		public IntegerCreator() {
+			this(1);
+		}
+
+		/**
+		 * Creates a new object creator that returns the integer value of the
+		 * given column.
+		 *
+		 * @param index
+		 *            The index of the column to return
+		 */
+		public IntegerCreator(int index) {
+			this.index = index;
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.ObjectCreator#createObject(java.sql.ResultSet)
+		 */
+		@Override
+		public Integer createObject(ResultSet resultSet) throws SQLException {
+			Integer result = resultSet.getInt(index);
+			if (resultSet.wasNull()) {
+				return null;
+			}
+			return result;
+		}
+
+	}
+
+	/**
+	 * Creates a single {@link String} from a result set.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class StringCreator implements ObjectCreator<String> {
+
+		/** The index of the value. */
+		private final int index;
+
+		/**
+		 * Creates a new object creator that returns the value of the result
+		 * set’s first column as a {@link String}.
+		 */
+		public StringCreator() {
+			this(1);
+		}
+
+		/**
+		 * Creates a new object creator that returns the value of the result
+		 * set’s given column as a {@link String}.
+		 *
+		 * @param index
+		 *            The index of the column
+		 */
+		public StringCreator(int index) {
+			this.index = index;
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.ObjectCreator#createObject(java.sql.ResultSet)
+		 */
+		@Override
+		public String createObject(ResultSet resultSet) throws SQLException {
+			return resultSet.getString(index);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/OrderField.java b/alien/src/net/pterodactylus/util/database/OrderField.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/OrderField.java
@@ -0,0 +1,90 @@
+/*
+ * utils - OrderField.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+/**
+ * An order field stores the name of the field to order a query result by and
+ * the direction of the sorting.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class OrderField {
+
+	/**
+	 * Defines the possible sort orders.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public enum Order {
+
+		/** Sort the results in ascending (smallest first) order. */
+		ASCENDING,
+
+		/** Sort the results in descending (largest first) order. */
+		DESCENDING
+
+	}
+
+	/** The field to sort by. */
+	private final Field field;
+
+	/** The sort direction. */
+	private final Order order;
+
+	/**
+	 * Creates a new order field, sorting ascending by the given field.
+	 *
+	 * @param field
+	 *            The field to sort by
+	 */
+	public OrderField(Field field) {
+		this(field, Order.ASCENDING);
+	}
+
+	/**
+	 * Creates a new order field, sorting in the given order by the given field.
+	 *
+	 * @param field
+	 *            The field to sort by
+	 * @param order
+	 *            The sort direction
+	 */
+	public OrderField(Field field, Order order) {
+		this.field = field;
+		this.order = order;
+	}
+
+	/**
+	 * Returns the name of the field to sort by.
+	 *
+	 * @return The name of the field to sort by
+	 */
+	public Field getField() {
+		return field;
+	}
+
+	/**
+	 * Returns the order of the sort.
+	 *
+	 * @return The sort order
+	 */
+	public Order getOrder() {
+		return order;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/PairCreator.java b/alien/src/net/pterodactylus/util/database/PairCreator.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/PairCreator.java
@@ -0,0 +1,64 @@
+/*
+ * utils - PairCreator.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import net.pterodactylus.util.collection.Pair;
+
+/**
+ * Object creator implementation that creates a pair from two other object
+ * creators.
+ *
+ * @param <L>
+ *            The type of the left object
+ * @param <R>
+ *            The type of the right object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PairCreator<L, R> implements ObjectCreator<Pair<L, R>> {
+
+	/** The object creator for the left object. */
+	private final ObjectCreator<L> leftObjectCreator;
+
+	/** The object creator for the right object. */
+	private final ObjectCreator<R> rightObjectCreator;
+
+	/**
+	 * Creates a new pair object creator.
+	 *
+	 * @param leftObjectCreator
+	 *            The left object creator
+	 * @param rightObjectCreator
+	 *            The right object creator
+	 */
+	public PairCreator(ObjectCreator<L> leftObjectCreator, ObjectCreator<R> rightObjectCreator) {
+		this.leftObjectCreator = leftObjectCreator;
+		this.rightObjectCreator = rightObjectCreator;
+	}
+
+	/**
+	 * @see net.pterodactylus.util.database.ObjectCreator#createObject(java.sql.ResultSet)
+	 */
+	@Override
+	public Pair<L, R> createObject(ResultSet resultSet) throws SQLException {
+		return new Pair<L, R>(leftObjectCreator.createObject(resultSet), rightObjectCreator.createObject(resultSet));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/Parameter.java b/alien/src/net/pterodactylus/util/database/Parameter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/Parameter.java
@@ -0,0 +1,186 @@
+/*
+ * utils - Parameter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+/**
+ * A {@code Parameter} stores a parameter value in its native type and can set
+ * it on a {@link PreparedStatement} using correct type information.
+ *
+ * @param <T>
+ *            The type of the parameter value
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class Parameter<T> {
+
+	/** The value of the parameter. */
+	protected final T value;
+
+	/**
+	 * Creates a new parameter.
+	 *
+	 * @param value
+	 *            The value of the parameter
+	 */
+	protected Parameter(T value) {
+		this.value = value;
+	}
+
+	/**
+	 * Sets the value on the prepared statement.
+	 *
+	 * @param preparedStatement
+	 *            The prepared statement to set the value on
+	 * @param index
+	 *            The index of the parameter
+	 * @throws SQLException
+	 *             if an SQL error occurs
+	 */
+	public abstract void set(PreparedStatement preparedStatement, int index) throws SQLException;
+
+	/**
+	 * {@link Parameter} implemetation for a {@link Boolean} value that supports
+	 * NULL values.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class BooleanParameter extends Parameter<Boolean> {
+
+		/**
+		 * Creates a new parameter.
+		 *
+		 * @param value
+		 *            The value of the parameter (may be {@code null})
+		 */
+		public BooleanParameter(Boolean value) {
+			super(value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(PreparedStatement preparedStatement, int index) throws SQLException {
+			if (value == null) {
+				preparedStatement.setNull(index, Types.BOOLEAN);
+			} else {
+				preparedStatement.setBoolean(index, value);
+			}
+		}
+
+	}
+
+	/**
+	 * {@link Parameter} implemetation for a {@link Integer} value that supports
+	 * NULL values.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class IntegerParameter extends Parameter<Integer> {
+
+		/**
+		 * Creates a new parameter.
+		 *
+		 * @param value
+		 *            The value of the parameter (may be {@code null})
+		 */
+		public IntegerParameter(Integer value) {
+			super(value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(PreparedStatement preparedStatement, int index) throws SQLException {
+			if (value == null) {
+				preparedStatement.setNull(index, Types.INTEGER);
+			} else {
+				preparedStatement.setInt(index, value);
+			}
+		}
+
+	}
+
+	/**
+	 * {@link Parameter} implemetation for a {@link Long} value that supports
+	 * NULL values.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class LongParameter extends Parameter<Long> {
+
+		/**
+		 * Creates a new parameter.
+		 *
+		 * @param value
+		 *            The value of the parameter (may be {@code null})
+		 */
+		public LongParameter(Long value) {
+			super(value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(PreparedStatement preparedStatement, int index) throws SQLException {
+			if (value == null) {
+				preparedStatement.setNull(index, Types.BIGINT);
+			} else {
+				preparedStatement.setLong(index, value);
+			}
+		}
+
+	}
+
+	/**
+	 * {@link Parameter} implemetation for a {@link String} value that supports
+	 * NULL values.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class StringParameter extends Parameter<String> {
+
+		/**
+		 * Creates a new parameter.
+		 *
+		 * @param value
+		 *            The value of the parameter (may be {@code null})
+		 */
+		public StringParameter(String value) {
+			super(value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(PreparedStatement preparedStatement, int index) throws SQLException {
+			if (value == null) {
+				preparedStatement.setNull(index, Types.VARCHAR);
+			} else {
+				preparedStatement.setString(index, value);
+			}
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/PooledDataSource.java b/alien/src/net/pterodactylus/util/database/PooledDataSource.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/PooledDataSource.java
@@ -0,0 +1,662 @@
+/*
+ * utils - PooledDataSource.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.PrintWriter;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.SQLClientInfoException;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.sql.DataSource;
+
+import net.pterodactylus.util.collection.Pair;
+
+/**
+ * A data source that uses an internal pool of opened connections. Whenever a
+ * connection is requested with {@link #getConnection()} or
+ * {@link #getConnection(String, String)} a new connection is created if there
+ * is none available. This will allow the pool to grow on demand.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PooledDataSource implements DataSource {
+
+	/** The original data source. */
+	private DataSource originalDataSource;
+
+	/** The pool of currently opened connections. */
+	private Set<Connection> currentConnections = new HashSet<Connection>();
+
+	/**
+	 * The pool of currently opened connections with custom username/password
+	 * combinations.
+	 */
+	private Map<Pair<String, String>, Set<Connection>> usernamePasswordConnections = new HashMap<Pair<String, String>, Set<Connection>>();
+
+	/**
+	 * Creates a new pooled data source that wraps the given data source.
+	 *
+	 * @param originalDataSource
+	 *            The original data source
+	 */
+	public PooledDataSource(DataSource originalDataSource) {
+		this.originalDataSource = originalDataSource;
+	}
+
+	/**
+	 * @see javax.sql.DataSource#getConnection()
+	 */
+	@Override
+	public Connection getConnection() throws SQLException {
+		synchronized (currentConnections) {
+			if (currentConnections.isEmpty()) {
+				Connection newConnection = new PooledConnection(originalDataSource.getConnection());
+				currentConnections.add(newConnection);
+			}
+			Connection connection = currentConnections.iterator().next();
+			currentConnections.remove(connection);
+			return connection;
+		}
+	}
+
+	/**
+	 * @see javax.sql.DataSource#getConnection(java.lang.String,
+	 *      java.lang.String)
+	 */
+	@Override
+	public Connection getConnection(String username, String password) throws SQLException {
+		Pair<String, String> usernamePasswordPair = new Pair<String, String>(username, password);
+		synchronized (usernamePasswordConnections) {
+			if (!usernamePasswordConnections.containsKey(usernamePasswordPair)) {
+				Set<Connection> connections = new HashSet<Connection>();
+				usernamePasswordConnections.put(usernamePasswordPair, connections);
+			}
+			Set<Connection> connections = usernamePasswordConnections.get(usernamePasswordPair);
+			if (usernamePasswordConnections.isEmpty()) {
+				Connection newConnection = new PooledUsernamePasswordConnection(originalDataSource.getConnection(username, password), username, password);
+				connections.add(newConnection);
+			}
+			Connection connection = connections.iterator().next();
+			connections.remove(connection);
+			return connection;
+		}
+	}
+
+	/**
+	 * @see javax.sql.CommonDataSource#getLogWriter()
+	 */
+	@Override
+	public PrintWriter getLogWriter() throws SQLException {
+		return originalDataSource.getLogWriter();
+	}
+
+	/**
+	 * @see javax.sql.CommonDataSource#getLoginTimeout()
+	 */
+	@Override
+	public int getLoginTimeout() throws SQLException {
+		return originalDataSource.getLoginTimeout();
+	}
+
+	/**
+	 * @see javax.sql.CommonDataSource#setLogWriter(java.io.PrintWriter)
+	 */
+	@Override
+	public void setLogWriter(PrintWriter out) throws SQLException {
+		originalDataSource.setLogWriter(out);
+	}
+
+	/**
+	 * @see javax.sql.CommonDataSource#setLoginTimeout(int)
+	 */
+	@Override
+	public void setLoginTimeout(int seconds) throws SQLException {
+		originalDataSource.setLoginTimeout(seconds);
+	}
+
+	/**
+	 * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
+	 */
+	@Override
+	public boolean isWrapperFor(Class<?> iface) throws SQLException {
+		return originalDataSource.isWrapperFor(iface);
+	}
+
+	/**
+	 * @see java.sql.Wrapper#unwrap(java.lang.Class)
+	 */
+	@Override
+	public <T> T unwrap(Class<T> iface) throws SQLException {
+		return originalDataSource.unwrap(iface);
+	}
+
+	/**
+	 * Wrapper around a connection that was created by the
+	 * {@link PooledDataSource#originalDataSource}. This wrapper only overrides
+	 * the {@link Connection#close()} method to not close the connection but to
+	 * return it to the connection pool instead.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class PooledConnection implements Connection {
+
+		/** The original connection. */
+		private final Connection originalConnection;
+
+		/**
+		 * Creates a new pooled connection.
+		 *
+		 * @param originalConnection
+		 *            The original connection to wrap
+		 */
+		public PooledConnection(Connection originalConnection) {
+			this.originalConnection = originalConnection;
+		}
+
+		//
+		// PROTECTED METHODS
+		//
+
+		/**
+		 * Returns the original connection that is wrapped by this pooled
+		 * connection.
+		 *
+		 * @return The original connection
+		 */
+		protected Connection getOriginalConnection() {
+			return originalConnection;
+		}
+
+		//
+		// INTERFACE Connection
+		//
+
+		/**
+		 * @see java.sql.Connection#clearWarnings()
+		 */
+		@Override
+		public void clearWarnings() throws SQLException {
+			originalConnection.clearWarnings();
+		}
+
+		/**
+		 * @see java.sql.Connection#close()
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		public void close() throws SQLException {
+			if (!isValid(1)) {
+				originalConnection.close();
+				return;
+			}
+			synchronized (currentConnections) {
+				currentConnections.add(this);
+			}
+		}
+
+		/**
+		 * @see java.sql.Connection#commit()
+		 */
+		@Override
+		public void commit() throws SQLException {
+			originalConnection.commit();
+		}
+
+		/**
+		 * @see java.sql.Connection#createArrayOf(java.lang.String,
+		 *      java.lang.Object[])
+		 */
+		@Override
+		public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
+			return originalConnection.createArrayOf(typeName, elements);
+		}
+
+		/**
+		 * @see java.sql.Connection#createBlob()
+		 */
+		@Override
+		public Blob createBlob() throws SQLException {
+			return originalConnection.createBlob();
+		}
+
+		/**
+		 * @see java.sql.Connection#createClob()
+		 */
+		@Override
+		public Clob createClob() throws SQLException {
+			return originalConnection.createClob();
+		}
+
+		/**
+		 * @see java.sql.Connection#createNClob()
+		 */
+		@Override
+		public NClob createNClob() throws SQLException {
+			return originalConnection.createNClob();
+		}
+
+		/**
+		 * @see java.sql.Connection#createSQLXML()
+		 */
+		@Override
+		public SQLXML createSQLXML() throws SQLException {
+			return originalConnection.createSQLXML();
+		}
+
+		/**
+		 * @see java.sql.Connection#createStatement()
+		 */
+		@Override
+		public Statement createStatement() throws SQLException {
+			return originalConnection.createStatement();
+		}
+
+		/**
+		 * @see java.sql.Connection#createStatement(int, int, int)
+		 */
+		@Override
+		public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+			return originalConnection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
+		}
+
+		/**
+		 * @see java.sql.Connection#createStatement(int, int)
+		 */
+		@Override
+		public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
+			return originalConnection.createStatement(resultSetType, resultSetConcurrency);
+		}
+
+		/**
+		 * @see java.sql.Connection#createStruct(java.lang.String,
+		 *      java.lang.Object[])
+		 */
+		@Override
+		public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
+			return originalConnection.createStruct(typeName, attributes);
+		}
+
+		/**
+		 * @see java.sql.Connection#getAutoCommit()
+		 */
+		@Override
+		public boolean getAutoCommit() throws SQLException {
+			return originalConnection.getAutoCommit();
+		}
+
+		/**
+		 * @see java.sql.Connection#getCatalog()
+		 */
+		@Override
+		public String getCatalog() throws SQLException {
+			return originalConnection.getCatalog();
+		}
+
+		/**
+		 * @see java.sql.Connection#getClientInfo()
+		 */
+		@Override
+		public Properties getClientInfo() throws SQLException {
+			return originalConnection.getClientInfo();
+		}
+
+		/**
+		 * @see java.sql.Connection#getClientInfo(java.lang.String)
+		 */
+		@Override
+		public String getClientInfo(String name) throws SQLException {
+			return originalConnection.getClientInfo(name);
+		}
+
+		/**
+		 * @see java.sql.Connection#getHoldability()
+		 */
+		@Override
+		public int getHoldability() throws SQLException {
+			return originalConnection.getHoldability();
+		}
+
+		/**
+		 * @see java.sql.Connection#getMetaData()
+		 */
+		@Override
+		public DatabaseMetaData getMetaData() throws SQLException {
+			return originalConnection.getMetaData();
+		}
+
+		/**
+		 * @see java.sql.Connection#getTransactionIsolation()
+		 */
+		@Override
+		public int getTransactionIsolation() throws SQLException {
+			return originalConnection.getTransactionIsolation();
+		}
+
+		/**
+		 * @see java.sql.Connection#getTypeMap()
+		 */
+		@Override
+		public Map<String, Class<?>> getTypeMap() throws SQLException {
+			return originalConnection.getTypeMap();
+		}
+
+		/**
+		 * @see java.sql.Connection#getWarnings()
+		 */
+		@Override
+		public SQLWarning getWarnings() throws SQLException {
+			return originalConnection.getWarnings();
+		}
+
+		/**
+		 * @see java.sql.Connection#isClosed()
+		 */
+		@Override
+		public boolean isClosed() throws SQLException {
+			return originalConnection.isClosed();
+		}
+
+		/**
+		 * @see java.sql.Connection#isReadOnly()
+		 */
+		@Override
+		public boolean isReadOnly() throws SQLException {
+			return originalConnection.isReadOnly();
+		}
+
+		/**
+		 * @see java.sql.Connection#isValid(int)
+		 */
+		@Override
+		public boolean isValid(int timeout) throws SQLException {
+			return originalConnection.isValid(timeout);
+		}
+
+		/**
+		 * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
+		 */
+		@Override
+		public boolean isWrapperFor(Class<?> iface) throws SQLException {
+			return originalConnection.isWrapperFor(iface);
+		}
+
+		/**
+		 * @see java.sql.Connection#nativeSQL(java.lang.String)
+		 */
+		@Override
+		public String nativeSQL(String sql) throws SQLException {
+			return originalConnection.nativeSQL(sql);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareCall(java.lang.String, int, int, int)
+		 */
+		@Override
+		public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+			return originalConnection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareCall(java.lang.String, int, int)
+		 */
+		@Override
+		public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+			return originalConnection.prepareCall(sql, resultSetType, resultSetConcurrency);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareCall(java.lang.String)
+		 */
+		@Override
+		public CallableStatement prepareCall(String sql) throws SQLException {
+			return originalConnection.prepareCall(sql);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String, int, int,
+		 *      int)
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+			return originalConnection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String, int, int)
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+			return originalConnection.prepareStatement(sql, resultSetType, resultSetConcurrency);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String, int)
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
+			return originalConnection.prepareStatement(sql, autoGeneratedKeys);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String, int[])
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
+			return originalConnection.prepareStatement(sql, columnIndexes);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String,
+		 *      java.lang.String[])
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
+			return originalConnection.prepareStatement(sql, columnNames);
+		}
+
+		/**
+		 * @see java.sql.Connection#prepareStatement(java.lang.String)
+		 */
+		@Override
+		public PreparedStatement prepareStatement(String sql) throws SQLException {
+			return originalConnection.prepareStatement(sql);
+		}
+
+		/**
+		 * @see java.sql.Connection#releaseSavepoint(java.sql.Savepoint)
+		 */
+		@Override
+		public void releaseSavepoint(Savepoint savepoint) throws SQLException {
+			originalConnection.releaseSavepoint(savepoint);
+		}
+
+		/**
+		 * @see java.sql.Connection#rollback()
+		 */
+		@Override
+		public void rollback() throws SQLException {
+			originalConnection.rollback();
+		}
+
+		/**
+		 * @see java.sql.Connection#rollback(java.sql.Savepoint)
+		 */
+		@Override
+		public void rollback(Savepoint savepoint) throws SQLException {
+			originalConnection.rollback(savepoint);
+		}
+
+		/**
+		 * @see java.sql.Connection#setAutoCommit(boolean)
+		 */
+		@Override
+		public void setAutoCommit(boolean autoCommit) throws SQLException {
+			originalConnection.setAutoCommit(autoCommit);
+		}
+
+		/**
+		 * @see java.sql.Connection#setCatalog(java.lang.String)
+		 */
+		@Override
+		public void setCatalog(String catalog) throws SQLException {
+			originalConnection.setCatalog(catalog);
+		}
+
+		/**
+		 * @see java.sql.Connection#setClientInfo(java.util.Properties)
+		 */
+		@Override
+		public void setClientInfo(Properties properties) throws SQLClientInfoException {
+			originalConnection.setClientInfo(properties);
+		}
+
+		/**
+		 * @see java.sql.Connection#setClientInfo(java.lang.String,
+		 *      java.lang.String)
+		 */
+		@Override
+		public void setClientInfo(String name, String value) throws SQLClientInfoException {
+			originalConnection.setClientInfo(name, value);
+		}
+
+		/**
+		 * @see java.sql.Connection#setHoldability(int)
+		 */
+		@Override
+		public void setHoldability(int holdability) throws SQLException {
+			originalConnection.setHoldability(holdability);
+		}
+
+		/**
+		 * @see java.sql.Connection#setReadOnly(boolean)
+		 */
+		@Override
+		public void setReadOnly(boolean readOnly) throws SQLException {
+			originalConnection.setReadOnly(readOnly);
+		}
+
+		/**
+		 * @see java.sql.Connection#setSavepoint()
+		 */
+		@Override
+		public Savepoint setSavepoint() throws SQLException {
+			return originalConnection.setSavepoint();
+		}
+
+		/**
+		 * @see java.sql.Connection#setSavepoint(java.lang.String)
+		 */
+		@Override
+		public Savepoint setSavepoint(String name) throws SQLException {
+			return originalConnection.setSavepoint(name);
+		}
+
+		/**
+		 * @see java.sql.Connection#setTransactionIsolation(int)
+		 */
+		@Override
+		public void setTransactionIsolation(int level) throws SQLException {
+			originalConnection.setTransactionIsolation(level);
+		}
+
+		/**
+		 * @see java.sql.Connection#setTypeMap(java.util.Map)
+		 */
+		@Override
+		public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+			originalConnection.setTypeMap(map);
+		}
+
+		/**
+		 * @see java.sql.Wrapper#unwrap(java.lang.Class)
+		 */
+		@Override
+		public <T> T unwrap(Class<T> iface) throws SQLException {
+			return originalConnection.unwrap(iface);
+		}
+
+	}
+
+	/**
+	 * Wrapper around a connection that was created with a username and a
+	 * password. This wrapper also only overrides the {@link Connection#close}
+	 * method but returns the connection to the appropriate connection pool in
+	 * {@link PooledDataSource#usernamePasswordConnections}.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class PooledUsernamePasswordConnection extends PooledConnection {
+
+		/** The username of the connection. */
+		private final String username;
+
+		/** The password of the connection. */
+		private final String password;
+
+		/**
+		 * Creates a new pooled connection that was created with a username and
+		 * a password.
+		 *
+		 * @param originalConnection
+		 *            The original connection to wrap
+		 * @param username
+		 *            The username the connection was created with
+		 * @param password
+		 *            The password the connection was created with
+		 */
+		public PooledUsernamePasswordConnection(Connection originalConnection, String username, String password) {
+			super(originalConnection);
+			this.username = username;
+			this.password = password;
+		}
+
+		/**
+		 * @see net.pterodactylus.util.database.PooledDataSource.PooledConnection#close()
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		public void close() throws SQLException {
+			if (!isValid(1)) {
+				getOriginalConnection().close();
+				return;
+			}
+			synchronized (usernamePasswordConnections) {
+				usernamePasswordConnections.get(new Pair<String, String>(username, password)).add(this);
+			}
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/Query.java b/alien/src/net/pterodactylus/util/database/Query.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/Query.java
@@ -0,0 +1,302 @@
+/*
+ * utils - Query.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.pterodactylus.util.database.OrderField.Order;
+
+/**
+ * Container for SQL queries and their parameters.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Query {
+
+	/**
+	 * The type of the query.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public enum Type {
+
+		/** Query is a SELECT. */
+		SELECT,
+
+		/** Query is an INSERT. */
+		INSERT,
+
+		/** Query is an UPDATE. */
+		UPDATE,
+
+		/** Query is a DELETE. */
+		DELETE
+
+	}
+
+	/** The type of the query. */
+	private final Type type;
+
+	/** The fields for a select query. */
+	private final List<Field> fields = new ArrayList<Field>();
+
+	/** The value fields for an insert or update query. */
+	private final List<ValueField> valueFields = new ArrayList<ValueField>();
+
+	/** The name of the table. */
+	private final String table;
+
+	/** The WHERE clauses. */
+	private final List<WhereClause> whereClauses = new ArrayList<WhereClause>();
+
+	/** The order fields for select queries. */
+	private final List<OrderField> orderFields = new ArrayList<OrderField>();
+
+	/** The limit, if any. */
+	private Limit limit;
+
+	/**
+	 * Creates a new query.
+	 *
+	 * @param type
+	 *            The type of the query
+	 * @param table
+	 *            The table that is processed
+	 */
+	public Query(Type type, String table) {
+		this.type = type;
+		this.table = table;
+	}
+
+	/**
+	 * Adds one or more fields to this query.
+	 *
+	 * @param fields
+	 *            The fields to add
+	 */
+	public void addField(Field... fields) {
+		for (Field field : fields) {
+			this.fields.add(field);
+		}
+	}
+
+	/**
+	 * Adds one or more value fields to this query.
+	 *
+	 * @param valueFields
+	 *            The value fields to add
+	 */
+	public void addValueField(ValueField... valueFields) {
+		for (ValueField valueField : valueFields) {
+			this.valueFields.add(valueField);
+		}
+	}
+
+	/**
+	 * Adds one or more WHERE clauses to this query.
+	 *
+	 * @param whereClauses
+	 *            The WHERE clauses to add
+	 */
+	public void addWhereClause(WhereClause... whereClauses) {
+		for (WhereClause whereClause : whereClauses) {
+			this.whereClauses.add(whereClause);
+		}
+	}
+
+	/**
+	 * Adds one or more order fields to this query.
+	 *
+	 * @param orderFields
+	 *            The order fields to add
+	 */
+	public void addOrderField(OrderField... orderFields) {
+		if (type != Type.SELECT) {
+			throw new IllegalStateException("Order fields are only allowed in SELECT queries.");
+		}
+		for (OrderField orderField : orderFields) {
+			this.orderFields.add(orderField);
+		}
+	}
+
+	/**
+	 * Sets the limit for this query.
+	 *
+	 * @param limit
+	 *            The limit for this query
+	 */
+	public void setLimit(Limit limit) {
+		this.limit = limit;
+	}
+
+	/**
+	 * Creates an SQL statement from the given connection and prepares it for
+	 * execution.
+	 *
+	 * @param connection
+	 *            The connection to create a statement from
+	 * @return The prepared statement, ready for execution
+	 * @throws SQLException
+	 *             if an SQL error occurs
+	 */
+	public PreparedStatement createStatement(Connection connection) throws SQLException {
+		StringWriter queryWriter = new StringWriter();
+		try {
+			render(queryWriter);
+		} catch (IOException ioe1) {
+			/* ignore. */
+		}
+		String query = queryWriter.toString();
+		PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
+		int index = 0;
+		if ((type == Type.UPDATE) || (type == Type.INSERT)) {
+			for (ValueField valueField : valueFields) {
+				valueField.getParameter().set(preparedStatement, ++index);
+			}
+		}
+		if ((type == Type.SELECT) || (type == Type.UPDATE) || (type == Type.DELETE)) {
+			for (WhereClause whereClause : whereClauses) {
+				for (Parameter<?> parameter : whereClause.getParameters()) {
+					parameter.set(preparedStatement, ++index);
+				}
+			}
+		}
+		return preparedStatement;
+	}
+
+	/**
+	 * Renders this query to the given writer.
+	 *
+	 * @param writer
+	 *            The writer to render the query to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void render(Writer writer) throws IOException {
+		writer.write(type.name());
+		if (type == Type.SELECT) {
+			writer.write(' ');
+			if (fields.isEmpty()) {
+				writer.write('*');
+			} else {
+				boolean first = true;
+				for (Field field : fields) {
+					if (!first) {
+						writer.write(", ");
+					}
+					writer.write(field.getName());
+					first = false;
+				}
+			}
+			writer.write(" FROM ");
+			writer.write(table);
+			renderWhereClauses(writer);
+			if (!orderFields.isEmpty()) {
+				writer.write(" ORDER BY ");
+				boolean first = true;
+				for (OrderField orderField : orderFields) {
+					if (!first) {
+						writer.write(", ");
+					}
+					writer.write(orderField.getField().getName());
+					writer.write((orderField.getOrder() == Order.ASCENDING) ? " ASC" : " DESC");
+					first = false;
+				}
+			}
+			if (limit != null) {
+				writer.write(" LIMIT ");
+				if (limit.getStart() != 0) {
+					writer.write(String.valueOf(limit.getStart()));
+					writer.write(", ");
+				}
+				writer.write(String.valueOf(limit.getNumber()));
+			}
+		} else if (type == Type.UPDATE) {
+			writer.write(' ');
+			writer.write(table);
+			writer.write(" SET ");
+			boolean first = true;
+			for (ValueField valueField : valueFields) {
+				if (!first) {
+					writer.write(", ");
+				}
+				writer.write(valueField.getName());
+				writer.write(" = ?");
+				first = false;
+			}
+			renderWhereClauses(writer);
+		} else if (type == Type.INSERT) {
+			writer.write(" INTO ");
+			writer.write(table);
+			writer.write(" (");
+			boolean first = true;
+			for (ValueField valueField : valueFields) {
+				if (!first) {
+					writer.write(", ");
+				}
+				writer.write(valueField.getName());
+				first = false;
+			}
+			writer.write(") VALUES (");
+			first = true;
+			for (int fieldIndex = 0; fieldIndex < valueFields.size(); ++fieldIndex) {
+				if (!first) {
+					writer.write(", ");
+				}
+				writer.write('?');
+				first = false;
+			}
+			writer.write(')');
+		} else if (type == Type.DELETE) {
+			writer.write(" FROM ");
+			writer.write(table);
+			renderWhereClauses(writer);
+		}
+	}
+
+	/**
+	 * Renders the WHERE clauses, prepending a “WHERE” if there are WHERE
+	 * clauses to render.
+	 *
+	 * @param writer
+	 *            The writer to render the WHERE clauses to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private void renderWhereClauses(Writer writer) throws IOException {
+		if (whereClauses.isEmpty()) {
+			return;
+		}
+		writer.write(" WHERE ");
+		if (whereClauses.size() == 1) {
+			whereClauses.get(0).render(writer);
+			return;
+		}
+		AndWhereClause whereClause = new AndWhereClause(whereClauses);
+		whereClause.render(writer);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/ResultProcessor.java b/alien/src/net/pterodactylus/util/database/ResultProcessor.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/ResultProcessor.java
@@ -0,0 +1,41 @@
+/*
+ * utils - ResultProcessor.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * A result processor is similar to an {@link ObjectCreator} but it does not
+ * return created objects but processes results in an arbitrary way.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ResultProcessor {
+
+	/**
+	 * Processes the current row of the result set.
+	 *
+	 * @param resultSet
+	 *            The result set to process
+	 * @throws SQLException
+	 *             if an SQL error occurs
+	 */
+	public void processResult(ResultSet resultSet) throws SQLException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/URLDataSource.java b/alien/src/net/pterodactylus/util/database/URLDataSource.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/URLDataSource.java
@@ -0,0 +1,127 @@
+/*
+ * utils - URLDataSource.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+/**
+ * {@link DataSource} implementation that creates connections from a JDBC URL
+ * using {@link DriverManager#getConnection(String)}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class URLDataSource implements DataSource {
+
+	/** The URL to connect to. */
+	private final String connectionUrl;
+
+	/** The log writer. */
+	private PrintWriter logWriter;
+
+	/** The login timeout. */
+	private int loginTimeout;
+
+	/** The login properties. */
+	private final Properties loginProperties = new Properties();
+
+	/**
+	 * Creates a URL data source.
+	 *
+	 * @param connectionUrl
+	 *            The URL to connect to
+	 */
+	public URLDataSource(String connectionUrl) {
+		this.connectionUrl = connectionUrl;
+		loginProperties.setProperty("connectTimeout", "0");
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public PrintWriter getLogWriter() throws SQLException {
+		return logWriter;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setLogWriter(PrintWriter logWriter) throws SQLException {
+		this.logWriter = logWriter;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setLoginTimeout(int loginTimeout) throws SQLException {
+		this.loginTimeout = loginTimeout;
+		loginProperties.setProperty("connectTimeout", String.valueOf(loginTimeout * 1000L));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getLoginTimeout() throws SQLException {
+		return loginTimeout;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public <T> T unwrap(Class<T> iface) throws SQLException {
+		throw new SQLException("No wrapped object found for " + iface.getClass().getName() + ".");
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean isWrapperFor(Class<?> iface) throws SQLException {
+		return false;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Connection getConnection() throws SQLException {
+		return DriverManager.getConnection(connectionUrl, loginProperties);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Connection getConnection(String username, String password) throws SQLException {
+		Properties userProperties = new Properties(loginProperties);
+		userProperties.setProperty("user", username);
+		userProperties.setProperty("password", password);
+		return DriverManager.getConnection(connectionUrl, username, password);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/ValueField.java b/alien/src/net/pterodactylus/util/database/ValueField.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/ValueField.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2010 knorpelfisk.de
+ */
+
+package net.pterodactylus.util.database;
+
+
+/**
+ * A value field stores its name and its value.
+ *
+ * @author <a href="mailto:dr@knorpelfisk.de">David Roden</a>
+ */
+public class ValueField extends Field {
+
+	/** The value of the field */
+	private final Parameter<?> parameter;
+
+	/**
+	 * Creates a new value field.
+	 *
+	 * @param name
+	 *            The name of the field
+	 * @param parameter
+	 *            The value of the field
+	 */
+	public ValueField(String name, Parameter<?> parameter) {
+		super(name);
+		this.parameter = parameter;
+	}
+
+	/**
+	 * Returns the value of this field.
+	 *
+	 * @return The value of this field
+	 */
+	public Parameter<?> getParameter() {
+		return parameter;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/ValueFieldWhereClause.java b/alien/src/net/pterodactylus/util/database/ValueFieldWhereClause.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/ValueFieldWhereClause.java
@@ -0,0 +1,77 @@
+/*
+ * utils - ValueFieldWhereClause.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A WHERE clause that requires a field to match a value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ValueFieldWhereClause implements WhereClause {
+
+	/** The field and the value to match. */
+	private final ValueField valueField;
+
+	/** Whether to invert the result. */
+	private final boolean invert;
+
+	/**
+	 * Creates a new WHERE clause that checks a field for the given value.
+	 *
+	 * @param valueField
+	 *            The field and the value to check for
+	 */
+	public ValueFieldWhereClause(ValueField valueField) {
+		this(valueField, false);
+	}
+
+	/**
+	 * Creates a new WHERE clause that checks a field for the given value.
+	 *
+	 * @param valueField
+	 *            The field and the value to check for
+	 * @param invert
+	 *            {@code true} to invert the result
+	 */
+	public ValueFieldWhereClause(ValueField valueField, boolean invert) {
+		this.valueField = valueField;
+		this.invert = invert;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<Parameter<?>> getParameters() {
+		return Arrays.<Parameter<?>> asList(valueField.getParameter());
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(Writer writer) throws IOException {
+		writer.write("(" + valueField.getName() + " " + (invert ? "!=" : "=") + " ?)");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/WhereClause.java b/alien/src/net/pterodactylus/util/database/WhereClause.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/WhereClause.java
@@ -0,0 +1,50 @@
+/*
+ * utils - WhereClause.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.database;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * Interface for a WHERE clause that can be used to specify criteria for
+ * matching {@link DataObject}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface WhereClause {
+
+	/**
+	 * Returns all parameters of this WHERE clause, in the same order as
+	 * placeholders are specified.
+	 *
+	 * @return The parameters of this WHERE clause
+	 */
+	public List<Parameter<?>> getParameters();
+
+	/**
+	 * Writes this WHERE clause to the given writer.
+	 *
+	 * @param writer
+	 *            The writer to write to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void render(Writer writer) throws IOException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/database/package-info.java b/alien/src/net/pterodactylus/util/database/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/database/package-info.java
@@ -0,0 +1,69 @@
+/**
+ * <p>
+ * Helper and abstraction classes for database interaction.
+ * </p>
+ * <h1>Database Backends</h1>
+ * <p>
+ * The {@link net.pterodactylus.util.database.Database} interface defines data
+ * manipulation methods that can be used to perform 95% of all necessary
+ * interaction with the database.
+ * </p>
+ * <h2>Getting a Single Value from the Database</h2>
+ * <p>
+ * The {@link net.pterodactylus.util.database.Database#getSingle(Query, ObjectCreator)}
+ * method is used to return a single value or object from the database. This
+ * method delegates the work of actually creating the return value to the given
+ * {@link net.pterodactylus.util.database.ObjectCreator} instance which uses the first
+ * row that is returned by the given query to create the appropriate object.
+ * </p>
+ * <h2>Getting Multiple Values from the Database</h2>
+ * <p>
+ * The
+ * {@link net.pterodactylus.util.database.Database#getMultiple(Query, ObjectCreator)}
+ * method is used to return multiple values or objects from the database. Again,
+ * the actual creation process is delegated to the given object creator which
+ * will be used for every row the query returns.
+ * </p>
+ * <h2>Inserting Values into the Database</h2>
+ * <p>
+ * Use the {@link net.pterodactylus.util.database.Database#insert(Query)} method to
+ * execute a query that will insert one new record into a database. It will
+ * return the first auto-generated ID, or <code>-1</code> if no ID was
+ * generated.
+ * </p>
+ * <h2>Updating Values in the Database</h2>
+ * <p>
+ * Finally, the {@link net.pterodactylus.util.database.Database#update(Query)} method
+ * will let you execute a query that updates values in the database. It will
+ * return the number of changed records.
+ * </p>
+ * <h1>The {@link net.pterodactylus.util.database.AbstractDatabase} Implementation</h1>
+ * <p>
+ * This class introduces some helper classes the perform the actual SQL queries
+ * and reduces the cost of writing new database backends to the implementation
+ * of two methods:
+ * {@link net.pterodactylus.util.database.AbstractDatabase#getConnection()
+ * getConnection()} and
+ * {@link net.pterodactylus.util.database.AbstractDatabase#returnConnection(java.sql.Connection)
+ * returnConnection()} . It also contains a method to create a database from a
+ * {@link javax.sql.DataSource} (
+ * {@link net.pterodactylus.util.database.AbstractDatabase#fromDataSource(javax.sql.DataSource)}
+ * ).
+ * </p>
+ * <h1>The {@link net.pterodactylus.util.database.Query} Class</h1>
+ * <p>
+ * This class stores an SQL query string and its parameters. It is able to
+ * create a {@link java.sql.PreparedStatement} from the parameters and it can be
+ * persisted easily.
+ * </p>
+ * <h1>The {@link net.pterodactylus.util.database.ObjectCreator} Class</h1>
+ * <p>
+ * An object creator is responsible for creating objects from the current row of
+ * a {@link java.sql.ResultSet}. It is used by the helper classes in
+ * {@link net.pterodactylus.util.database.AbstractDatabase} to allow the user to control
+ * the conversion from a result set to a domain-specific object.
+ * </p>
+ */
+
+package net.pterodactylus.util.database;
+
diff --git a/alien/src/net/pterodactylus/util/event/AbstractListenerManager.java b/alien/src/net/pterodactylus/util/event/AbstractListenerManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/event/AbstractListenerManager.java
@@ -0,0 +1,129 @@
+/*
+ * utils - AbstractListenerManager.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.event;
+
+import java.util.EventListener;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+
+import net.pterodactylus.util.thread.CurrentThreadExecutor;
+
+/**
+ * Abstract implementation of a listener support class. The listener support
+ * takes care of adding and removing {@link EventListener} implementations, and
+ * subclasses are responsible for firing appropriate events.
+ *
+ * @param <S>
+ *            The type of the source
+ * @param <L>
+ *            The type of the event listeners
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractListenerManager<S, L extends EventListener> {
+
+	/** The source that emits the events. */
+	private final S source;
+
+	/** The list of listeners. */
+	private final List<L> listeners = new CopyOnWriteArrayList<L>();
+
+	/** Service that executes event threads. */
+	private final Executor executor;
+
+	/**
+	 * Creates a new listener support that emits events from the given source.
+	 *
+	 * @param source
+	 *            The source of the events
+	 */
+	public AbstractListenerManager(S source) {
+		this(source, new CurrentThreadExecutor());
+	}
+
+	/**
+	 * Creates a new listener support that emits events from the given source.
+	 *
+	 * @param source
+	 *            The source of the events
+	 * @param executor
+	 *            The executor used to fire events
+	 */
+	public AbstractListenerManager(S source, Executor executor) {
+		this.source = source;
+		this.executor = executor;
+	}
+
+	/**
+	 * Adds the given listener to the list of reigstered listeners.
+	 *
+	 * @param listener
+	 *            The listener to add
+	 */
+	public void addListener(L listener) {
+		synchronized (listeners) {
+			listeners.add(listener);
+		}
+	}
+
+	/**
+	 * Removes the given listener from the list of registered listeners.
+	 *
+	 * @param listener
+	 *            The listener to remove
+	 */
+	public void removeListener(L listener) {
+		synchronized (listeners) {
+			listeners.remove(listener);
+		}
+	}
+
+	//
+	// PROTECTED METHODS
+	//
+
+	/**
+	 * Returns the source for the events.
+	 *
+	 * @return The event source
+	 */
+	protected S getSource() {
+		return source;
+	}
+
+	/**
+	 * Returns the executor for the event firing.
+	 *
+	 * @return The executor
+	 */
+	protected Executor getExecutor() {
+		return executor;
+	}
+
+	/**
+	 * Returns a list of all registered listeners. The returned list is a copy
+	 * of the original list so structural modifications will never occur when
+	 * using the returned list.
+	 *
+	 * @return The list of all registered listeners
+	 */
+	protected List<L> getListeners() {
+		return listeners;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/filter/Filter.java b/alien/src/net/pterodactylus/util/filter/Filter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/filter/Filter.java
@@ -0,0 +1,41 @@
+/*
+ * utils - Filter.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+package net.pterodactylus.util.filter;
+
+/**
+ * Interface for a filter that determines whether a certain action can be
+ * performed on an object based on its properties.
+ *
+ * @param <T>
+ *            The type of the filtered object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Filter<T> {
+
+	/**
+	 * Runs the given object through this filter and return whether the object
+	 * matches this filter or not.
+	 *
+	 * @param object
+	 *            The object to analyse
+	 * @return <code>true</code> if the object matched this filter,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean filterObject(T object);
+
+}
diff --git a/alien/src/net/pterodactylus/util/filter/Filters.java b/alien/src/net/pterodactylus/util/filter/Filters.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/filter/Filters.java
@@ -0,0 +1,193 @@
+/*
+ * utils - Filters.java - Copyright © 2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+package net.pterodactylus.util.filter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Defines various methods to filter {@link Collection}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Filters {
+
+	/**
+	 * Returns a list that contains only the elements from the given list that
+	 * match the given filter.
+	 *
+	 * @param <E>
+	 *            The type of the list elements
+	 * @param list
+	 *            The list to filter
+	 * @param listFilter
+	 *            The list filter
+	 * @return The filtered list
+	 */
+	public static <E> List<E> filteredList(List<E> list, Filter<E> listFilter) {
+		List<E> filteredList = new ArrayList<E>();
+		for (E element : list) {
+			if (listFilter.filterObject(element)) {
+				filteredList.add(element);
+			}
+		}
+		return filteredList;
+	}
+
+	/**
+	 * Returns a set that contains only the elements from the given set that
+	 * match the given filter.
+	 *
+	 * @param <E>
+	 *            The type of the set elements
+	 * @param set
+	 *            The set to filter
+	 * @param setFilter
+	 *            The set filter
+	 * @return The filtered set
+	 */
+	public static <E> Set<E> filteredSet(Set<E> set, Filter<E> setFilter) {
+		Set<E> filteredSet = new HashSet<E>();
+		for (E element : set) {
+			if (setFilter.filterObject(element)) {
+				filteredSet.add(element);
+			}
+		}
+		return filteredSet;
+	}
+
+	/**
+	 * Returns a map that contains only the elements from the given map that
+	 * match the given filter.
+	 *
+	 * @param <K>
+	 *            The type of the map keys
+	 * @param <V>
+	 *            The type of the map values
+	 * @param map
+	 *            The map to filter
+	 * @param mapFilter
+	 *            The map filter
+	 * @return The filtered map
+	 */
+	public static <K, V> Map<K, V> filteredMap(Map<K, V> map, Filter<Entry<K, V>> mapFilter) {
+		Map<K, V> filteredMap = new HashMap<K, V>();
+		for (Entry<K, V> element : map.entrySet()) {
+			if (mapFilter.filterObject(element)) {
+				filteredMap.put(element.getKey(), element.getValue());
+			}
+		}
+		return filteredMap;
+	}
+
+	/**
+	 * Returns a collection that contains only the elements from the given
+	 * collection that match the given filter.
+	 *
+	 * @param <K>
+	 *            The type of the collection values
+	 * @param collection
+	 *            The collection to filter
+	 * @param collectionFilter
+	 *            The collection filter
+	 * @return The filtered collection
+	 */
+	public static <K> Collection<K> filteredCollection(Collection<K> collection, Filter<K> collectionFilter) {
+		return filteredList(new ArrayList<K>(collection), collectionFilter);
+	}
+
+	/**
+	 * Returns an iterator that contains only the elements from the given
+	 * iterator that match the given filter.
+	 *
+	 * @param <E>
+	 *            The type of the iterator elements
+	 * @param iterator
+	 *            The iterator to filter
+	 * @param iteratorFilter
+	 *            The iterator filter
+	 * @return The filtered iterator
+	 */
+	public static <E> Iterator<E> filteredIterator(final Iterator<E> iterator, final Filter<E> iteratorFilter) {
+		return new Iterator<E>() {
+
+			private boolean gotNextElement = false;
+
+			private E nextElement;
+
+			private void getNextElement() {
+				if (gotNextElement) {
+					return;
+				}
+				while (iterator.hasNext()) {
+					nextElement = iterator.next();
+					if (iteratorFilter.filterObject(nextElement)) {
+						gotNextElement = true;
+						break;
+					}
+				}
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see java.util.Iterator#hasNext()
+			 */
+			@Override
+			public boolean hasNext() {
+				getNextElement();
+				return gotNextElement;
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see java.util.Iterator#next()
+			 */
+			@Override
+			public E next() {
+				getNextElement();
+				if (!gotNextElement) {
+					throw new NoSuchElementException("no more elements in iteration");
+				}
+				gotNextElement = false;
+				return nextElement;
+			}
+
+			/**
+			 * {@inheritDoc}
+			 *
+			 * @see java.util.Iterator#remove()
+			 */
+			@Override
+			public void remove() {
+				throw new UnsupportedOperationException("remove() not supported on this iteration");
+			}
+
+		};
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/I18n.java b/alien/src/net/pterodactylus/util/i18n/I18n.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/I18n.java
@@ -0,0 +1,564 @@
+/*
+ * utils - I18n.java - Copyright © 2006-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n;
+
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.KeyStroke;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Handles internationalization.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class I18n {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(I18n.class);
+
+	/** Whether to log access to unknown keys. */
+	public static boolean LOG_UNKNOWN_KEYS = true;
+
+	/** The main source for l10n. */
+	private final Source mainSource;
+
+	/** Additional l10n sources. */
+	private final List<Source> additionalSources = Collections.synchronizedList(new ArrayList<Source>());
+
+	/** List of I18nable listeners. */
+	private final List<I18nable> i18nables = new ArrayList<I18nable>();
+
+	/** Mapping from remove reference to list of I18nables. */
+	private final Map<RemovalReference, List<I18nable>> removalReferenceI18nables = new HashMap<RemovalReference, List<I18nable>>();
+
+	/** The current locale. */
+	private Locale locale;
+
+	/** The current translation values. */
+	private Map<String, String> values = new HashMap<String, String>();
+
+	/** Whether to use {@link MessageFormat} for formatting. */
+	private boolean useMessageFormat = false;
+
+	/**
+	 * Creates a new i18n handler.
+	 *
+	 * @param name
+	 *            The name of the application
+	 * @param propertiesPath
+	 *            The path of the properties
+	 * @param defaultLocale
+	 *            The default locale of the application
+	 */
+	public I18n(String name, String propertiesPath, Locale defaultLocale) {
+		this(name, propertiesPath, defaultLocale, Thread.currentThread().getContextClassLoader(), Locale.getDefault());
+	}
+
+	/**
+	 * Creates a new i18n handler.
+	 *
+	 * @param name
+	 *            The name of the application
+	 * @param propertiesPath
+	 *            The path of the properties
+	 * @param defaultLocale
+	 *            The default locale of the application
+	 * @param classLoader
+	 *            The class loader used to load the properties
+	 * @param currentLocale
+	 *            The current locale
+	 */
+	public I18n(String name, String propertiesPath, Locale defaultLocale, ClassLoader classLoader, Locale currentLocale) {
+		this(new Source(name, propertiesPath, defaultLocale, classLoader), currentLocale);
+	}
+
+	/**
+	 * Creates a new i18n handler.
+	 *
+	 * @param source
+	 *            The l10n source
+	 */
+	public I18n(Source source) {
+		this(source, Locale.getDefault());
+	}
+
+	/**
+	 * Creates a new i18n handler.
+	 *
+	 * @param source
+	 *            The l10n source
+	 * @param currentLocale
+	 *            The current locale
+	 */
+	public I18n(Source source, Locale currentLocale) {
+		mainSource = source;
+		locale = currentLocale;
+		reload();
+	}
+
+	//
+	// LISTENER MANAGEMENT
+	//
+
+	/**
+	 * Adds an i18n listener that is notified when the language is changed or
+	 * additional sources add added or removed.
+	 *
+	 * @param i18nable
+	 *            The i18n listener to add
+	 */
+	public void addI18nable(I18nable i18nable) {
+		addI18nable(i18nable, null);
+	}
+
+	/**
+	 * Adds an i18n listener that is notified when the language is changed or
+	 * additional sources add added or removed.
+	 *
+	 * @param i18nable
+	 *            The i18n listener to add
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 */
+	public void addI18nable(I18nable i18nable, RemovalReference removalReference) {
+		i18nables.add(i18nable);
+		if (removalReference != null) {
+			List<I18nable> i18nableList = removalReferenceI18nables.get(removalReference);
+			if (i18nableList == null) {
+				i18nableList = new ArrayList<I18nable>();
+				removalReferenceI18nables.put(removalReference, i18nableList);
+			}
+			i18nableList.add(i18nable);
+		}
+	}
+
+	/**
+	 * Removes an i18n listener.
+	 *
+	 * @param i18nable
+	 *            The i18n listener to remove
+	 */
+	public void removeI18nable(I18nable i18nable) {
+		i18nables.remove(i18nable);
+	}
+
+	/**
+	 * Removes all i18n listeners that have been
+	 * {@link #addI18nable(I18nable, RemovalReference)} using the given object
+	 * as removal reference.
+	 *
+	 * @param removalReference
+	 *            The removal reference
+	 */
+	public void removeI18nables(RemovalReference removalReference) {
+		List<I18nable> i18nableList = removalReferenceI18nables.remove(removalReference);
+		if (i18nableList != null) {
+			for (I18nable i18nable : i18nableList) {
+				i18nables.remove(i18nable);
+			}
+		}
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Sets whether to use {@link MessageFormat} for formatting values with
+	 * parameters.
+	 *
+	 * @param useMessageFormat
+	 *            {@code true} to use {@link MessageFormat}, {@code false} to
+	 *            use {@link String#format(String, Object...)}
+	 */
+	public void useMessageFormat(boolean useMessageFormat) {
+		this.useMessageFormat = useMessageFormat;
+	}
+
+	/**
+	 * Returns the current locale.
+	 *
+	 * @return The current locale
+	 */
+	public Locale getLocale() {
+		return locale;
+	}
+
+	/**
+	 * Sets the current locale.
+	 *
+	 * @param locale
+	 *            The new locale
+	 */
+	public void setLocale(Locale locale) {
+		this.locale = locale;
+		reload();
+	}
+
+	/**
+	 * Adds an additional l10n source.
+	 *
+	 * @param source
+	 *            The l10n source to add
+	 */
+	public void addSource(Source source) {
+		additionalSources.add(source);
+		reload();
+	}
+
+	/**
+	 * Removes an additional l10n source.
+	 *
+	 * @param source
+	 *            The l10n source to remove
+	 */
+	public void removeSource(Source source) {
+		if (additionalSources.remove(source)) {
+			reload();
+		}
+	}
+
+	/**
+	 * Returns whether the current translation contains the given key.
+	 *
+	 * @param key
+	 *            The key to check for
+	 * @return {@code true} if there is a translation for the given key, {@code
+	 *         false} otherwise
+	 */
+	public boolean has(String key) {
+		synchronized (values) {
+			return values.containsKey(key);
+		}
+	}
+
+	/**
+	 * Returns the translated value for the given key. If no translation is
+	 * found, the name of the key is returned.
+	 *
+	 * @see Formatter
+	 * @param key
+	 *            The key to get the translation for
+	 * @param parameters
+	 *            Parameters to substitute in the value of the key
+	 * @return The translated value, or the key
+	 */
+	public String get(String key, Object... parameters) {
+		String value;
+		synchronized (values) {
+			value = values.get(key);
+		}
+		if (value == null) {
+			if (LOG_UNKNOWN_KEYS) {
+				logger.log(Level.WARNING, String.format("Please supply a value for “%1$s”!", key), new Exception());
+			}
+			return key;
+		}
+		if ((parameters != null) && (parameters.length > 0)) {
+			return useMessageFormat ? MessageFormat.format(value, parameters) : String.format(value, parameters);
+		}
+		return value;
+	}
+
+	/**
+	 * Returns the keycode from the value of the given key. You can specify the
+	 * constants in {@link KeyEvent} in the properties file, e.g. VK_S for the
+	 * keycode ‘s’ when used for mnemonics.
+	 *
+	 * @param key
+	 *            The key under which the keycode is stored
+	 * @return The keycode
+	 */
+	public int getKey(String key) {
+		String value;
+		synchronized (values) {
+			value = values.get(key);
+		}
+		if ((value != null) && value.startsWith("VK_")) {
+			try {
+				Field field = KeyEvent.class.getField(value);
+				return field.getInt(null);
+			} catch (SecurityException e) {
+				/* ignore. */
+			} catch (NoSuchFieldException e) {
+				/* ignore. */
+			} catch (IllegalArgumentException e) {
+				/* ignore. */
+			} catch (IllegalAccessException e) {
+				/* ignore. */
+			}
+		}
+		if (LOG_UNKNOWN_KEYS) {
+			logger.log(Level.WARNING, "please fix “" + key + "”!", new Throwable());
+		}
+		return KeyEvent.VK_UNDEFINED;
+	}
+
+	/**
+	 * Returns a key stroke for use with swing accelerators.
+	 *
+	 * @param key
+	 *            The key of the key stroke
+	 * @return The key stroke, or <code>null</code> if no key stroke could be
+	 *         created from the translated value
+	 */
+	public KeyStroke getKeyStroke(String key) {
+		String value;
+		synchronized (values) {
+			value = values.get(key);
+		}
+		if (value == null) {
+			return null;
+		}
+		StringTokenizer keyTokens = new StringTokenizer(value, "+- ");
+		int modifierMask = 0;
+		while (keyTokens.hasMoreTokens()) {
+			String keyToken = keyTokens.nextToken();
+			if ("ctrl".equalsIgnoreCase(keyToken)) {
+				modifierMask |= InputEvent.CTRL_DOWN_MASK;
+			} else if ("alt".equalsIgnoreCase(keyToken)) {
+				modifierMask |= InputEvent.ALT_DOWN_MASK;
+			} else if ("shift".equalsIgnoreCase(keyToken)) {
+				modifierMask |= InputEvent.SHIFT_DOWN_MASK;
+			} else {
+				if (keyToken.startsWith("VK_")) {
+					if (keyToken.equals("VK_UNDEFINED")) {
+						return null;
+					}
+					try {
+						Field field = KeyEvent.class.getField(keyToken);
+						return KeyStroke.getKeyStroke(field.getInt(null), modifierMask);
+					} catch (SecurityException e) {
+						/* ignore. */
+					} catch (NoSuchFieldException e) {
+						/* ignore. */
+					} catch (IllegalArgumentException e) {
+						/* ignore. */
+					} catch (IllegalAccessException e) {
+						/* ignore. */
+					}
+				}
+				return KeyStroke.getKeyStroke(keyToken.charAt(0), modifierMask);
+			}
+		}
+		return null;
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Reloads translation values for the current locale and l10n sources.
+	 */
+	private void reload() {
+		Properties currentValues = new Properties();
+		loadSource(currentValues, mainSource, mainSource.getDefaultLocale());
+		loadSource(currentValues, mainSource, locale);
+		for (Source additionalSource : additionalSources) {
+			loadSource(currentValues, additionalSource, additionalSource.getDefaultLocale());
+			loadSource(currentValues, additionalSource, locale);
+		}
+		synchronized (values) {
+			values.clear();
+			for (Entry<Object, Object> valueEntry : currentValues.entrySet()) {
+				values.put((String) valueEntry.getKey(), (String) valueEntry.getValue());
+			}
+		}
+		for (I18nable i18nable : i18nables) {
+			i18nable.updateI18n();
+		}
+	}
+
+	/**
+	 * Loads the translation values from a given source.
+	 *
+	 * @param currentValues
+	 *            The current translation values
+	 * @param source
+	 *            The l10n source to load
+	 * @param locale
+	 *            The locale to load from the source
+	 */
+	private void loadSource(Properties currentValues, Source source, Locale locale) {
+		for (String variant : buildResourceNames(locale)) {
+			loadResource(currentValues, source.getClassLoader(), source.getPropertiesPath() + "/" + source.getName() + "_" + variant + ".properties");
+		}
+	}
+
+	/**
+	 * Builds up to three resource names. The first resource name is always the
+	 * language (“en”), the (optional) second one consists of the language and
+	 * the country (“en_GB”) and the (optional) third one includes a variant
+	 * (“en_GB_MAC”).
+	 *
+	 * @param locale
+	 *            The locale to build variant names from
+	 * @return The variant names
+	 */
+	private String[] buildResourceNames(Locale locale) {
+		List<String> variants = new ArrayList<String>();
+		String currentVariant = locale.getLanguage();
+		variants.add(currentVariant);
+		if (!locale.getCountry().equals("")) {
+			currentVariant += "_" + locale.getCountry();
+			variants.add(currentVariant);
+		}
+		if ((locale.getVariant() != null) && (!locale.getVariant().equals(""))) {
+			if (locale.getCountry().equals("")) {
+				currentVariant += "_";
+			}
+			currentVariant += "_" + locale.getVariant();
+			variants.add(locale.getVariant());
+		}
+		return variants.toArray(new String[variants.size()]);
+	}
+
+	/**
+	 * Loads a resource from the given class loader.
+	 *
+	 * @param currentValues
+	 *            The current translation values to load the resource into
+	 * @param classLoader
+	 *            The class loader used to load the resource
+	 * @param resourceName
+	 *            The name of the resource
+	 */
+	private void loadResource(Properties currentValues, ClassLoader classLoader, String resourceName) {
+		logger.log(Level.FINEST, "Trying to load resources from " + resourceName + "…");
+		InputStream inputStream = classLoader.getResourceAsStream(resourceName);
+		if (inputStream != null) {
+			try {
+				logger.log(Level.FINEST, "Loading resources from " + resourceName + "…");
+				currentValues.load(inputStream);
+				logger.log(Level.FINEST, "Resources successfully loaded.");
+			} catch (IOException ioe1) {
+				logger.log(Level.WARNING, String.format("Could not read properties from “%1$s”.", resourceName), ioe1);
+			} catch (IllegalArgumentException iae1) {
+				logger.log(Level.WARNING, String.format("Could not parse properties from “%1$s”.", resourceName), iae1);
+			}
+		}
+	}
+
+	/**
+	 * A localization (l10n) source.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class Source {
+
+		/** The name of the application. */
+		private final String name;
+
+		/** The path of the properties. */
+		private final String propertiesPath;
+
+		/** The default locale of the application. */
+		private final Locale defaultLocale;
+
+		/** The class loader for loading resources. */
+		private final ClassLoader classLoader;
+
+		/**
+		 * Creates a new l10n source.
+		 *
+		 * @param name
+		 *            The name of the application
+		 * @param propertiesPath
+		 *            The path of the properties
+		 * @param defaultLocale
+		 *            The default locale of the source
+		 * @param classLoader
+		 *            The class loader for the source’s resources
+		 */
+		public Source(String name, String propertiesPath, Locale defaultLocale, ClassLoader classLoader) {
+			this.name = name;
+			this.propertiesPath = propertiesPath;
+			this.defaultLocale = defaultLocale;
+			this.classLoader = classLoader;
+		}
+
+		/**
+		 * Returns the name of the application.
+		 *
+		 * @return The name of the application
+		 */
+		public String getName() {
+			return name;
+		}
+
+		/**
+		 * Returns the path of the properties.
+		 *
+		 * @return The path of the properties
+		 */
+		public String getPropertiesPath() {
+			return propertiesPath;
+		}
+
+		/**
+		 * Returns the default locale of the source.
+		 *
+		 * @return The default locale of the source
+		 */
+		public Locale getDefaultLocale() {
+			return defaultLocale;
+		}
+
+		/**
+		 * Returns the source’s class loader.
+		 *
+		 * @return The class loader of the source
+		 */
+		public ClassLoader getClassLoader() {
+			return classLoader;
+		}
+
+	}
+
+	/**
+	 * Identifying container for a removal reference.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class RemovalReference {
+
+		/* nothing here. */
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/I18nable.java b/alien/src/net/pterodactylus/util/i18n/I18nable.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/I18nable.java
@@ -0,0 +1,32 @@
+/*
+ * utils - I18nable.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n;
+
+/**
+ * Interface for objects that want to be notified when the language is changed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface I18nable {
+
+	/**
+	 * Notifies the object that the language in {@link I18n} was changed.
+	 */
+	public void updateI18n();
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/gui/I18nAction.java b/alien/src/net/pterodactylus/util/i18n/gui/I18nAction.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/gui/I18nAction.java
@@ -0,0 +1,201 @@
+/*
+ * utils - I18nAction.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n.gui;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.Icon;
+
+import net.pterodactylus.util.i18n.I18n;
+import net.pterodactylus.util.i18n.I18nable;
+import net.pterodactylus.util.i18n.I18n.RemovalReference;
+
+/**
+ * Helper class that initializes actions with values from {@link I18n}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class I18nAction extends AbstractAction implements I18nable {
+
+	/** The i18n handler. */
+	protected final I18n i18n;
+
+	/** The I18n basename. */
+	private final String i18nName;
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nName
+	 *            The base name of the action
+	 */
+	public I18nAction(I18n i18n, String i18nName) {
+		this(i18n, null, i18nName);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nName
+	 *            The base name of the action
+	 */
+	public I18nAction(I18n i18n, RemovalReference removalReference, String i18nName) {
+		this(i18n, removalReference, i18nName, null);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n} and the given icon.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param icon
+	 *            The icon for the action
+	 */
+	public I18nAction(I18n i18n, String i18nName, Icon icon) {
+		this(i18n, null, i18nName, icon);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n} and the given icon.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param icon
+	 *            The icon for the action
+	 */
+	public I18nAction(I18n i18n, RemovalReference removalReference, String i18nName, Icon icon) {
+		this(i18n, removalReference, i18nName, true, icon);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param enabled
+	 *            Whether the action should be enabled
+	 */
+	public I18nAction(I18n i18n, String i18nName, boolean enabled) {
+		this(i18n, null, i18nName, enabled);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param enabled
+	 *            Whether the action should be enabled
+	 */
+	public I18nAction(I18n i18n, RemovalReference removalReference, String i18nName, boolean enabled) {
+		this(i18n, removalReference, i18nName, enabled, null);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n} and the given icon.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param enabled
+	 *            Whether the action should be enabled
+	 * @param icon
+	 *            The icon for the action
+	 */
+	public I18nAction(I18n i18n, String i18nName, boolean enabled, Icon icon) {
+		this(i18n, null, i18nName, enabled, icon);
+	}
+
+	/**
+	 * Creates a new action that uses the given name as base name to get values
+	 * from {@link I18n} and the given icon.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nName
+	 *            The base name of the action
+	 * @param enabled
+	 *            Whether the action should be enabled
+	 * @param icon
+	 *            The icon for the action
+	 */
+	public I18nAction(I18n i18n, RemovalReference removalReference, String i18nName, boolean enabled, Icon icon) {
+		this.i18n = i18n;
+		this.i18nName = i18nName;
+		if (icon != null) {
+			putValue(Action.SMALL_ICON, icon);
+		}
+		setEnabled(enabled);
+		i18n.addI18nable(this, removalReference);
+		updateI18n();
+	}
+
+	/**
+	 * Returns the i18n basename of this action.
+	 *
+	 * @return This action’s i18n basename
+	 */
+	String getI18nBasename() {
+		return i18nName;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void updateI18n() {
+		putValue(Action.NAME, i18n.get(i18nName + ".name"));
+		putValue(Action.MNEMONIC_KEY, i18n.getKey(i18nName + ".mnemonic"));
+		putValue(Action.ACCELERATOR_KEY, i18n.getKeyStroke(i18nName + ".accelerator"));
+		putValue(Action.SHORT_DESCRIPTION, i18n.get(i18nName + ".shortDescription"));
+		if (i18n.has(i18nName + ".longDescription")) {
+			putValue(Action.LONG_DESCRIPTION, i18n.get(i18nName + ".longDescription"));
+		} else {
+			putValue(Action.LONG_DESCRIPTION, i18n.get(i18nName + ".shortDescription"));
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/gui/I18nLabel.java b/alien/src/net/pterodactylus/util/i18n/gui/I18nLabel.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/gui/I18nLabel.java
@@ -0,0 +1,201 @@
+/*
+ * utils - I18nLabel.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n.gui;
+
+import java.awt.Component;
+
+import javax.swing.JLabel;
+
+import net.pterodactylus.util.i18n.I18n;
+import net.pterodactylus.util.i18n.I18nable;
+import net.pterodactylus.util.i18n.I18n.RemovalReference;
+
+/**
+ * Label that can update itself from {@link I18n}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class I18nLabel extends JLabel implements I18nable {
+
+	/** The i18n handler. */
+	private final I18n i18n;
+
+	/** The I18n basename of the label. */
+	private final String i18nBasename;
+
+	/** Optional arguments for i18n replacement. */
+	private final Object[] arguments;
+
+	/**
+	 * Creates a new label with the given I18n basename.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 */
+	public I18nLabel(I18n i18n, String i18nBasename) {
+		this(i18n, null, i18nBasename);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 */
+	public I18nLabel(I18n i18n, RemovalReference removalReference, String i18nBasename) {
+		this(i18n, removalReference, i18nBasename, (Component) null);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param component
+	 *            The component that is activated by the label, or
+	 *            <code>null</code> if this label should not activate a
+	 *            component
+	 */
+	public I18nLabel(I18n i18n, String i18nBasename, Component component) {
+		this(i18n, null, i18nBasename, component);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param component
+	 *            The component that is activated by the label, or
+	 *            <code>null</code> if this label should not activate a
+	 *            component
+	 */
+	public I18nLabel(I18n i18n, RemovalReference removalReference, String i18nBasename, Component component) {
+		this(i18n, removalReference, i18nBasename, component, (Object[]) null);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param arguments
+	 *            Optional arguments that are handed in to
+	 *            {@link I18n#get(String, Object...)}
+	 */
+	public I18nLabel(I18n i18n, String i18nBasename, Object... arguments) {
+		this(i18n, (RemovalReference) null, i18nBasename, arguments);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param arguments
+	 *            Optional arguments that are handed in to
+	 *            {@link I18n#get(String, Object...)}
+	 */
+	public I18nLabel(I18n i18n, RemovalReference removalReference, String i18nBasename, Object... arguments) {
+		this(i18n, removalReference, i18nBasename, (Component) null, arguments);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param component
+	 *            The component that is activated by the label, or
+	 *            <code>null</code> if this label should not activate a
+	 *            component
+	 * @param arguments
+	 *            Optional arguments that are handed in to
+	 *            {@link I18n#get(String, Object...)}
+	 */
+	public I18nLabel(I18n i18n, String i18nBasename, Component component, Object... arguments) {
+		this(i18n, null, i18nBasename, component, arguments);
+	}
+
+	/**
+	 * Creates a new label with the given I18n basename that optionally is a
+	 * label for the given component.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The I18n basename of the label
+	 * @param component
+	 *            The component that is activated by the label, or
+	 *            <code>null</code> if this label should not activate a
+	 *            component
+	 * @param arguments
+	 *            Optional arguments that are handed in to
+	 *            {@link I18n#get(String, Object...)}
+	 */
+	public I18nLabel(I18n i18n, RemovalReference removalReference, String i18nBasename, Component component, Object... arguments) {
+		super();
+		this.i18n = i18n;
+		this.i18nBasename = i18nBasename;
+		i18n.addI18nable(this, removalReference);
+		this.arguments = arguments;
+		if (component != null) {
+			setLabelFor(component);
+		}
+		updateI18n();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void updateI18n() {
+		setText(i18n.get(i18nBasename + ".name", arguments));
+		if (getLabelFor() != null) {
+			setDisplayedMnemonic(i18n.getKey(i18nBasename + ".mnemonic"));
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/gui/I18nMenu.java b/alien/src/net/pterodactylus/util/i18n/gui/I18nMenu.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/gui/I18nMenu.java
@@ -0,0 +1,81 @@
+/*
+ * utils - I18nMenu.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n.gui;
+
+import javax.swing.JMenu;
+
+import net.pterodactylus.util.i18n.I18n;
+import net.pterodactylus.util.i18n.I18nable;
+import net.pterodactylus.util.i18n.I18n.RemovalReference;
+
+/**
+ * Menu that receives its properties from {@link I18n}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class I18nMenu extends JMenu implements I18nable {
+
+	/** The i18n handler. */
+	private final I18n i18n;
+
+	/** The {@link I18n} basename. */
+	private final String i18nBasename;
+
+	/**
+	 * Creates a new menu with the given {@link I18n} basename.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The basename of the {@link I18n} properties
+	 */
+	public I18nMenu(I18n i18n, String i18nBasename) {
+		this(i18n, null, i18nBasename);
+	}
+
+	/**
+	 * Creates a new menu with the given {@link I18n} basename.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The basename of the {@link I18n} properties
+	 */
+	public I18nMenu(I18n i18n, RemovalReference removalReference, String i18nBasename) {
+		this.i18n = i18n;
+		this.i18nBasename = i18nBasename;
+		i18n.addI18nable(this, removalReference);
+		updateI18n();
+	}
+
+	//
+	// INTERFACE I18nable
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void updateI18n() {
+		setText(i18n.get(i18nBasename + ".name"));
+		setMnemonic(i18n.getKey(i18nBasename + ".mnemonic"));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/gui/I18nMenuItem.java b/alien/src/net/pterodactylus/util/i18n/gui/I18nMenuItem.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/gui/I18nMenuItem.java
@@ -0,0 +1,170 @@
+/*
+ * utils - I18nMenuItem.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.i18n.gui;
+
+import java.awt.MenuItem;
+import java.awt.MenuShortcut;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.Action;
+
+import net.pterodactylus.util.i18n.I18n;
+import net.pterodactylus.util.i18n.I18nable;
+import net.pterodactylus.util.i18n.I18n.RemovalReference;
+
+/**
+ * {@link I18nable} wrapper around an AWT {@link MenuItem} that can also use an
+ * {@link Action} to be performed when it is selected. This should not be used
+ * for “normal” Swing applications!
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class I18nMenuItem extends MenuItem implements I18nable {
+
+	/** The i18n handler. */
+	private final I18n i18n;
+
+	/** The i18n basename of the menu item. */
+	private final String i18nBasename;
+
+	/** The action, if this menu item was derived from one. */
+	private final Action action;
+
+	/**
+	 * Creates a new i18nable menu item.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The i18n basename of the menu item
+	 */
+	public I18nMenuItem(I18n i18n, String i18nBasename) {
+		this(i18n, null, i18nBasename);
+	}
+
+	/**
+	 * Creates a new i18nable menu item.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The i18n basename of the menu item
+	 */
+	public I18nMenuItem(I18n i18n, RemovalReference removalReference, String i18nBasename) {
+		this(i18n, removalReference, i18nBasename, null);
+	}
+
+	/**
+	 * Creates a new i18nable menu item that will perform the given action when
+	 * selected.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nBasename
+	 *            The i18n base name of the menu item
+	 * @param action
+	 *            The action to perform when selected
+	 */
+	public I18nMenuItem(I18n i18n, String i18nBasename, Action action) {
+		this(i18n, null, i18nBasename, action);
+	}
+
+	/**
+	 * Creates a new i18nable menu item that will perform the given action when
+	 * selected.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nBasename
+	 *            The i18n base name of the menu item
+	 * @param action
+	 *            The action to perform when selected
+	 */
+	public I18nMenuItem(I18n i18n, RemovalReference removalReference, String i18nBasename, final Action action) {
+		this.i18n = i18n;
+		this.i18nBasename = i18nBasename;
+		i18n.addI18nable(this, removalReference);
+		this.action = action;
+		if (action != null) {
+			addActionListener(new ActionListener() {
+
+				@Override
+				public void actionPerformed(ActionEvent actionEvent) {
+					action.actionPerformed(actionEvent);
+				}
+			});
+			action.addPropertyChangeListener(new PropertyChangeListener() {
+
+				@Override
+				public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
+					if ("enabled".equals(propertyChangeEvent.getPropertyName())) {
+						updateI18n();
+					}
+				}
+			});
+		}
+		updateI18n();
+	}
+
+	/**
+	 * Creates a new i18nable menu item that used the properties of the given
+	 * {@link I18nAction}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param i18nAction
+	 *            The action to copy
+	 */
+	public I18nMenuItem(I18n i18n, I18nAction i18nAction) {
+		this(i18n, (RemovalReference) null, i18nAction);
+	}
+
+	/**
+	 * Creates a new i18nable menu item that used the properties of the given
+	 * {@link I18nAction}.
+	 *
+	 * @param i18n
+	 *            The i18n handler
+	 * @param removalReference
+	 *            Removal reference (optional)
+	 * @param i18nAction
+	 *            The action to copy
+	 */
+	public I18nMenuItem(I18n i18n, RemovalReference removalReference, I18nAction i18nAction) {
+		this(i18n, removalReference, i18nAction.getI18nBasename(), i18nAction);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void updateI18n() {
+		setLabel(i18n.get(i18nBasename + ".name"));
+		setShortcut(new MenuShortcut(i18n.getKey(i18nBasename + ".mnemonic"), false));
+		if (action != null) {
+			setEnabled(action.isEnabled());
+		}
+	}
+}
diff --git a/alien/src/net/pterodactylus/util/i18n/gui/package-info.java b/alien/src/net/pterodactylus/util/i18n/gui/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/i18n/gui/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * utils - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Contains various i18n-related Swing helper classes.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+
+package net.pterodactylus.util.i18n.gui;
\ No newline at end of file
diff --git a/alien/src/net/pterodactylus/util/image/IconLoader.java b/alien/src/net/pterodactylus/util/image/IconLoader.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/image/IconLoader.java
@@ -0,0 +1,78 @@
+/*
+ * utils - IconLoader.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version.
+ *
+ * This program 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 General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.util.image;
+
+import java.awt.Image;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+/**
+ * Loads an {@link Icon} or an {@link Image} from the class path.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IconLoader {
+
+	/**
+	 * Loads an icon from the class path.
+	 *
+	 * @param resourceName
+	 *            The name of the icon
+	 * @return The icon, or <code>null</code> if no icon was found
+	 */
+	public static Icon loadIcon(String resourceName) {
+		try {
+			InputStream resourceStream = IconLoader.class.getResourceAsStream(resourceName);
+			if (resourceStream == null) {
+				return null;
+			}
+			ByteArrayOutputStream imageOutput = new ByteArrayOutputStream();
+			byte[] buffer = new byte[16384];
+			int r = 0;
+			while ((r = resourceStream.read(buffer)) != -1) {
+				imageOutput.write(buffer, 0, r);
+			}
+			imageOutput.flush();
+			return new ImageIcon(imageOutput.toByteArray());
+		} catch (IOException e) {
+			/* ignore. */
+		}
+		return null;
+	}
+
+	/**
+	 * Loads an image from the class path.
+	 *
+	 * @param resourceName
+	 *            The name of the image
+	 * @return The image, or <code>null</code> if no image was found
+	 */
+	public static Image loadImage(String resourceName) {
+		ImageIcon imageIcon = (ImageIcon) loadIcon(resourceName);
+		if (imageIcon == null) {
+			return null;
+		}
+		return imageIcon.getImage();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/BitShiftedInputStream.java b/alien/src/net/pterodactylus/util/io/BitShiftedInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/BitShiftedInputStream.java
@@ -0,0 +1,120 @@
+/*
+ * utils - BitShiftedOutputStream.java - Copyright © 2006-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.pterodactylus.util.number.Bits;
+
+/**
+ * An InputStream that can read values with a bit size of other than 8.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BitShiftedInputStream extends FilterInputStream {
+
+	/** The number of bits per value. */
+	protected int valueSize;
+
+	/** The current bit position in the underlying input stream. */
+	protected int currentBitPosition;
+
+	/** The current value from the underlying input stream. */
+	protected int currentValue;
+
+	/**
+	 * Creates a new bit-shifted input stream wrapped around the specified input
+	 * stream with the specified value size.
+	 *
+	 * @param in
+	 *            The input stream to wrap
+	 * @param valueSize
+	 *            The number of bits per value
+	 */
+	public BitShiftedInputStream(InputStream in, int valueSize) {
+		super(in);
+		if ((valueSize < 1) || (valueSize > 32)) {
+			throw new IllegalArgumentException("valueSize out of range 1-32");
+		}
+		this.valueSize = valueSize;
+		currentBitPosition = 8;
+	}
+
+	/**
+	 * Reads a value from the underlying input stream.
+	 *
+	 * @return A value from the underlying input stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	@Override
+	public int read() throws IOException {
+		return read(valueSize);
+	}
+
+	/**
+	 * Reads a value with the given number of bits from the underlying input
+	 * stream.
+	 *
+	 * @param valueSize
+	 *            The number of bits to read
+	 * @return A value from the underlying input stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public int read(int valueSize) throws IOException {
+		int bitsLeft = valueSize;
+		int value = 0;
+		while (bitsLeft > 0) {
+			if (currentBitPosition > 7) {
+				currentValue = super.read();
+				currentBitPosition = 0;
+			}
+			value = Bits.encodeBits(value, valueSize - bitsLeft, 1, currentValue);
+			currentValue >>>= 1;
+			currentBitPosition++;
+			bitsLeft--;
+		}
+		return value;
+	}
+
+	/**
+	 * Skips the specified number of bits. This can be used to re-align the bit
+	 * stream.
+	 *
+	 * @param numberOfBits
+	 *            The number of bits to skip
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void skipBits(int numberOfBits) throws IOException {
+		int bitsLeft = numberOfBits;
+		while (bitsLeft > 0) {
+			if (currentBitPosition > 7) {
+				currentValue = super.read();
+				currentBitPosition = 0;
+			}
+			currentValue >>>= 1;
+			currentBitPosition++;
+			bitsLeft--;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/BitShiftedOutputStream.java b/alien/src/net/pterodactylus/util/io/BitShiftedOutputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/BitShiftedOutputStream.java
@@ -0,0 +1,178 @@
+/*
+ * utils - BitShiftedOutputStream.java - Copyright © 2006-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import net.pterodactylus.util.number.Bits;
+
+/**
+ * A bit-shifted output stream can write an (almost) arbitrary amount of bits
+ * for a value given to {@link #write(int)}. Due to implementation reasons the
+ * amount of bits should be between 1 and 32, inclusive. Also note that you can
+ * not use the {@link OutputStream#write(byte[])} or
+ * {@link OutputStream#write(byte[], int, int)} methods because they will
+ * truncate your value to the lowest eight bits which is of course only a
+ * problem if you intend to write values larger than eight bits.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BitShiftedOutputStream extends FilterOutputStream {
+
+	/** The number of bits to write for a value. */
+	private final int valueSize;
+
+	/** The current bit position. */
+	private int currentBitPosition;
+
+	/** The current value. */
+	private int currentValue;
+
+	/**
+	 * Creates a new bit-shifted output stream that writes
+	 * <code>valueSize</code> bits per value.
+	 *
+	 * @param outputStream
+	 *            The underlying output stream
+	 * @param valueSize
+	 *            The number of bits to write with one {@link #write(int)}
+	 *            instruction
+	 * @throws IllegalArgumentException
+	 *             if <code>valueSize</code> is not in the range of 1 to 32,
+	 *             inclusive
+	 */
+	public BitShiftedOutputStream(OutputStream outputStream, int valueSize) throws IllegalArgumentException {
+		super(outputStream);
+		if ((valueSize < 1) || (valueSize > 32)) {
+			throw new IllegalArgumentException("valueSize out of range [1-32]");
+		}
+		this.valueSize = valueSize;
+		this.currentBitPosition = 0;
+		this.currentValue = 0;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.io.FilterOutputStream#write(int)
+	 */
+	@Override
+	public void write(int value) throws IOException {
+		int valueLeft = value;
+		int bitsLeft = valueSize;
+		while (bitsLeft > 0) {
+			int bitsToEncode = Math.min(8, Math.min(bitsLeft, 8 - currentBitPosition));
+			currentValue = Bits.encodeBits(currentValue, currentBitPosition, bitsToEncode, valueLeft);
+			valueLeft >>>= bitsToEncode;
+			currentBitPosition += bitsToEncode;
+			bitsLeft -= bitsToEncode;
+			if (currentBitPosition == 8) {
+				super.write(currentValue & 0xff);
+				currentBitPosition = 0;
+				currentValue = -1;
+			}
+		}
+	}
+
+	/**
+	 * Writes the specified number of zero-bits to this stream.
+	 *
+	 * @param numberOfBits
+	 *            The number of zero-bits to write
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void writePadding(int numberOfBits) throws IOException {
+		writePadding(numberOfBits, 0x00);
+	}
+
+	/**
+	 * Writes the specified number of bits to the stream. The bit used to pad
+	 * the stream is the lowest bit of <code>fillBit</code>.
+	 *
+	 * @param numberOfBits
+	 *            The number of padding bits to write
+	 * @param fillBit
+	 *            Contains at the lowest bit position the padding bit to write
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void writePadding(int numberOfBits, int fillBit) throws IOException {
+		int bitsLeft = numberOfBits;
+		while (bitsLeft > 0) {
+			currentValue = Bits.encodeBits(currentValue, currentBitPosition, 1, fillBit);
+			currentBitPosition++;
+			bitsLeft--;
+			if (currentBitPosition == 8) {
+				super.write(currentValue & 0xff);
+				currentBitPosition = 0;
+				currentValue = -1;
+			}
+		}
+	}
+
+	/**
+	 * Flushes all unwritten data to the underlying output stream.
+	 * <p>
+	 * This is a convenience method for {@link #flush(int) flush(0x00)} which
+	 * will zero-pad the unused bits in the last byte.
+	 *
+	 * @see java.io.FilterOutputStream#flush()
+	 */
+	@Override
+	public void flush() throws IOException {
+		flush(0x00);
+	}
+
+	/**
+	 * Flushes this output stream, writing all unwritten values to the
+	 * underlying output stream.
+	 *
+	 * @param fillBit
+	 *            The lowest bit of this value will determine what the unused
+	 *            space in a byte is filled with
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void flush(int fillBit) throws IOException {
+		if (currentBitPosition != 0) {
+			currentValue &= (0xff >> (8 - currentBitPosition));
+			for (int bit = currentBitPosition; bit < 8; bit++) {
+				currentValue = Bits.encodeBits(currentValue, bit, 1, fillBit & 0x01);
+			}
+			currentBitPosition = 0;
+			super.write(currentValue);
+			currentValue = -1;
+		}
+		super.flush();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.io.FilterOutputStream#close()
+	 */
+	@Override
+	public void close() throws IOException {
+		flush();
+		super.close();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/Closer.java b/alien/src/net/pterodactylus/util/io/Closer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/Closer.java
@@ -0,0 +1,248 @@
+/*
+ * utils - Closer.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Helper class that can close all kinds of resources without throwing exception
+ * so that clean-up code can be written with less code. All methods check that
+ * the given resource is not <code>null</code> before invoking the close()
+ * method of the respective type.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Closer {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(Closer.class.getName());
+
+	/**
+	 * Closes the given result set.
+	 *
+	 * @param resultSet
+	 *            The result set to close
+	 * @see ResultSet#close()
+	 */
+	public static void close(ResultSet resultSet) {
+		if (resultSet != null) {
+			try {
+				resultSet.close();
+			} catch (SQLException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given statement.
+	 *
+	 * @param statement
+	 *            The statement to close
+	 * @see Statement#close()
+	 */
+	public static void close(Statement statement) {
+		if (statement != null) {
+			try {
+				statement.close();
+			} catch (SQLException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given connection.
+	 *
+	 * @param connection
+	 *            The connection to close
+	 * @see Connection#close()
+	 */
+	public static void close(Connection connection) {
+		if (connection != null) {
+			try {
+				connection.close();
+			} catch (SQLException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given server socket.
+	 *
+	 * @param serverSocket
+	 *            The server socket to close
+	 * @see ServerSocket#close()
+	 */
+	public static void close(ServerSocket serverSocket) {
+		if (serverSocket != null) {
+			try {
+				serverSocket.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given socket.
+	 *
+	 * @param socket
+	 *            The socket to close
+	 * @see Socket#close()
+	 */
+	public static void close(Socket socket) {
+		if (socket != null) {
+			try {
+				socket.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given input stream.
+	 *
+	 * @param inputStream
+	 *            The input stream to close
+	 * @see InputStream#close()
+	 */
+	public static void close(InputStream inputStream) {
+		if (inputStream != null) {
+			try {
+				inputStream.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given output stream.
+	 *
+	 * @param outputStream
+	 *            The output stream to close
+	 * @see OutputStream#close()
+	 */
+	public static void close(OutputStream outputStream) {
+		if (outputStream != null) {
+			try {
+				outputStream.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given reader.
+	 *
+	 * @param reader
+	 *            The reader to close
+	 * @see Reader#close()
+	 */
+	public static void close(Reader reader) {
+		if (reader != null) {
+			try {
+				reader.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given writer.
+	 *
+	 * @param writer
+	 *            The write to close
+	 * @see Writer#close()
+	 */
+	public static void close(Writer writer) {
+		if (writer != null) {
+			try {
+				writer.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Closes the given jar file.
+	 *
+	 * @param jarFile
+	 *            The JAR file to close
+	 * @see JarFile#close()
+	 */
+	public static void close(JarFile jarFile) {
+		if (jarFile != null) {
+			try {
+				jarFile.close();
+			} catch (IOException ioe1) {
+				/* ignore. */
+			}
+		}
+	}
+
+	/**
+	 * Tries to call the close() method on the given object.
+	 *
+	 * @param object
+	 *            The object to call the close() method on
+	 */
+	public static void close(Object object) {
+		if (object == null) {
+			return;
+		}
+		try {
+			Method closeMethod = object.getClass().getMethod("close");
+			closeMethod.invoke(object);
+		} catch (SecurityException se1) {
+			logger.log(Level.WARNING, "Could not call close() method on " + object, se1);
+		} catch (NoSuchMethodException e1) {
+			/* ignore. */
+		} catch (IllegalArgumentException iae1) {
+			logger.log(Level.WARNING, "Illegal argument for close() method on " + object, iae1);
+		} catch (IllegalAccessException iae1) {
+			logger.log(Level.WARNING, "Could not call close() method on " + object, iae1);
+		} catch (InvocationTargetException e1) {
+			/* ignore. */
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/LimitedInputStream.java b/alien/src/net/pterodactylus/util/io/LimitedInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/LimitedInputStream.java
@@ -0,0 +1,146 @@
+/*
+ * utils - LimitedInputStream.java - Copyright © 2008-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A wrapper around an {@link InputStream} that only supplies a limit number of
+ * bytes from the underlying input stream.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LimitedInputStream extends FilterInputStream {
+
+	/** The remaining number of bytes that can be read. */
+	private long remaining;
+
+	/**
+	 * Creates a new LimitedInputStream that supplies at most
+	 * <code>length</code> bytes from the given input stream.
+	 *
+	 * @param inputStream
+	 *            The input stream
+	 * @param length
+	 *            The number of bytes to read
+	 */
+	public LimitedInputStream(InputStream inputStream, long length) {
+		super(inputStream);
+		remaining = length;
+	}
+
+	/**
+	 * @see java.io.FilterInputStream#available()
+	 */
+	@Override
+	public synchronized int available() throws IOException {
+		if (remaining == 0) {
+			return 0;
+		}
+		return (int) Math.min(super.available(), Math.min(Integer.MAX_VALUE, remaining));
+	}
+
+	/**
+	 * @see java.io.FilterInputStream#read()
+	 */
+	@Override
+	public synchronized int read() throws IOException {
+		int read = -1;
+		if (remaining > 0) {
+			read = super.read();
+			remaining--;
+		}
+		return read;
+	}
+
+	/**
+	 * @see java.io.FilterInputStream#read(byte[], int, int)
+	 */
+	@Override
+	public synchronized int read(byte[] b, int off, int len) throws IOException {
+		if (remaining == 0) {
+			return -1;
+		}
+		int toCopy = (int) Math.min(len, Math.min(remaining, Integer.MAX_VALUE));
+		int read = super.read(b, off, toCopy);
+		remaining -= read;
+		return read;
+	}
+
+	/**
+	 * @see java.io.FilterInputStream#skip(long)
+	 */
+	@Override
+	public synchronized long skip(long n) throws IOException {
+		if ((n < 0) || (remaining == 0)) {
+			return 0;
+		}
+		long skipped = super.skip(Math.min(n, remaining));
+		remaining -= skipped;
+		return skipped;
+	}
+
+	/**
+	 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
+	 * {@link #reset()} are not supported.
+	 *
+	 * @see java.io.FilterInputStream#mark(int)
+	 */
+	@Override
+	public void mark(int readlimit) {
+		/* do nothing. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.io.FilterInputStream#markSupported()
+	 * @return <code>false</code>
+	 */
+	@Override
+	public boolean markSupported() {
+		return false;
+	}
+
+	/**
+	 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
+	 * {@link #reset()} are not supported.
+	 *
+	 * @see java.io.FilterInputStream#reset()
+	 */
+	@Override
+	public void reset() throws IOException {
+		/* do nothing. */
+	}
+
+	/**
+	 * Consumes the input stream, i.e. read all bytes until the limit is
+	 * reached.
+	 *
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void consume() throws IOException {
+		while (remaining > 0) {
+			skip(remaining);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/MimeTypes.java b/alien/src/net/pterodactylus/util/io/MimeTypes.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/MimeTypes.java
@@ -0,0 +1,834 @@
+/*
+ * utils - MimeTypes.java - Copyright © 2008-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Handles MIME types and maps them to file extensions.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MimeTypes {
+
+	/** The default MIME type for unknown extensions. */
+	public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
+
+	/** List of all MIME types. */
+	private static final List<String> mimeTypes = new ArrayList<String>();
+
+	/** Maps from MIME types to registered extensions. */
+	private static final Map<String, List<String>> mimeTypeExtensions = new HashMap<String, List<String>>();
+
+	/** Maps from extensions to registered MIME types. */
+	private static final Map<String, List<String>> extensionMimeTypes = new HashMap<String, List<String>>();
+
+	/* MIME type list generated from my /etc/mime.types. */
+	static {
+		addMimeType("application/activemessage");
+		addMimeType("application/andrew-inset", "ez");
+		addMimeType("application/applefile");
+		addMimeType("application/atomicmail");
+		addMimeType("application/batch-SMTP");
+		addMimeType("application/beep+xml");
+		addMimeType("application/cals-1840");
+		addMimeType("application/commonground");
+		addMimeType("application/cu-seeme", "cu");
+		addMimeType("application/cybercash");
+		addMimeType("application/dca-rft");
+		addMimeType("application/dec-dx");
+		addMimeType("application/docbook+xml");
+		addMimeType("application/dsptype", "tsp");
+		addMimeType("application/dvcs");
+		addMimeType("application/edi-consent");
+		addMimeType("application/edi-x12");
+		addMimeType("application/edifact");
+		addMimeType("application/eshop");
+		addMimeType("application/font-tdpfr");
+		addMimeType("application/futuresplash", "spl");
+		addMimeType("application/ghostview");
+		addMimeType("application/hta", "hta");
+		addMimeType("application/http");
+		addMimeType("application/hyperstudio");
+		addMimeType("application/iges");
+		addMimeType("application/index");
+		addMimeType("application/index.cmd");
+		addMimeType("application/index.obj");
+		addMimeType("application/index.response");
+		addMimeType("application/index.vnd");
+		addMimeType("application/iotp");
+		addMimeType("application/ipp");
+		addMimeType("application/isup");
+		addMimeType("application/java-archive", "jar");
+		addMimeType("application/java-serialized-object", "ser");
+		addMimeType("application/java-vm", "class");
+		addMimeType("application/mac-binhex40", "hqx");
+		addMimeType("application/mac-compactpro", "cpt");
+		addMimeType("application/macwriteii");
+		addMimeType("application/marc");
+		addMimeType("application/mathematica", "nb");
+		addMimeType("application/mathematica-old");
+		addMimeType("application/msaccess", "mdb");
+		addMimeType("application/msword", "doc", "dot");
+		addMimeType("application/news-message-id");
+		addMimeType("application/news-transmission");
+		addMimeType("application/ocsp-request");
+		addMimeType("application/ocsp-response");
+		addMimeType("application/octet-stream", "bin");
+		addMimeType("application/oda", "oda");
+		addMimeType("application/ogg", "ogg");
+		addMimeType("application/parityfec");
+		addMimeType("application/pdf", "pdf");
+		addMimeType("application/pgp-encrypted");
+		addMimeType("application/pgp-keys", "key");
+		addMimeType("application/pgp-signature", "pgp");
+		addMimeType("application/pics-rules", "prf");
+		addMimeType("application/pkcs10");
+		addMimeType("application/pkcs7-mime");
+		addMimeType("application/pkcs7-signature");
+		addMimeType("application/pkix-cert");
+		addMimeType("application/pkix-crl");
+		addMimeType("application/pkixcmp");
+		addMimeType("application/postscript", "ps", "ai", "eps");
+		addMimeType("application/prs.alvestrand.titrax-sheet");
+		addMimeType("application/prs.cww");
+		addMimeType("application/prs.nprend");
+		addMimeType("application/qsig");
+		addMimeType("application/rar", "rar");
+		addMimeType("application/rdf+xml", "rdf");
+		addMimeType("application/remote-printing");
+		addMimeType("application/riscos");
+		addMimeType("application/rss+xml", "rss");
+		addMimeType("application/rtf");
+		addMimeType("application/sdp");
+		addMimeType("application/set-payment");
+		addMimeType("application/set-payment-initiation");
+		addMimeType("application/set-registration");
+		addMimeType("application/set-registration-initiation");
+		addMimeType("application/sgml");
+		addMimeType("application/sgml-open-catalog");
+		addMimeType("application/sieve");
+		addMimeType("application/slate");
+		addMimeType("application/smil", "smi", "smil");
+		addMimeType("application/timestamp-query");
+		addMimeType("application/timestamp-reply");
+		addMimeType("application/vemmi");
+		addMimeType("application/whoispp-query");
+		addMimeType("application/whoispp-response");
+		addMimeType("application/wita");
+		addMimeType("application/wordperfect", "wpd");
+		addMimeType("application/wordperfect5.1", "wp5");
+		addMimeType("application/x400-bp");
+		addMimeType("application/xhtml+xml", "xhtml", "xht");
+		addMimeType("application/xml", "xml", "xsl");
+		addMimeType("application/xml-dtd");
+		addMimeType("application/xml-external-parsed-entity");
+		addMimeType("application/zip", "zip");
+		addMimeType("application/vnd.3M.Post-it-Notes");
+		addMimeType("application/vnd.accpac.simply.aso");
+		addMimeType("application/vnd.accpac.simply.imp");
+		addMimeType("application/vnd.acucobol");
+		addMimeType("application/vnd.aether.imp");
+		addMimeType("application/vnd.anser-web-certificate-issue-initiation");
+		addMimeType("application/vnd.anser-web-funds-transfer-initiation");
+		addMimeType("application/vnd.audiograph");
+		addMimeType("application/vnd.bmi");
+		addMimeType("application/vnd.businessobjects");
+		addMimeType("application/vnd.canon-cpdl");
+		addMimeType("application/vnd.canon-lips");
+		addMimeType("application/vnd.cinderella", "cdy");
+		addMimeType("application/vnd.claymore");
+		addMimeType("application/vnd.commerce-battelle");
+		addMimeType("application/vnd.commonspace");
+		addMimeType("application/vnd.comsocaller");
+		addMimeType("application/vnd.contact.cmsg");
+		addMimeType("application/vnd.cosmocaller");
+		addMimeType("application/vnd.ctc-posml");
+		addMimeType("application/vnd.cups-postscript");
+		addMimeType("application/vnd.cups-raster");
+		addMimeType("application/vnd.cups-raw");
+		addMimeType("application/vnd.cybank");
+		addMimeType("application/vnd.dna");
+		addMimeType("application/vnd.dpgraph");
+		addMimeType("application/vnd.dxr");
+		addMimeType("application/vnd.ecdis-update");
+		addMimeType("application/vnd.ecowin.chart");
+		addMimeType("application/vnd.ecowin.filerequest");
+		addMimeType("application/vnd.ecowin.fileupdate");
+		addMimeType("application/vnd.ecowin.series");
+		addMimeType("application/vnd.ecowin.seriesrequest");
+		addMimeType("application/vnd.ecowin.seriesupdate");
+		addMimeType("application/vnd.enliven");
+		addMimeType("application/vnd.epson.esf");
+		addMimeType("application/vnd.epson.msf");
+		addMimeType("application/vnd.epson.quickanime");
+		addMimeType("application/vnd.epson.salt");
+		addMimeType("application/vnd.epson.ssf");
+		addMimeType("application/vnd.ericsson.quickcall");
+		addMimeType("application/vnd.eudora.data");
+		addMimeType("application/vnd.fdf");
+		addMimeType("application/vnd.ffsns");
+		addMimeType("application/vnd.flographit");
+		addMimeType("application/vnd.framemaker");
+		addMimeType("application/vnd.fsc.weblaunch");
+		addMimeType("application/vnd.fujitsu.oasys");
+		addMimeType("application/vnd.fujitsu.oasys2");
+		addMimeType("application/vnd.fujitsu.oasys3");
+		addMimeType("application/vnd.fujitsu.oasysgp");
+		addMimeType("application/vnd.fujitsu.oasysprs");
+		addMimeType("application/vnd.fujixerox.ddd");
+		addMimeType("application/vnd.fujixerox.docuworks");
+		addMimeType("application/vnd.fujixerox.docuworks.binder");
+		addMimeType("application/vnd.fut-misnet");
+		addMimeType("application/vnd.grafeq");
+		addMimeType("application/vnd.groove-account");
+		addMimeType("application/vnd.groove-identity-message");
+		addMimeType("application/vnd.groove-injector");
+		addMimeType("application/vnd.groove-tool-message");
+		addMimeType("application/vnd.groove-tool-template");
+		addMimeType("application/vnd.groove-vcard");
+		addMimeType("application/vnd.hhe.lesson-player");
+		addMimeType("application/vnd.hp-HPGL");
+		addMimeType("application/vnd.hp-PCL");
+		addMimeType("application/vnd.hp-PCLXL");
+		addMimeType("application/vnd.hp-hpid");
+		addMimeType("application/vnd.hp-hps");
+		addMimeType("application/vnd.httphone");
+		addMimeType("application/vnd.hzn-3d-crossword");
+		addMimeType("application/vnd.ibm.MiniPay");
+		addMimeType("application/vnd.ibm.afplinedata");
+		addMimeType("application/vnd.ibm.modcap");
+		addMimeType("application/vnd.informix-visionary");
+		addMimeType("application/vnd.intercon.formnet");
+		addMimeType("application/vnd.intertrust.digibox");
+		addMimeType("application/vnd.intertrust.nncp");
+		addMimeType("application/vnd.intu.qbo");
+		addMimeType("application/vnd.intu.qfx");
+		addMimeType("application/vnd.irepository.package+xml");
+		addMimeType("application/vnd.is-xpr");
+		addMimeType("application/vnd.japannet-directory-service");
+		addMimeType("application/vnd.japannet-jpnstore-wakeup");
+		addMimeType("application/vnd.japannet-payment-wakeup");
+		addMimeType("application/vnd.japannet-registration");
+		addMimeType("application/vnd.japannet-registration-wakeup");
+		addMimeType("application/vnd.japannet-setstore-wakeup");
+		addMimeType("application/vnd.japannet-verification");
+		addMimeType("application/vnd.japannet-verification-wakeup");
+		addMimeType("application/vnd.koan");
+		addMimeType("application/vnd.lotus-1-2-3");
+		addMimeType("application/vnd.lotus-approach");
+		addMimeType("application/vnd.lotus-freelance");
+		addMimeType("application/vnd.lotus-notes");
+		addMimeType("application/vnd.lotus-organizer");
+		addMimeType("application/vnd.lotus-screencam");
+		addMimeType("application/vnd.lotus-wordpro");
+		addMimeType("application/vnd.mcd");
+		addMimeType("application/vnd.mediastation.cdkey");
+		addMimeType("application/vnd.meridian-slingshot");
+		addMimeType("application/vnd.mif");
+		addMimeType("application/vnd.minisoft-hp3000-save");
+		addMimeType("application/vnd.mitsubishi.misty-guard.trustweb");
+		addMimeType("application/vnd.mobius.daf");
+		addMimeType("application/vnd.mobius.dis");
+		addMimeType("application/vnd.mobius.msl");
+		addMimeType("application/vnd.mobius.plc");
+		addMimeType("application/vnd.mobius.txf");
+		addMimeType("application/vnd.motorola.flexsuite");
+		addMimeType("application/vnd.motorola.flexsuite.adsi");
+		addMimeType("application/vnd.motorola.flexsuite.fis");
+		addMimeType("application/vnd.motorola.flexsuite.gotap");
+		addMimeType("application/vnd.motorola.flexsuite.kmr");
+		addMimeType("application/vnd.motorola.flexsuite.ttc");
+		addMimeType("application/vnd.motorola.flexsuite.wem");
+		addMimeType("application/vnd.mozilla.xul+xml", "xul");
+		addMimeType("application/vnd.ms-artgalry");
+		addMimeType("application/vnd.ms-asf");
+		addMimeType("application/vnd.ms-excel", "xls", "xlb", "xlt");
+		addMimeType("application/vnd.ms-lrm");
+		addMimeType("application/vnd.ms-pki.seccat", "cat");
+		addMimeType("application/vnd.ms-pki.stl", "stl");
+		addMimeType("application/vnd.ms-powerpoint", "ppt", "pps");
+		addMimeType("application/vnd.ms-project");
+		addMimeType("application/vnd.ms-tnef");
+		addMimeType("application/vnd.ms-works");
+		addMimeType("application/vnd.mseq");
+		addMimeType("application/vnd.msign");
+		addMimeType("application/vnd.music-niff");
+		addMimeType("application/vnd.musician");
+		addMimeType("application/vnd.netfpx");
+		addMimeType("application/vnd.noblenet-directory");
+		addMimeType("application/vnd.noblenet-sealer");
+		addMimeType("application/vnd.noblenet-web");
+		addMimeType("application/vnd.novadigm.EDM");
+		addMimeType("application/vnd.novadigm.EDX");
+		addMimeType("application/vnd.novadigm.EXT");
+		addMimeType("application/vnd.oasis.opendocument.chart", "odc");
+		addMimeType("application/vnd.oasis.opendocument.database", "odb");
+		addMimeType("application/vnd.oasis.opendocument.formula", "odf");
+		addMimeType("application/vnd.oasis.opendocument.graphics", "odg");
+		addMimeType("application/vnd.oasis.opendocument.graphics-template", "otg");
+		addMimeType("application/vnd.oasis.opendocument.image", "odi");
+		addMimeType("application/vnd.oasis.opendocument.presentation", "odp");
+		addMimeType("application/vnd.oasis.opendocument.presentation-template", "otp");
+		addMimeType("application/vnd.oasis.opendocument.spreadsheet", "ods");
+		addMimeType("application/vnd.oasis.opendocument.spreadsheet-template", "ots");
+		addMimeType("application/vnd.oasis.opendocument.text", "odt");
+		addMimeType("application/vnd.oasis.opendocument.text-master", "odm");
+		addMimeType("application/vnd.oasis.opendocument.text-template", "ott");
+		addMimeType("application/vnd.oasis.opendocument.text-web", "oth");
+		addMimeType("application/vnd.osa.netdeploy");
+		addMimeType("application/vnd.palm");
+		addMimeType("application/vnd.pg.format");
+		addMimeType("application/vnd.pg.osasli");
+		addMimeType("application/vnd.powerbuilder6");
+		addMimeType("application/vnd.powerbuilder6-s");
+		addMimeType("application/vnd.powerbuilder7");
+		addMimeType("application/vnd.powerbuilder7-s");
+		addMimeType("application/vnd.powerbuilder75");
+		addMimeType("application/vnd.powerbuilder75-s");
+		addMimeType("application/vnd.previewsystems.box");
+		addMimeType("application/vnd.publishare-delta-tree");
+		addMimeType("application/vnd.pvi.ptid1");
+		addMimeType("application/vnd.pwg-xhtml-print+xml");
+		addMimeType("application/vnd.rapid");
+		addMimeType("application/vnd.rim.cod", "cod");
+		addMimeType("application/vnd.s3sms");
+		addMimeType("application/vnd.seemail");
+		addMimeType("application/vnd.shana.informed.formdata");
+		addMimeType("application/vnd.shana.informed.formtemplate");
+		addMimeType("application/vnd.shana.informed.interchange");
+		addMimeType("application/vnd.shana.informed.package");
+		addMimeType("application/vnd.smaf", "mmf");
+		addMimeType("application/vnd.sss-cod");
+		addMimeType("application/vnd.sss-dtf");
+		addMimeType("application/vnd.sss-ntf");
+		addMimeType("application/vnd.stardivision.calc", "sdc");
+		addMimeType("application/vnd.stardivision.draw", "sda");
+		addMimeType("application/vnd.stardivision.impress", "sdd", "sdp");
+		addMimeType("application/vnd.stardivision.math", "smf");
+		addMimeType("application/vnd.stardivision.writer", "sdw", "vor");
+		addMimeType("application/vnd.stardivision.writer-global", "sgl");
+		addMimeType("application/vnd.street-stream");
+		addMimeType("application/vnd.sun.xml.calc", "sxc");
+		addMimeType("application/vnd.sun.xml.calc.template", "stc");
+		addMimeType("application/vnd.sun.xml.draw", "sxd");
+		addMimeType("application/vnd.sun.xml.draw.template", "std");
+		addMimeType("application/vnd.sun.xml.impress", "sxi");
+		addMimeType("application/vnd.sun.xml.impress.template", "sti");
+		addMimeType("application/vnd.sun.xml.math", "sxm");
+		addMimeType("application/vnd.sun.xml.writer", "sxw");
+		addMimeType("application/vnd.sun.xml.writer.global", "sxg");
+		addMimeType("application/vnd.sun.xml.writer.template", "stw");
+		addMimeType("application/vnd.svd");
+		addMimeType("application/vnd.swiftview-ics");
+		addMimeType("application/vnd.symbian.install", "sis");
+		addMimeType("application/vnd.triscape.mxs");
+		addMimeType("application/vnd.trueapp");
+		addMimeType("application/vnd.truedoc");
+		addMimeType("application/vnd.tve-trigger");
+		addMimeType("application/vnd.ufdl");
+		addMimeType("application/vnd.uplanet.alert");
+		addMimeType("application/vnd.uplanet.alert-wbxml");
+		addMimeType("application/vnd.uplanet.bearer-choice");
+		addMimeType("application/vnd.uplanet.bearer-choice-wbxml");
+		addMimeType("application/vnd.uplanet.cacheop");
+		addMimeType("application/vnd.uplanet.cacheop-wbxml");
+		addMimeType("application/vnd.uplanet.channel");
+		addMimeType("application/vnd.uplanet.channel-wbxml");
+		addMimeType("application/vnd.uplanet.list");
+		addMimeType("application/vnd.uplanet.list-wbxml");
+		addMimeType("application/vnd.uplanet.listcmd");
+		addMimeType("application/vnd.uplanet.listcmd-wbxml");
+		addMimeType("application/vnd.uplanet.signal");
+		addMimeType("application/vnd.vcx");
+		addMimeType("application/vnd.vectorworks");
+		addMimeType("application/vnd.vidsoft.vidconference");
+		addMimeType("application/vnd.visio", "vsd");
+		addMimeType("application/vnd.vividence.scriptfile");
+		addMimeType("application/vnd.wap.sic");
+		addMimeType("application/vnd.wap.slc");
+		addMimeType("application/vnd.wap.wbxml", "wbxml");
+		addMimeType("application/vnd.wap.wmlc", "wmlc");
+		addMimeType("application/vnd.wap.wmlscriptc", "wmlsc");
+		addMimeType("application/vnd.webturbo");
+		addMimeType("application/vnd.wrq-hp3000-labelled");
+		addMimeType("application/vnd.wt.stf");
+		addMimeType("application/vnd.xara");
+		addMimeType("application/vnd.xfdl");
+		addMimeType("application/vnd.yellowriver-custom-menu");
+		addMimeType("application/x-123", "wk");
+		addMimeType("application/x-abiword", "abw");
+		addMimeType("application/x-apple-diskimage", "dmg");
+		addMimeType("application/x-bcpio", "bcpio");
+		addMimeType("application/x-bittorrent", "torrent");
+		addMimeType("application/x-cdf", "cdf");
+		addMimeType("application/x-cdlink", "vcd");
+		addMimeType("application/x-chess-pgn", "pgn");
+		addMimeType("application/x-core");
+		addMimeType("application/x-cpio", "cpio");
+		addMimeType("application/x-csh", "csh");
+		addMimeType("application/x-debian-package", "deb", "udeb");
+		addMimeType("application/x-director", "dcr", "dir", "dxr");
+		addMimeType("application/x-dms", "dms");
+		addMimeType("application/x-doom", "wad");
+		addMimeType("application/x-dvi", "dvi");
+		addMimeType("application/x-executable");
+		addMimeType("application/x-flac", "flac");
+		addMimeType("application/x-font", "pfa", "pfb", "gsf", "pcf", "pcf.Z");
+		addMimeType("application/x-freemind", "mm");
+		addMimeType("application/x-futuresplash", "spl");
+		addMimeType("application/x-gnumeric", "gnumeric");
+		addMimeType("application/x-go-sgf", "sgf");
+		addMimeType("application/x-graphing-calculator", "gcf");
+		addMimeType("application/x-gtar", "gtar", "tgz", "taz");
+		addMimeType("application/x-hdf", "hdf");
+		addMimeType("application/x-ica", "ica");
+		addMimeType("application/x-internet-signup", "ins", "isp");
+		addMimeType("application/x-iphone", "iii");
+		addMimeType("application/x-iso9660-image", "iso");
+		addMimeType("application/x-java-applet");
+		addMimeType("application/x-java-bean");
+		addMimeType("application/x-java-jnlp-file", "jnlp");
+		addMimeType("application/x-javascript", "js");
+		addMimeType("application/x-jmol", "jmz");
+		addMimeType("application/x-kchart", "chrt");
+		addMimeType("application/x-kdelnk");
+		addMimeType("application/x-killustrator", "kil");
+		addMimeType("application/x-koan", "skp", "skd", "skt", "skm");
+		addMimeType("application/x-kpresenter", "kpr", "kpt");
+		addMimeType("application/x-kspread", "ksp");
+		addMimeType("application/x-kword", "kwd", "kwt");
+		addMimeType("application/x-latex", "latex");
+		addMimeType("application/x-lha", "lha");
+		addMimeType("application/x-lzh", "lzh");
+		addMimeType("application/x-lzx", "lzx");
+		addMimeType("application/x-maker", "frm", "maker", "frame", "fm", "fb", "book", "fbdoc");
+		addMimeType("application/x-mif", "mif");
+		addMimeType("application/x-ms-wmd", "wmd");
+		addMimeType("application/x-ms-wmz", "wmz");
+		addMimeType("application/x-msdos-program", "com", "exe", "bat", "dll");
+		addMimeType("application/x-msi", "msi");
+		addMimeType("application/x-netcdf", "nc");
+		addMimeType("application/x-ns-proxy-autoconfig", "pac");
+		addMimeType("application/x-nwc", "nwc");
+		addMimeType("application/x-object", "o");
+		addMimeType("application/x-oz-application", "oza");
+		addMimeType("application/x-pkcs7-certreqresp", "p7r");
+		addMimeType("application/x-pkcs7-crl", "crl");
+		addMimeType("application/x-python-code", "pyc", "pyo");
+		addMimeType("application/x-quicktimeplayer", "qtl");
+		addMimeType("application/x-redhat-package-manager", "rpm");
+		addMimeType("application/x-rx");
+		addMimeType("application/x-sh", "sh");
+		addMimeType("application/x-shar", "shar");
+		addMimeType("application/x-shellscript");
+		addMimeType("application/x-shockwave-flash", "swf", "swfl");
+		addMimeType("application/x-stuffit", "sit");
+		addMimeType("application/x-sv4cpio", "sv4cpio");
+		addMimeType("application/x-sv4crc", "sv4crc");
+		addMimeType("application/x-tar", "tar");
+		addMimeType("application/x-tcl", "tcl");
+		addMimeType("application/x-tex-gf", "gf");
+		addMimeType("application/x-tex-pk", "pk");
+		addMimeType("application/x-texinfo", "texinfo", "texi");
+		addMimeType("application/x-trash", "~", "%", "bak", "old", "sik");
+		addMimeType("application/x-troff", "t", "tr", "roff");
+		addMimeType("application/x-troff-man", "man");
+		addMimeType("application/x-troff-me", "me");
+		addMimeType("application/x-troff-ms", "ms");
+		addMimeType("application/x-ustar", "ustar");
+		addMimeType("application/x-videolan");
+		addMimeType("application/x-wais-source", "src");
+		addMimeType("application/x-wingz", "wz");
+		addMimeType("application/x-x509-ca-cert", "crt");
+		addMimeType("application/x-xcf", "xcf");
+		addMimeType("application/x-xfig", "fig");
+		addMimeType("application/x-xpinstall", "xpi");
+		addMimeType("audio/32kadpcm");
+		addMimeType("audio/basic", "au", "snd");
+		addMimeType("audio/dvi4");
+		addMimeType("audio/g.722.1");
+		addMimeType("audio/g722");
+		addMimeType("audio/g723");
+		addMimeType("audio/g726-16");
+		addMimeType("audio/g726-24");
+		addMimeType("audio/g726-32");
+		addMimeType("audio/g726-40");
+		addMimeType("audio/g728");
+		addMimeType("audio/g729");
+		addMimeType("audio/g729d");
+		addMimeType("audio/g729e");
+		addMimeType("audio/gsm");
+		addMimeType("audio/gsm-efr");
+		addMimeType("audio/l8");
+		addMimeType("audio/l16");
+		addMimeType("audio/lpc");
+		addMimeType("audio/midi", "mid", "midi", "kar");
+		addMimeType("audio/mp4a-latm");
+		addMimeType("audio/mpa");
+		addMimeType("audio/mpa-robust");
+		addMimeType("audio/mpeg", "mpga", "mpega", "mp2", "mp3", "m4a");
+		addMimeType("audio/mpegurl", "m3u");
+		addMimeType("audio/parityfec");
+		addMimeType("audio/pcma");
+		addMimeType("audio/pcmu");
+		addMimeType("audio/prs.sid", "sid");
+		addMimeType("audio/qcelp");
+		addMimeType("audio/red");
+		addMimeType("audio/telephone-event");
+		addMimeType("audio/tone");
+		addMimeType("audio/vdvi");
+		addMimeType("audio/vnd.cisco.nse");
+		addMimeType("audio/vnd.cns.anp1");
+		addMimeType("audio/vnd.cns.inf1");
+		addMimeType("audio/vnd.digital-winds");
+		addMimeType("audio/vnd.everad.plj");
+		addMimeType("audio/vnd.lucent.voice");
+		addMimeType("audio/vnd.nortel.vbk");
+		addMimeType("audio/vnd.nuera.ecelp4800");
+		addMimeType("audio/vnd.nuera.ecelp7470");
+		addMimeType("audio/vnd.nuera.ecelp9600");
+		addMimeType("audio/vnd.octel.sbc");
+		addMimeType("audio/vnd.qcelp");
+		addMimeType("audio/vnd.rhetorex.32kadpcm");
+		addMimeType("audio/vnd.vmx.cvsd");
+		addMimeType("audio/x-aiff", "aif", "aiff", "aifc");
+		addMimeType("audio/x-gsm", "gsm");
+		addMimeType("audio/x-mpegurl", "m3u");
+		addMimeType("audio/x-ms-wma", "wma");
+		addMimeType("audio/x-ms-wax", "wax");
+		addMimeType("audio/x-pn-realaudio-plugin");
+		addMimeType("audio/x-pn-realaudio", "ra", "rm", "ram");
+		addMimeType("audio/x-realaudio", "ra");
+		addMimeType("audio/x-scpls", "pls");
+		addMimeType("audio/x-sd2", "sd2");
+		addMimeType("audio/x-wav", "wav");
+		addMimeType("chemical/x-alchemy", "alc");
+		addMimeType("chemical/x-cache", "cac", "cache");
+		addMimeType("chemical/x-cache-csf", "csf");
+		addMimeType("chemical/x-cactvs-binary", "cbin", "cascii", "ctab");
+		addMimeType("chemical/x-cdx", "cdx");
+		addMimeType("chemical/x-cerius", "cer");
+		addMimeType("chemical/x-chem3d", "c3d");
+		addMimeType("chemical/x-chemdraw", "chm");
+		addMimeType("chemical/x-cif", "cif");
+		addMimeType("chemical/x-cmdf", "cmdf");
+		addMimeType("chemical/x-cml", "cml");
+		addMimeType("chemical/x-compass", "cpa");
+		addMimeType("chemical/x-crossfire", "bsd");
+		addMimeType("chemical/x-csml", "csml", "csm");
+		addMimeType("chemical/x-ctx", "ctx");
+		addMimeType("chemical/x-cxf", "cxf", "cef");
+		addMimeType("chemical/x-embl-dl-nucleotide", "emb", "embl");
+		addMimeType("chemical/x-galactic-spc", "spc");
+		addMimeType("chemical/x-gamess-input", "inp", "gam", "gamin");
+		addMimeType("chemical/x-gaussian-checkpoint", "fch", "fchk");
+		addMimeType("chemical/x-gaussian-cube", "cub");
+		addMimeType("chemical/x-gaussian-input", "gau", "gjc", "gjf");
+		addMimeType("chemical/x-gaussian-log", "gal");
+		addMimeType("chemical/x-gcg8-sequence", "gcg");
+		addMimeType("chemical/x-genbank", "gen");
+		addMimeType("chemical/x-hin", "hin");
+		addMimeType("chemical/x-isostar", "istr", "ist");
+		addMimeType("chemical/x-jcamp-dx", "jdx", "dx");
+		addMimeType("chemical/x-kinemage", "kin");
+		addMimeType("chemical/x-macmolecule", "mcm");
+		addMimeType("chemical/x-macromodel-input", "mmd", "mmod");
+		addMimeType("chemical/x-mdl-molfile", "mol");
+		addMimeType("chemical/x-mdl-rdfile", "rd");
+		addMimeType("chemical/x-mdl-rxnfile", "rxn");
+		addMimeType("chemical/x-mdl-sdfile", "sd", "sdf");
+		addMimeType("chemical/x-mdl-tgf", "tgf");
+		addMimeType("chemical/x-mmcif", "mcif");
+		addMimeType("chemical/x-mol2", "mol2");
+		addMimeType("chemical/x-molconn-Z", "b");
+		addMimeType("chemical/x-mopac-graph", "gpt");
+		addMimeType("chemical/x-mopac-input", "mop", "mopcrt", "mpc", "dat", "zmt");
+		addMimeType("chemical/x-mopac-out", "moo");
+		addMimeType("chemical/x-mopac-vib", "mvb");
+		addMimeType("chemical/x-ncbi-asn1", "asn");
+		addMimeType("chemical/x-ncbi-asn1-ascii", "prt", "ent");
+		addMimeType("chemical/x-ncbi-asn1-binary", "val", "aso");
+		addMimeType("chemical/x-ncbi-asn1-spec", "asn");
+		addMimeType("chemical/x-pdb", "pdb", "ent");
+		addMimeType("chemical/x-rosdal", "ros");
+		addMimeType("chemical/x-swissprot", "sw");
+		addMimeType("chemical/x-vamas-iso14976", "vms");
+		addMimeType("chemical/x-vmd", "vmd");
+		addMimeType("chemical/x-xtel", "xtel");
+		addMimeType("chemical/x-xyz", "xyz");
+		addMimeType("image/cgm");
+		addMimeType("image/g3fax");
+		addMimeType("image/gif", "gif");
+		addMimeType("image/ief", "ief");
+		addMimeType("image/jpeg", "jpeg", "jpg", "jpe");
+		addMimeType("image/naplps");
+		addMimeType("image/pcx", "pcx");
+		addMimeType("image/png", "png");
+		addMimeType("image/prs.btif");
+		addMimeType("image/prs.pti");
+		addMimeType("image/svg+xml", "svg", "svgz");
+		addMimeType("image/tiff", "tiff", "tif");
+		addMimeType("image/vnd.cns.inf2");
+		addMimeType("image/vnd.djvu", "djvu", "djv");
+		addMimeType("image/vnd.dwg");
+		addMimeType("image/vnd.dxf");
+		addMimeType("image/vnd.fastbidsheet");
+		addMimeType("image/vnd.fpx");
+		addMimeType("image/vnd.fst");
+		addMimeType("image/vnd.fujixerox.edmics-mmr");
+		addMimeType("image/vnd.fujixerox.edmics-rlc");
+		addMimeType("image/vnd.mix");
+		addMimeType("image/vnd.net-fpx");
+		addMimeType("image/vnd.svf");
+		addMimeType("image/vnd.wap.wbmp", "wbmp");
+		addMimeType("image/vnd.xiff");
+		addMimeType("image/x-cmu-raster", "ras");
+		addMimeType("image/x-coreldraw", "cdr");
+		addMimeType("image/x-coreldrawpattern", "pat");
+		addMimeType("image/x-coreldrawtemplate", "cdt");
+		addMimeType("image/x-corelphotopaint", "cpt");
+		addMimeType("image/x-icon", "ico");
+		addMimeType("image/x-jg", "art");
+		addMimeType("image/x-jng", "jng");
+		addMimeType("image/x-ms-bmp", "bmp");
+		addMimeType("image/x-photoshop", "psd");
+		addMimeType("image/x-portable-anymap", "pnm");
+		addMimeType("image/x-portable-bitmap", "pbm");
+		addMimeType("image/x-portable-graymap", "pgm");
+		addMimeType("image/x-portable-pixmap", "ppm");
+		addMimeType("image/x-rgb", "rgb");
+		addMimeType("image/x-xbitmap", "xbm");
+		addMimeType("image/x-xpixmap", "xpm");
+		addMimeType("image/x-xwindowdump", "xwd");
+		addMimeType("inode/chardevice");
+		addMimeType("inode/blockdevice");
+		addMimeType("inode/directory-locked");
+		addMimeType("inode/directory");
+		addMimeType("inode/fifo");
+		addMimeType("inode/socket");
+		addMimeType("message/delivery-status");
+		addMimeType("message/disposition-notification");
+		addMimeType("message/external-body");
+		addMimeType("message/http");
+		addMimeType("message/s-http");
+		addMimeType("message/news");
+		addMimeType("message/partial");
+		addMimeType("message/rfc822");
+		addMimeType("model/iges", "igs", "iges");
+		addMimeType("model/mesh", "msh", "mesh", "silo");
+		addMimeType("model/vnd.dwf");
+		addMimeType("model/vnd.flatland.3dml");
+		addMimeType("model/vnd.gdl");
+		addMimeType("model/vnd.gs-gdl");
+		addMimeType("model/vnd.gtw");
+		addMimeType("model/vnd.mts");
+		addMimeType("model/vnd.vtu");
+		addMimeType("model/vrml", "wrl", "vrml");
+		addMimeType("multipart/alternative");
+		addMimeType("multipart/appledouble");
+		addMimeType("multipart/byteranges");
+		addMimeType("multipart/digest");
+		addMimeType("multipart/encrypted");
+		addMimeType("multipart/form-data");
+		addMimeType("multipart/header-set");
+		addMimeType("multipart/mixed");
+		addMimeType("multipart/parallel");
+		addMimeType("multipart/related");
+		addMimeType("multipart/report");
+		addMimeType("multipart/signed");
+		addMimeType("multipart/voice-message");
+		addMimeType("text/calendar", "ics", "icz");
+		addMimeType("text/comma-separated-values", "csv");
+		addMimeType("text/css", "css");
+		addMimeType("text/directory");
+		addMimeType("text/english");
+		addMimeType("text/enriched");
+		addMimeType("text/h323", "323");
+		addMimeType("text/html", "html", "htm", "shtml");
+		addMimeType("text/iuls", "uls");
+		addMimeType("text/mathml", "mml");
+		addMimeType("text/parityfec");
+		addMimeType("text/plain", "asc", "txt", "text", "diff", "pot");
+		addMimeType("text/prs.lines.tag");
+		addMimeType("text/x-psp", "psp");
+		addMimeType("text/rfc822-headers");
+		addMimeType("text/richtext", "rtx");
+		addMimeType("text/rtf", "rtf");
+		addMimeType("text/scriptlet", "sct", "wsc");
+		addMimeType("text/t140");
+		addMimeType("text/texmacs", "tm", "ts");
+		addMimeType("text/tab-separated-values", "tsv");
+		addMimeType("text/uri-list");
+		addMimeType("text/vnd.abc");
+		addMimeType("text/vnd.curl");
+		addMimeType("text/vnd.DMClientScript");
+		addMimeType("text/vnd.flatland.3dml");
+		addMimeType("text/vnd.fly");
+		addMimeType("text/vnd.fmi.flexstor");
+		addMimeType("text/vnd.in3d.3dml");
+		addMimeType("text/vnd.in3d.spot");
+		addMimeType("text/vnd.IPTC.NewsML");
+		addMimeType("text/vnd.IPTC.NITF");
+		addMimeType("text/vnd.latex-z");
+		addMimeType("text/vnd.motorola.reflex");
+		addMimeType("text/vnd.ms-mediapackage");
+		addMimeType("text/vnd.sun.j2me.app-descriptor", "jad");
+		addMimeType("text/vnd.wap.si");
+		addMimeType("text/vnd.wap.sl");
+		addMimeType("text/vnd.wap.wml", "wml");
+		addMimeType("text/vnd.wap.wmlscript", "wmls");
+		addMimeType("text/x-bibtex", "bib");
+		addMimeType("text/x-c++hdr", "h++", "hpp", "hxx", "hh");
+		addMimeType("text/x-c++src", "c++", "cpp", "cxx", "cc");
+		addMimeType("text/x-chdr", "h");
+		addMimeType("text/x-crontab");
+		addMimeType("text/x-csh", "csh");
+		addMimeType("text/x-csrc", "c");
+		addMimeType("text/x-haskell", "hs");
+		addMimeType("text/x-java", "java");
+		addMimeType("text/x-literate-haskell", "lhs");
+		addMimeType("text/x-makefile");
+		addMimeType("text/x-moc", "moc");
+		addMimeType("text/x-pascal", "p", "pas");
+		addMimeType("text/x-pcs-gcd", "gcd");
+		addMimeType("text/x-perl", "pl", "pm");
+		addMimeType("text/x-python", "py");
+		addMimeType("text/x-server-parsed-html");
+		addMimeType("text/x-setext", "etx");
+		addMimeType("text/x-sh", "sh");
+		addMimeType("text/x-tcl", "tcl", "tk");
+		addMimeType("text/x-tex", "tex", "ltx", "sty", "cls");
+		addMimeType("text/x-vcalendar", "vcs");
+		addMimeType("text/x-vcard", "vcf");
+		addMimeType("video/bmpeg");
+		addMimeType("video/bt656");
+		addMimeType("video/celb");
+		addMimeType("video/dl", "dl");
+		addMimeType("video/dv", "dif", "dv");
+		addMimeType("video/fli", "fli");
+		addMimeType("video/gl", "gl");
+		addMimeType("video/jpeg");
+		addMimeType("video/h261");
+		addMimeType("video/h263");
+		addMimeType("video/h263-1998");
+		addMimeType("video/h263-2000");
+		addMimeType("video/mp1s");
+		addMimeType("video/mp2p");
+		addMimeType("video/mp2t");
+		addMimeType("video/mp4", "mp4");
+		addMimeType("video/mp4v-es");
+		addMimeType("video/mpeg", "mpeg", "mpg", "mpe");
+		addMimeType("video/mpv");
+		addMimeType("video/nv");
+		addMimeType("video/parityfec");
+		addMimeType("video/pointer");
+		addMimeType("video/quicktime", "qt", "mov");
+		addMimeType("video/vnd.fvt");
+		addMimeType("video/vnd.motorola.video");
+		addMimeType("video/vnd.motorola.videop");
+		addMimeType("video/vnd.mpegurl", "mxu");
+		addMimeType("video/vnd.mts");
+		addMimeType("video/vnd.nokia.interleaved-multimedia");
+		addMimeType("video/vnd.vivo");
+		addMimeType("video/x-la-asf", "lsf", "lsx");
+		addMimeType("video/x-mng", "mng");
+		addMimeType("video/x-ms-asf", "asf", "asx");
+		addMimeType("video/x-ms-wm", "wm");
+		addMimeType("video/x-ms-wmv", "wmv");
+		addMimeType("video/x-ms-wmx", "wmx");
+		addMimeType("video/x-ms-wvx", "wvx");
+		addMimeType("video/x-msvideo", "avi");
+		addMimeType("video/x-sgi-movie", "movie");
+		addMimeType("video/x-flv", "flv");
+		addMimeType("x-conference/x-cooltalk", "ice");
+		addMimeType("x-world/x-vrml", "vrm", "vrml", "wrl");
+	}
+
+	/**
+	 * Returns a list of all known MIME types.
+	 *
+	 * @return All known MIME types
+	 */
+	public static List<String> getAllMimeTypes() {
+		return new ArrayList<String>(mimeTypes);
+	}
+
+	/**
+	 * Returns a list of MIME types that are registered for the given extension.
+	 *
+	 * @param extension
+	 *            The extension to get the MIME types for
+	 * @return A list of MIME types, or an empty list if there are no registered
+	 *         MIME types for the extension
+	 */
+	public static List<String> getMimeTypes(String extension) {
+		if (extensionMimeTypes.containsKey(extension)) {
+			return extensionMimeTypes.get(extension);
+		}
+		return Collections.emptyList();
+	}
+
+	/**
+	 * Returns a default MIME type for the given extension. If the extension
+	 * does not match a MIME type the default MIME typ
+	 * “application/octet-stream” is returned.
+	 *
+	 * @param extension
+	 *            The extension to get the MIME type for
+	 * @return The MIME type for the extension, or the default MIME type
+	 *         “application/octet-stream”
+	 */
+	public static String getMimeType(String extension) {
+		if (extensionMimeTypes.containsKey(extension)) {
+			return extensionMimeTypes.get(extension).get(0);
+		}
+		return DEFAULT_CONTENT_TYPE;
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Adds a MIME type and optional extensions.
+	 *
+	 * @param mimeType
+	 *            The MIME type to add
+	 * @param extensions
+	 *            The extension the MIME type is registered for
+	 */
+	private static void addMimeType(String mimeType, String... extensions) {
+		mimeTypes.add(mimeType);
+		for (String extension : extensions) {
+			if (!mimeTypeExtensions.containsKey(mimeType)) {
+				mimeTypeExtensions.put(mimeType, new ArrayList<String>());
+			}
+			mimeTypeExtensions.get(mimeType).add(extension);
+			if (!extensionMimeTypes.containsKey(extension)) {
+				extensionMimeTypes.put(extension, new ArrayList<String>());
+			}
+			extensionMimeTypes.get(extension).add(mimeType);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/Renderable.java b/alien/src/net/pterodactylus/util/io/Renderable.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/Renderable.java
@@ -0,0 +1,41 @@
+/*
+ * utils - Renderable.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Interface for objects that can render themselves to a {@link Writer}. It is
+ * not suitable for objects that want to write binary output.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Renderable {
+
+	/**
+	 * Renders this object to the given writer.
+	 *
+	 * @param writer
+	 *            The writer to render the object to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public void render(Writer writer) throws IOException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/StreamCopier.java b/alien/src/net/pterodactylus/util/io/StreamCopier.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/StreamCopier.java
@@ -0,0 +1,130 @@
+/*
+ * utils - Closer.java - Copyright © 2006-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Helper class that copies bytes from an {@link InputStream} to an
+ * {@link OutputStream}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StreamCopier {
+
+	/** Default buffer size is 64k. */
+	private static final int DEFAULT_BUFFER_SIZE = 1 << 16;
+
+	/** The current buffer size. */
+	private static int bufferSize = DEFAULT_BUFFER_SIZE;
+
+	/**
+	 * Sets the buffer size for following transfers.
+	 *
+	 * @param bufferSize
+	 *            The new buffer size
+	 */
+	public static void setBufferSize(int bufferSize) {
+		StreamCopier.bufferSize = bufferSize;
+	}
+
+	/**
+	 * Copies <code>length</code> bytes from the source input stream to the
+	 * destination output stream. If <code>length</code> is <code>-1</code> as
+	 * much bytes as possible will be copied (i.e. until
+	 * {@link InputStream#read()} returns <code>-1</code> to signal the end of
+	 * the stream).
+	 *
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @param length
+	 *            The number of bytes to copy
+	 * @return The number of bytes that have been read from the input stream and
+	 *         written to the output stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static long copy(InputStream source, OutputStream destination, long length) throws IOException {
+		long remaining = length;
+		byte[] buffer = new byte[bufferSize];
+		long total = 0;
+		int read = 0;
+		while ((remaining == -1) || (remaining > 0)) {
+			read = source.read(buffer, 0, ((remaining > bufferSize) || (remaining == -1)) ? bufferSize : (int) remaining);
+			if (read == -1) {
+				if (length == -1) {
+					return total;
+				}
+				throw new EOFException("stream reached eof");
+			}
+			destination.write(buffer, 0, read);
+			if (remaining > -1) {
+				remaining -= read;
+			}
+			total += read;
+		}
+		return total;
+	}
+
+	/**
+	 * Copies as much bytes as possible (i.e. until {@link InputStream#read()}
+	 * returns <code>-1</code>) from the source input stream to the destination
+	 * output stream.
+	 *
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @return The number of bytes that have been read from the input stream and
+	 *         written to the output stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static long copy(InputStream source, OutputStream destination) throws IOException {
+		return copy(source, destination, -1);
+	}
+
+	/**
+	 * Finds the length of the input stream by reading until
+	 * {@link InputStream#read(byte[])} returns <code>-1</code>.
+	 *
+	 * @param source
+	 *            The input stream to measure
+	 * @return The length of the input stream in bytes
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static long findLength(InputStream source) throws IOException {
+		long length = 0;
+		byte[] buffer = new byte[bufferSize];
+		int read = 0;
+		while (read != -1) {
+			read = source.read(buffer);
+			if (read != -1) {
+				length += read;
+			}
+		}
+		return length;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/TeeOutputStream.java b/alien/src/net/pterodactylus/util/io/TeeOutputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/TeeOutputStream.java
@@ -0,0 +1,125 @@
+/*
+ * utils - TeeOutputStream.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * {@link OutputStream} that sends all data it receives to multiple other output
+ * streams. If an error occurs during a {@link #write(int)} to one of the
+ * underlying output streams no guarantees are made about how much data is sent
+ * to each stream, i.e. there is no good way to recover from such an error.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TeeOutputStream extends OutputStream {
+
+	/** The output streams. */
+	private final OutputStream[] outputStreams;
+
+	/**
+	 * Creates a new tee output stream that sends all to all given output
+	 * streams.
+	 *
+	 * @param outputStreams
+	 *            The output streams to send all data to
+	 */
+	public TeeOutputStream(OutputStream... outputStreams) {
+		this.outputStreams = outputStreams;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void write(int data) throws IOException {
+		for (OutputStream outputStream : outputStreams) {
+			outputStream.write(data);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void write(byte[] buffer) throws IOException {
+		for (OutputStream outputStream : outputStreams) {
+			outputStream.write(buffer);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void write(byte[] buffer, int offset, int length) throws IOException {
+		for (OutputStream outputStream : outputStreams) {
+			outputStream.write(buffer, offset, length);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * An effort is made to flush all output streams. If multiple exceptions
+	 * occur, only the first exception is thrown after all output streams have
+	 * been tried to flush.
+	 */
+	@Override
+	public void flush() throws IOException {
+		IOException occuredException = null;
+		for (OutputStream outputStream : outputStreams) {
+			try {
+				outputStream.flush();
+			} catch (IOException ioe1) {
+				if (occuredException == null) {
+					occuredException = ioe1;
+				}
+			}
+		}
+		if (occuredException != null) {
+			throw occuredException;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * An effort is made to close all output streams. If multiple exceptions
+	 * occur, only the first exception is thrown after all output streams have
+	 * been tried to close.
+	 */
+	@Override
+	public void close() throws IOException {
+		IOException occuredException = null;
+		for (OutputStream outputStream : outputStreams) {
+			try {
+				outputStream.flush();
+			} catch (IOException ioe1) {
+				if (occuredException == null) {
+					occuredException = ioe1;
+				}
+			}
+		}
+		if (occuredException != null) {
+			throw occuredException;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/io/TemporaryInputStream.java b/alien/src/net/pterodactylus/util/io/TemporaryInputStream.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/io/TemporaryInputStream.java
@@ -0,0 +1,148 @@
+/*
+ * utils - TemporaryInputStream.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.io;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An input stream implementation that copies a given input stream to a
+ * temporary file and delivers the content of the temporary file at a later
+ * time.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemporaryInputStream extends FilterInputStream {
+
+	/**
+	 * Maps input streams to temporary files, for deletion on {@link #close()}.
+	 */
+	private static final Map<InputStream, File> streamFiles = new HashMap<InputStream, File>();
+
+	/** Counter for streams per file. */
+	private static final Map<File, Integer> fileCounts = new HashMap<File, Integer>();
+
+	/**
+	 * Creates a new temporary input stream.
+	 *
+	 * @param sourceInputStream
+	 *            The input stream to copy to a temporary file
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public TemporaryInputStream(InputStream sourceInputStream) throws IOException {
+		super(createFileInputStream(sourceInputStream));
+	}
+
+	/**
+	 * Creates a new temporary input stream from the given temporary file.
+	 *
+	 * @param tempFile
+	 *            The temporary file
+	 * @throws FileNotFoundException
+	 *             if the file can not be found
+	 */
+	private TemporaryInputStream(File tempFile) throws FileNotFoundException {
+		super(new FileInputStream(tempFile));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void close() throws IOException {
+		super.close();
+		synchronized (fileCounts) {
+			File tempFile = streamFiles.remove(in);
+			if (tempFile != null) {
+				if (fileCounts.get(tempFile) > 0) {
+					fileCounts.put(tempFile, fileCounts.get(tempFile) - 1);
+				} else {
+					fileCounts.remove(tempFile);
+					tempFile.delete();
+				}
+			}
+		}
+	}
+
+	/**
+	 * Creates a new input stream from the temporary file that is backing this
+	 * input stream. If the file has already been removed, this method will
+	 * throw an exception.
+	 *
+	 * @return A new input stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public InputStream reopen() throws IOException {
+		synchronized (fileCounts) {
+			File tempFile = streamFiles.get(in);
+			if (tempFile != null) {
+				fileCounts.put(tempFile, fileCounts.get(tempFile) + 1);
+				return new TemporaryInputStream(tempFile);
+			}
+			throw new FileNotFoundException("Temporary file has already disappeared.");
+		}
+	}
+
+	/**
+	 * Creates a temporary file, copies the given input stream to the temporary
+	 * file, and creates an input stream reading from the temporary file. The
+	 * returned input stream will delete the temporary file when its
+	 * {@link #close()} method is called.
+	 *
+	 * @param sourceInputStream
+	 *            The input stream to copy
+	 * @return The copied input stream, ready for consumption
+	 * @throws IOException
+	 */
+	private static InputStream createFileInputStream(InputStream sourceInputStream) throws IOException {
+		File tempFile = File.createTempFile("utils-temp-", ".tmp");
+		tempFile.deleteOnExit();
+		FileOutputStream fileOutputStream = null;
+		try {
+			fileOutputStream = new FileOutputStream(tempFile);
+			StreamCopier.copy(sourceInputStream, fileOutputStream);
+		} catch (IOException ioe1) {
+			throw ioe1;
+		} finally {
+			Closer.close(fileOutputStream);
+		}
+		FileInputStream fileInputStream = null;
+		try {
+			fileInputStream = new FileInputStream(tempFile);
+			streamFiles.put(fileInputStream, tempFile);
+			synchronized (fileCounts) {
+				fileCounts.put(tempFile, 0);
+			}
+			return fileInputStream;
+		} catch (IOException ioe1) {
+			Closer.close(fileInputStream);
+			tempFile.delete();
+			throw ioe1;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/logging/Logging.java b/alien/src/net/pterodactylus/util/logging/Logging.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/logging/Logging.java
@@ -0,0 +1,288 @@
+/*
+ * utils - Logging.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.logging;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Sets up logging.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Logging {
+
+	/** The log handler. */
+	private static final LogHandler logHandler = new LogHandler();
+
+	/** The console handler. */
+	private static ConsoleHandler consoleHandler = new ConsoleHandler();
+
+	/** Cache for logger’s classes. */
+	private static final Map<String, Class<?>> classCache = new HashMap<String, Class<?>>();
+
+	static {
+		logHandler.setLevel(Level.ALL);
+	}
+
+	/** The root name of the hierarchy. */
+	private static String hierarchyRootName;
+
+	/**
+	 * Adds a listener to the log handler.
+	 *
+	 * @param loggingListener
+	 *            The listener to add
+	 */
+	public static void addLoggingListener(LoggingListener loggingListener) {
+		logHandler.addLoggingListener(loggingListener);
+	}
+
+	/**
+	 * Removes a listener from the log handler.
+	 *
+	 * @param loggingListener
+	 *            The listener to remove
+	 */
+	public static void removeLoggingListener(LoggingListener loggingListener) {
+		logHandler.removeLoggingListener(loggingListener);
+	}
+
+	/**
+	 * Sets up logging and installs the log handler.
+	 *
+	 * @param hierarchyName
+	 *            The name of the hierarchy root logger
+	 */
+	public static void setup(String hierarchyName) {
+		hierarchyRootName = hierarchyName;
+		Logger rootLogger = getRootLogger();
+		rootLogger.addHandler(logHandler);
+		rootLogger.setUseParentHandlers(false);
+		if (rootLogger.getLevel() == null) {
+			rootLogger.setLevel(Level.ALL);
+		}
+	}
+
+	/**
+	 * Initializes console logging.
+	 */
+	public static void setupConsoleLogging() {
+		Logger rootLogger = getRootLogger();
+		consoleHandler.setLevel(Level.ALL);
+		consoleHandler.setFormatter(new Formatter() {
+
+			private StringBuffer recordBuffer = new StringBuffer();
+			private DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z");
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			public synchronized String format(LogRecord record) {
+				recordBuffer.setLength(0);
+				String linePrefix = dateFormatter.format(new Date(record.getMillis())) + " [" + record.getLevel() + "] [" + Thread.currentThread().getName() + "] [" + record.getSourceClassName() + "." + record.getSourceMethodName() + "] ";
+				recordBuffer.append(linePrefix).append(String.format(record.getMessage(), record.getParameters())).append('\n');
+				if (record.getThrown() != null) {
+					Throwable throwable = record.getThrown();
+					boolean causedBy = false;
+					while (throwable != null) {
+						recordBuffer.append(linePrefix);
+						if (causedBy) {
+							recordBuffer.append("caused by: ");
+						}
+						recordBuffer.append(throwable.getClass().getName());
+						if (throwable.getMessage() != null) {
+							recordBuffer.append(": ").append(throwable.getMessage());
+						}
+						recordBuffer.append("\n");
+						StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+						for (StackTraceElement stackTraceElement : stackTraceElements) {
+							recordBuffer.append(linePrefix).append("  at ").append(stackTraceElement.getClassName()).append('.').append(stackTraceElement.getMethodName()).append("(").append(stackTraceElement.getFileName()).append(':').append(stackTraceElement.getLineNumber()).append(')').append("\n");
+						}
+						throwable = throwable.getCause();
+						causedBy = true;
+					}
+				}
+				return recordBuffer.toString();
+			}
+		});
+		rootLogger.addHandler(consoleHandler);
+	}
+
+	/**
+	 * Shuts this logging hierarchy down by removing all {@link Handler}s from
+	 * the root logger.
+	 */
+	public static void shutdown() {
+		getRootLogger().removeHandler(logHandler);
+		getRootLogger().removeHandler(consoleHandler);
+	}
+
+	/**
+	 * Returns a named logger from the logger hierarchy.
+	 *
+	 * @param name
+	 *            The name of the logger
+	 * @return The logger
+	 */
+	public static Logger getLogger(String name) {
+		Logger logger = Logger.getLogger(hierarchyRootName + "." + name);
+		return logger;
+	}
+
+	/**
+	 * Returns a named logger from the logger hierarchy.
+	 *
+	 * @param loggerClass
+	 *            The class of the logger
+	 * @return The logger
+	 */
+	public static Logger getLogger(Class<?> loggerClass) {
+		classCache.put(hierarchyRootName + "." + loggerClass.getName(), loggerClass);
+		return getLogger(loggerClass.getName());
+	}
+
+	/**
+	 * Sets the log level of the hierarchy’s root logger.
+	 *
+	 * @param rootLevel
+	 *            The log level for the root logger
+	 */
+	public static void setRootLevel(Level rootLevel) {
+		getRootLogger().setLevel(rootLevel);
+	}
+
+	/**
+	 * Returns the root logger of this logging hierarchy.
+	 *
+	 * @return The hierarchy’s root logger
+	 */
+	private static Logger getRootLogger() {
+		return Logger.getLogger(hierarchyRootName);
+	}
+
+	/**
+	 * Returns the class that created the logger with the given name.
+	 *
+	 * @param loggerName
+	 *            The name of the logger
+	 * @return The class of the creating class, if available
+	 */
+	public static Class<?> getLoggerClass(String loggerName) {
+		return classCache.get(loggerName);
+	}
+
+	/**
+	 * The log handler simply forwards every log message it receives to all
+	 * registered listeners.
+	 *
+	 * @see LoggingListener
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class LogHandler extends Handler {
+
+		/**
+		 * Package-private constructor.
+		 */
+		LogHandler() {
+			/* do nothing. */
+		}
+
+		/** The list of the listeners. */
+		private final List<LoggingListener> loggingListeners = Collections.synchronizedList(new ArrayList<LoggingListener>());
+
+		//
+		// EVENT MANAGEMENT
+		//
+
+		/**
+		 * Adds a listener to the log handler.
+		 *
+		 * @param loggingListener
+		 *            The listener to add
+		 */
+		public void addLoggingListener(LoggingListener loggingListener) {
+			loggingListeners.add(loggingListener);
+		}
+
+		/**
+		 * Removes a listener from the log handler.
+		 *
+		 * @param loggingListener
+		 *            The listener to remove
+		 */
+		public void removeLoggingListener(LoggingListener loggingListener) {
+			loggingListeners.remove(loggingListener);
+		}
+
+		/**
+		 * Notifies all listeners that a log record was received.
+		 *
+		 * @param logRecord
+		 *            The received log record
+		 */
+		private void fireLogged(LogRecord logRecord) {
+			for (LoggingListener loggingListener : loggingListeners) {
+				loggingListener.logged(logRecord);
+			}
+		}
+
+		//
+		// INTERFACE Handler
+		//
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void close() throws SecurityException {
+			/* do nothing. */
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void flush() {
+			/* do nothing. */
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void publish(LogRecord logRecord) {
+			fireLogged(logRecord);
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/logging/LoggingListener.java b/alien/src/net/pterodactylus/util/logging/LoggingListener.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/logging/LoggingListener.java
@@ -0,0 +1,38 @@
+/*
+ * utils - LoggingListener.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.logging;
+
+import java.util.EventListener;
+import java.util.logging.LogRecord;
+
+/**
+ * Interface for components that want to receive logged messages.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface LoggingListener extends EventListener {
+
+	/**
+	 * Notifies a listener that a new log record was received.
+	 *
+	 * @param logRecord
+	 *            The received log record
+	 */
+	public void logged(LogRecord logRecord);
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/AbstractNotification.java b/alien/src/net/pterodactylus/util/notify/AbstractNotification.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/AbstractNotification.java
@@ -0,0 +1,209 @@
+/*
+ * utils - AbstractNotification.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import java.util.UUID;
+
+/**
+ * Abstract base implementation of a {@link Notification} that takes care of
+ * everything but creating the text of the notification, so only
+ * {@link Notification#render(java.io.Writer)} needs to be override by
+ * subclasses.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractNotification implements Notification {
+
+	/** The listener manager. */
+	private final NotificationListenerManager notificationListenerManager = new NotificationListenerManager(this);
+
+	/** The ID of this notification. */
+	private final String id;
+
+	/** The time when this notification was created. */
+	private final long createdTime;
+
+	/** The time when this notification was last updated. */
+	private volatile long lastUpdatedTime;
+
+	/** Whether this notification is dismissable. */
+	private volatile boolean dismissable;
+
+	/**
+	 * Creates a new notification with a random ID, the current time as creation
+	 * and last udpate time and is dismissable by the user.
+	 */
+	public AbstractNotification() {
+		this(UUID.randomUUID().toString());
+	}
+
+	/**
+	 * Creates a new notification with the current time as creation and last
+	 * udpate time and is dismissable by the user.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 */
+	public AbstractNotification(String id) {
+		this(id, System.currentTimeMillis());
+	}
+
+	/**
+	 * Creates a new notification with the given time as creation and last
+	 * update time and is dismissable by the user.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 * @param createdTime
+	 *            The time when this notification was created
+	 */
+	public AbstractNotification(String id, long createdTime) {
+		this(id, createdTime, createdTime);
+	}
+
+	/**
+	 * Creates a new notification with the given creation and last update time
+	 * and is dismissable by the user.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 * @param createdTime
+	 *            The time when this notification was created
+	 * @param lastUpdatedTime
+	 *            The time when this notification was last udpated
+	 */
+	public AbstractNotification(String id, long createdTime, long lastUpdatedTime) {
+		this(id, createdTime, lastUpdatedTime, true);
+	}
+
+	/**
+	 * Creates a new notification.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 * @param createdTime
+	 *            The time when this notification was created
+	 * @param lastUpdatedTime
+	 *            The time when this notification was last udpated
+	 * @param dismissable
+	 *            {@code true} if this notification is dismissable by the user
+	 */
+	public AbstractNotification(String id, long createdTime, long lastUpdatedTime, boolean dismissable) {
+		this.id = id;
+		this.createdTime = createdTime;
+		this.lastUpdatedTime = lastUpdatedTime;
+		this.dismissable = dismissable;
+	}
+
+	//
+	// LISTENER MANAGEMENT
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void addNotificationListener(NotificationListener notificationListener) {
+		notificationListenerManager.addListener(notificationListener);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeNotificationListener(NotificationListener notificationListener) {
+		notificationListenerManager.removeListener(notificationListener);
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String getId() {
+		return id;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public long getCreatedTime() {
+		return createdTime;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public long getLastUpdatedTime() {
+		return lastUpdatedTime;
+	}
+
+	/**
+	 * Sets the last updated time.
+	 *
+	 * @param lastUpdateTime
+	 *            The time this notification was last updated
+	 */
+	public void setLastUpdateTime(long lastUpdateTime) {
+		this.lastUpdatedTime = lastUpdateTime;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean isDismissable() {
+		return dismissable;
+	}
+
+	/**
+	 * Sets whether this notification is dismissable.
+	 *
+	 * @param dismissable
+	 *            {@code true} if this notification is dismissable
+	 */
+	public void setDismissable(boolean dismissable) {
+		this.dismissable = dismissable;
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void dismiss() {
+		notificationListenerManager.fireNotificationDismissed();
+	}
+
+	/**
+	 * Updates the {@link #getLastUpdatedTime() last update time} to the current
+	 * time.
+	 */
+	protected void touch() {
+		setLastUpdateTime(System.currentTimeMillis());
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/Notification.java b/alien/src/net/pterodactylus/util/notify/Notification.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/Notification.java
@@ -0,0 +1,117 @@
+/*
+ * utils - Notification.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Comparator;
+
+import net.pterodactylus.util.io.Renderable;
+
+/**
+ * A notification can be used to keep track of things that a user needs to be
+ * notified about.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Notification extends Renderable {
+
+	/** Sorts notifications by creation time, oldest first. */
+	public static final Comparator<Notification> CREATED_TIME_SORTER = new Comparator<Notification>() {
+
+		@Override
+		public int compare(Notification leftNotification, Notification rightNotification) {
+			return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, leftNotification.getCreatedTime() - rightNotification.getCreatedTime()));
+		}
+
+	};
+
+	/** Sorts notifications by last update time, newest first. */
+	public static final Comparator<Notification> LAST_UPDATED_TIME_SORTER = new Comparator<Notification>() {
+
+		@Override
+		public int compare(Notification leftNotification, Notification rightNotification) {
+			return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, rightNotification.getLastUpdatedTime() - leftNotification.getLastUpdatedTime()));
+		}
+
+	};
+
+	/**
+	 * Adds the given notification listener.
+	 *
+	 * @param notificationListener
+	 *            The listener to add
+	 */
+	public void addNotificationListener(NotificationListener notificationListener);
+
+	/**
+	 * Removes the given notification listener.
+	 *
+	 * @param notificationListener
+	 *            The listener to remove
+	 */
+	public void removeNotificationListener(NotificationListener notificationListener);
+
+	/**
+	 * Returns the unique ID of this notification.
+	 *
+	 * @return The unique ID of this notification
+	 */
+	public String getId();
+
+	/**
+	 * Returns the time when this notifiation was last updated.
+	 *
+	 * @return The time when this notification was last updated (in milliseconds
+	 *         since Jan 1 1970, UTC)
+	 */
+	public long getLastUpdatedTime();
+
+	/**
+	 * Returns the time when this notifiation was created.
+	 *
+	 * @return The time when this notification was created (in milliseconds
+	 *         since Jan 1 1970, UTC)
+	 */
+	public long getCreatedTime();
+
+	/**
+	 * Returns whether this notification may be dismissed by the user.
+	 *
+	 * @return {@code true} if this notification is dismissable by the user,
+	 *         {@code false} otherwise
+	 */
+	public boolean isDismissable();
+
+	/**
+	 * Dismisses this notification.
+	 */
+	public void dismiss();
+
+	/**
+	 * Renders this notification to the given writer.
+	 *
+	 * @param writer
+	 *            The writer to render this notification to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	@Override
+	public void render(Writer writer) throws IOException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/NotificationListener.java b/alien/src/net/pterodactylus/util/notify/NotificationListener.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/NotificationListener.java
@@ -0,0 +1,38 @@
+/*
+ * utils - NotificationListener.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import java.util.EventListener;
+
+/**
+ * Listener interface for objects that want to be notified on certain
+ * {@link Notification} events, such as when the notification is dismissed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface NotificationListener extends EventListener {
+
+	/**
+	 * Notifies a listener that the given notification was dismissed.
+	 *
+	 * @param notification
+	 *            The dismissed notification
+	 */
+	public void notificationDismissed(Notification notification);
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/NotificationListenerManager.java b/alien/src/net/pterodactylus/util/notify/NotificationListenerManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/NotificationListenerManager.java
@@ -0,0 +1,54 @@
+/*
+ * utils - NotificationListenerManager.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manager for {@link NotificationListener}s and {@link Notification} events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NotificationListenerManager extends AbstractListenerManager<Notification, NotificationListener> {
+
+	/**
+	 * Creates a new {@link NotificationListener} manager.
+	 *
+	 * @param source
+	 *            The notification that emits all events
+	 */
+	public NotificationListenerManager(Notification source) {
+		super(source);
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Notifies all listeners that a notification was dismissed.
+	 *
+	 * @see NotificationListener#notificationDismissed(Notification)
+	 */
+	void fireNotificationDismissed() {
+		for (NotificationListener notificationListener : getListeners()) {
+			notificationListener.notificationDismissed(getSource());
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/NotificationManager.java b/alien/src/net/pterodactylus/util/notify/NotificationManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/NotificationManager.java
@@ -0,0 +1,151 @@
+/*
+ * utils - Notifications.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manager for all current notifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NotificationManager implements NotificationListener {
+
+	/** All notifications. */
+	private final Map<String, Notification> notifications = new HashMap<String, Notification>();
+
+	/** Notifications removed since last retrieval. */
+	/* synchronize access on notifications. */
+	private final Map<String, Notification> removedNotifications = new HashMap<String, Notification>();
+
+	/** The time the notifications were last retrieved. */
+	/* synchronize access on {@link #notifications}. */
+	private long lastRetrievalTime;
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Returns all current notifications.
+	 *
+	 * @return All current notifications
+	 */
+	public Set<Notification> getNotifications() {
+		synchronized (notifications) {
+			lastRetrievalTime = System.currentTimeMillis();
+			return new HashSet<Notification>(notifications.values());
+		}
+	}
+
+	/**
+	 * Returns all notifications that have been updated since the last
+	 * retrieval.
+	 *
+	 * @return All changed notifications
+	 */
+	public Set<Notification> getChangedNotifications() {
+		Set<Notification> changedNotifications = new HashSet<Notification>();
+		synchronized (notifications) {
+			for (Notification notification : notifications.values()) {
+				if (notification.getLastUpdatedTime() > lastRetrievalTime) {
+					changedNotifications.add(notification);
+				}
+			}
+			lastRetrievalTime = System.currentTimeMillis();
+		}
+		return changedNotifications;
+	}
+
+	/**
+	 * Returns all notifications that have been removed since the last retrieval
+	 * and clears the list of removed notifications.
+	 *
+	 * @return All removed notifications
+	 */
+	public Set<Notification> getRemovedNotifications() {
+		Set<Notification> notifications;
+		synchronized (this.notifications) {
+			notifications = new HashSet<Notification>(removedNotifications.values());
+			removedNotifications.clear();
+		}
+		return notifications;
+	}
+
+	/**
+	 * Returns the notification with the given ID.
+	 *
+	 * @param notificationId
+	 *            The ID of the notification
+	 * @return The notification, or {@code null} if there is no notification
+	 *         with the given ID
+	 */
+	public Notification getNotification(String notificationId) {
+		synchronized (notifications) {
+			return notifications.get(notificationId);
+		}
+	}
+
+	/**
+	 * Adds the given notification.
+	 *
+	 * @param notification
+	 *            The notification to add
+	 */
+	public void addNotification(Notification notification) {
+		synchronized (notifications) {
+			if (!notifications.containsKey(notification.getId())) {
+				notifications.put(notification.getId(), notification);
+				notification.addNotificationListener(this);
+				removedNotifications.remove(notification.getId());
+			}
+		}
+	}
+
+	/**
+	 * Removes the given notification.
+	 *
+	 * @param notification
+	 *            The notification to remove
+	 */
+	public void removeNotification(Notification notification) {
+		synchronized (notifications) {
+			if (notifications.containsKey(notification.getId())) {
+				notifications.remove(notification.getId());
+				notification.removeNotificationListener(this);
+				removedNotifications.put(notification.getId(), notification);
+			}
+		}
+	}
+
+	//
+	// NOTIFICATIONLISTENER METHODS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void notificationDismissed(Notification notification) {
+		removeNotification(notification);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/notify/TemplateNotification.java b/alien/src/net/pterodactylus/util/notify/TemplateNotification.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/notify/TemplateNotification.java
@@ -0,0 +1,125 @@
+/*
+ * utils - TemplateNotification.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.notify;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * {@link Template}-based implementation of a {@link Notification}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemplateNotification extends AbstractNotification {
+
+	/** The template to render. */
+	private final Template template;
+
+	/**
+	 * Creates a new notification.
+	 *
+	 * @param template
+	 *            The template to render
+	 */
+	public TemplateNotification(Template template) {
+		super();
+		this.template = template;
+	}
+
+	/**
+	 * Creates a new notification.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 * @param template
+	 *            The template to render
+	 */
+	public TemplateNotification(String id, Template template) {
+		super(id);
+		this.template = template;
+	}
+
+	/**
+	 * Creates a new notification.
+	 *
+	 * @param id
+	 *            The ID of the notification
+	 * @param creationTime
+	 *            The creation time of the notification
+	 * @param lastUpdatedTime
+	 *            The time the notification was last udpated
+	 * @param dismissable
+	 *            {@code true} if this notification is dismissable by the user
+	 * @param template
+	 *            The template to render
+	 */
+	public TemplateNotification(String id, long creationTime, long lastUpdatedTime, boolean dismissable, Template template) {
+		super(id, creationTime, lastUpdatedTime, dismissable);
+		this.template = template;
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Returns the template that renders this notification.
+	 *
+	 * @return The template that renders this notification
+	 */
+	public Template getTemplate() {
+		return template;
+	}
+
+	//
+	// NOTIFICATION METHODS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(Writer writer) throws IOException {
+		template.render(writer);
+	}
+
+	//
+	// OBJECT METHODS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String toString() {
+		StringWriter stringWriter = new StringWriter();
+		try {
+			render(stringWriter);
+		} catch (IOException ioe1) {
+			/* A StringWriter never throws. */
+		} finally {
+			Closer.close(stringWriter);
+		}
+		return stringWriter.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/number/Bits.java b/alien/src/net/pterodactylus/util/number/Bits.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/number/Bits.java
@@ -0,0 +1,89 @@
+/*
+ * utils - Bits.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.number;
+
+/**
+ * Utility class for bit manipulations.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Bits {
+
+	/**
+	 * Decodes <code>numberOfBits</code> bits from the specified start index.
+	 * The resulting value has the range <code>0</code> to
+	 * <code>2 ^ numberOfBits - 1</code>.
+	 *
+	 * @param value
+	 *            The value to decode bits from
+	 * @param bitIndex
+	 *            The index of the first bit to decode
+	 * @param numberOfBits
+	 *            The number of bits to decode
+	 * @return The decoded value
+	 */
+	public static int decodeBits(int value, int bitIndex, int numberOfBits) {
+		return (value >> bitIndex) & ((1 << numberOfBits) - 1);
+	}
+
+	/**
+	 * Changes <code>numberOfBits</code> bits starting from index
+	 * <code>bitIndex</code> in <code>octet</code> to the
+	 * <code>numberOfBits</code> lowest bits from <code>newValue</code>.
+	 *
+	 * @param oldValue
+	 *            The value to change
+	 * @param bitIndex
+	 *            The index of the lowest bit to change
+	 * @param numberOfBits
+	 *            The number of bits to change
+	 * @param newValue
+	 *            The new value of the changed bits
+	 * @return <code>octet</code> with the specified bits changed
+	 */
+	public static int encodeBits(int oldValue, int bitIndex, int numberOfBits, int newValue) {
+		return (oldValue & ~(((1 << numberOfBits) - 1) << bitIndex)) | ((newValue & ((1 << numberOfBits) - 1)) << bitIndex);
+	}
+
+	/**
+	 * Rotates the bits in the given value to the left.
+	 *
+	 * @param value
+	 *            The value to rotate
+	 * @param distance
+	 *            The distance of the rotation, in bits
+	 * @return The rotated value
+	 */
+	public static int rotateLeft(int value, int distance) {
+		return (value << (distance & 0x1f)) | (value >>> ((32 - distance) & 0x1f));
+	}
+
+	/**
+	 * Rotates the bits in the given value to the right.
+	 *
+	 * @param value
+	 *            The value to rotate
+	 * @param distance
+	 *            The distance of the rotation, in bits
+	 * @return The rotated value
+	 */
+	public static int rotateRight(int value, int distance) {
+		return (value >>> (distance & 0x1f)) | (value << ((32 - distance) & 0x1f));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/number/Digits.java b/alien/src/net/pterodactylus/util/number/Digits.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/number/Digits.java
@@ -0,0 +1,141 @@
+/*
+ * utils - Digits.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.number;
+
+/**
+ * Utility class for decimal number strings.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Digits {
+
+	/**
+	 * Zero-pads the given value until it is at least the specified length in
+	 * digits. This will only work with positive values!
+	 *
+	 * @param value
+	 *            The value to pad
+	 * @param digits
+	 *            The number of digits for the padded value
+	 * @return The zero-padded value
+	 */
+	public static String format(long value, int digits) {
+		String formattedValue = String.valueOf(value);
+		while (formattedValue.length() < digits) {
+			formattedValue = "0" + formattedValue;
+		}
+		return formattedValue;
+	}
+
+	/**
+	 * Returns the given value formatted with the given number of fractional
+	 * digits, showing final zeroes as well.
+	 *
+	 * @param value
+	 *            The value to format
+	 * @param fractionDigits
+	 *            The number of fractional digits
+	 * @param round
+	 *            <code>true</code> to round the formatted value,
+	 *            <code>false</code> to truncate it
+	 * @return The formatted value
+	 */
+	public static String formatFractions(double value, int fractionDigits, boolean round) {
+		double factor = Math.pow(10, fractionDigits);
+		int tempValue = (int) (value * factor + (round ? 0.5 : 0));
+		String formattedValue = String.valueOf(tempValue / factor);
+		if (formattedValue.indexOf('.') == -1) {
+			formattedValue += ".";
+			for (int count = 0; count < fractionDigits; count++) {
+				formattedValue += "0";
+			}
+		} else {
+			while (formattedValue.length() - formattedValue.indexOf('.') <= fractionDigits) {
+				formattedValue += "0";
+			}
+		}
+		return formattedValue;
+	}
+
+	/**
+	 * Parses the given string into a long, throwing an exception if the string
+	 * contains invalid characters.
+	 *
+	 * @param digits
+	 *            The number string to parse
+	 * @return A long value representing the number string
+	 * @throws NumberFormatException
+	 *             if the number string contains invalid characters
+	 */
+	public static long parseLong(String digits) throws NumberFormatException {
+		return Long.parseLong(digits);
+	}
+
+	/**
+	 * Parses the given string into a long, returning the given default value if
+	 * the string contains invalid characters.
+	 *
+	 * @param digits
+	 *            The number string to parse
+	 * @param defaultValue
+	 *            The value to return if the string can not be parsed
+	 * @return The long value represented by the string, or the default value if
+	 *         the string can not be parsed
+	 */
+	public static long parseLong(String digits, long defaultValue) {
+		try {
+			return Long.parseLong(digits);
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * Parses the given string into an int, throwing an exception if the string
+	 * contains invalid characters.
+	 *
+	 * @param digits
+	 *            The number string to parse
+	 * @return A int value representing the number string
+	 * @throws NumberFormatException
+	 *             if the number string contains invalid characters
+	 */
+	public static int parseInt(String digits) throws NumberFormatException {
+		return Integer.parseInt(digits);
+	}
+
+	/**
+	 * Parses the given string into an int, returning the given default value if
+	 * the string contains invalid characters.
+	 *
+	 * @param digits
+	 *            The number string to parse
+	 * @param defaultValue
+	 *            The value to return if the string can not be parsed
+	 * @return The int value represented by the string, or the default value if
+	 *         the string can not be parsed
+	 */
+	public static int parseInt(String digits, int defaultValue) {
+		try {
+			return Integer.parseInt(digits);
+		} catch (NumberFormatException nfe1) {
+			return defaultValue;
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/number/Hex.java b/alien/src/net/pterodactylus/util/number/Hex.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/number/Hex.java
@@ -0,0 +1,223 @@
+/*
+ * utils - Hex.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.number;
+
+/**
+ * Contains methods to convert byte arrays to hex strings and vice versa.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Hex {
+
+	/**
+	 * Converts the given string to a hexadecimal string.
+	 *
+	 * @param buffer
+	 *            The string to convert
+	 * @return A hexadecimal string
+	 * @see #toHex(byte[], int, int)
+	 */
+	public static String toHex(String buffer) {
+		return toHex(buffer.getBytes());
+	}
+
+	/**
+	 * Converts the given buffer to a hexadecimal string.
+	 *
+	 * @param buffer
+	 *            The buffer to convert
+	 * @return A hexadecimal string
+	 * @see #toHex(byte[], int, int)
+	 */
+	public static String toHex(byte[] buffer) {
+		return toHex(buffer, 0, buffer.length);
+	}
+
+	/**
+	 * Converts <code>length</code> bytes of the given buffer starting at index
+	 * <code>start</code> to a hexadecimal string.
+	 *
+	 * @param buffer
+	 *            The buffer to convert
+	 * @param start
+	 *            The index to start
+	 * @param length
+	 *            The length to convert
+	 * @return A hexadecimal string
+	 * @throws ArrayIndexOutOfBoundsException
+	 *             if <code>start</code> and/or <code>start + length</code> are
+	 *             outside the valid bounds of <code>buffer</code>
+	 * @see #toHex(byte[], int, int)
+	 */
+	public static String toHex(byte[] buffer, int start, int length) {
+		return toHex(buffer, start, length, false);
+	}
+
+	/**
+	 * Converts the given string to a hexadecimal string, using upper-case
+	 * characters for the digits ‘a’ to ‘f’, if desired.
+	 *
+	 * @param buffer
+	 *            The string to convert
+	 * @param upperCase
+	 *            if the digits 'a' to 'f' should be in upper-case characters
+	 * @return A hexadecimal string
+	 * @see #toHex(byte[], int, int, boolean)
+	 */
+	public static String toHex(String buffer, boolean upperCase) {
+		return toHex(buffer.getBytes(), upperCase);
+	}
+
+	/**
+	 * Converts the given buffer to a hexadecimal string, using upper-case
+	 * characters for the digits ‘a’ to ‘f’, if desired.
+	 *
+	 * @param buffer
+	 *            The buffer to convert
+	 * @param upperCase
+	 *            if the digits 'a' to 'f' should be in upper-case characters
+	 * @return A hexadecimal string
+	 * @see #toHex(byte[], int, int)
+	 */
+	public static String toHex(byte[] buffer, boolean upperCase) {
+		return toHex(buffer, 0, buffer.length, upperCase);
+	}
+
+	/**
+	 * Converts <code>length</code> bytes of the given buffer starting at index
+	 * <code>start</code> to a hexadecimal string, using upper-case characters
+	 * for the digits ‘a’ to ‘f’, if desired.
+	 *
+	 * @param buffer
+	 *            The buffer to convert
+	 * @param start
+	 *            The index to start
+	 * @param length
+	 *            The length to convert
+	 * @param upperCase
+	 *            if the digits 'a' to 'f' should be in upper-case characters
+	 * @return A hexadecimal string
+	 * @throws ArrayIndexOutOfBoundsException
+	 *             if <code>start</code> and/or <code>start + length</code> are
+	 *             outside the valid bounds of <code>buffer</code>
+	 * @see #toHex(byte[], int, int)
+	 */
+	public static String toHex(byte[] buffer, int start, int length, boolean upperCase) {
+		StringBuilder hexBuffer = new StringBuilder(length * 2);
+		for (int index = start; index < length; index++) {
+			String hexByte = Integer.toHexString(buffer[index] & 0xff);
+			if (upperCase) {
+				hexByte = hexByte.toUpperCase();
+			}
+			if (hexByte.length() < 2) {
+				hexBuffer.append('0');
+			}
+			hexBuffer.append(hexByte);
+		}
+		return hexBuffer.toString();
+	}
+
+	/**
+	 * Formats the given byte as a 2-digit hexadecimal value.
+	 *
+	 * @param value
+	 *            The byte to encode
+	 * @return The encoded 2-digit hexadecimal value
+	 */
+	public static String toHex(byte value) {
+		return toHex(value, 2);
+	}
+
+	/**
+	 * Formats the given shoirt as a 4-digit hexadecimal value.
+	 *
+	 * @param value
+	 *            The short to encode
+	 * @return The encoded 4-digit hexadecimal value
+	 */
+	public static String toHex(short value) {
+		return toHex(value, 4);
+	}
+
+	/**
+	 * Formats the given int as a 8-digit hexadecimal value.
+	 *
+	 * @param value
+	 *            The int to encode
+	 * @return The encoded 8-digit hexadecimal value
+	 */
+	public static String toHex(int value) {
+		return toHex(value, 8);
+	}
+
+	/**
+	 * Formats the given int as a 16-digit hexadecimal value.
+	 *
+	 * @param value
+	 *            The long to encode
+	 * @return The encoded 16-digit hexadecimal value
+	 */
+	public static String toHex(long value) {
+		return toHex(value, 16);
+	}
+
+	/**
+	 * Formats the given value with as a hexadecimal number with at least the
+	 * specified number of digits. The value will be padded with zeroes if it is
+	 * shorter than <code>digits</code>.
+	 *
+	 * @param value
+	 *            The value to encode
+	 * @param digits
+	 *            The minimum number of digits
+	 * @return The zero-padded hexadecimal value
+	 */
+	public static String toHex(long value, int digits) {
+		String hexValue = Long.toHexString(value);
+		if (hexValue.length() > digits) {
+			hexValue = hexValue.substring(hexValue.length() - digits, hexValue.length());
+		}
+		while (hexValue.length() < digits) {
+			hexValue = "0" + hexValue;
+		}
+		return hexValue;
+	}
+
+	/**
+	 * Decodes a hexadecimal string into a byte array.
+	 *
+	 * @param hexString
+	 *            The hexadecimal representation to decode
+	 * @return The decoded byte array
+	 * @see Integer#parseInt(java.lang.String, int)
+	 */
+	public static byte[] toByte(String hexString) {
+		if ((hexString.length() & 0x01) == 0x01) {
+			/* odd length, this is not correct. */
+			throw new IllegalArgumentException("hex string must have even length.");
+		}
+		byte[] dataBytes = new byte[hexString.length() / 2];
+		for (int stringIndex = 0; stringIndex < hexString.length(); stringIndex += 2) {
+			String hexNumber = hexString.substring(stringIndex, stringIndex + 2);
+			byte dataByte = (byte) Integer.parseInt(hexNumber, 16);
+			dataBytes[stringIndex / 2] = dataByte;
+		}
+		return dataBytes;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/number/Numbers.java b/alien/src/net/pterodactylus/util/number/Numbers.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/number/Numbers.java
@@ -0,0 +1,101 @@
+/*
+ * utils - Numbers.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.number;
+
+/**
+ * Collection of various helper methods that deal with numbers.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Numbers {
+
+	/**
+	 * Tries to parse the {@link String} representation of the given object (as
+	 * per {@link String#valueOf(Object)}) as an {@link Integer}.
+	 *
+	 * @param object
+	 *            The object to parse
+	 * @return The parsed {@link Integer}, or {@code null} if the object could
+	 *         not be parsed
+	 */
+	public static Integer safeParseInteger(Object object) {
+		return safeParseInteger(object, null);
+	}
+
+	/**
+	 * Tries to parse the {@link String} representation of the given object (as
+	 * per {@link String#valueOf(Object)}) as an {@link Integer}.
+	 *
+	 * @param object
+	 *            The object to parse
+	 * @param defaultValue
+	 *            The value to return if the object is {@code null} or can not
+	 *            be parsed as an {@link Integer}
+	 * @return The parsed Integer, or {@code null} if the object could not be
+	 *         parsed
+	 */
+	public static Integer safeParseInteger(Object object, Integer defaultValue) {
+		if (object == null) {
+			return defaultValue;
+		}
+		try {
+			return Integer.parseInt(String.valueOf(object));
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Tries to parse the {@link String} representation of the given object (as
+	 * per {@link String#valueOf(Object)}) as a {@link Long}.
+	 *
+	 * @param object
+	 *            The object to parse
+	 * @return The parsed {@link Long}, or {@code null} if the object could not
+	 *         be parsed
+	 */
+	public static Long safeParseLong(Object object) {
+		return safeParseLong(object, null);
+	}
+
+	/**
+	 * Tries to parse the {@link String} representation of the given object (as
+	 * per {@link String#valueOf(Object)}) as a {@link Long}.
+	 *
+	 * @param object
+	 *            The object to parse
+	 * @param defaultValue
+	 *            The value to return if the object is {@code null} or can not
+	 *            be parsed as an {@link Long}
+	 * @return The parsed Long, or {@code null} if the object could not be
+	 *         parsed
+	 */
+	public static Long safeParseLong(Object object, Long defaultValue) {
+		if (object == null) {
+			return defaultValue;
+		}
+		try {
+			return Long.parseLong(String.valueOf(object));
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		return defaultValue;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/number/SI.java b/alien/src/net/pterodactylus/util/number/SI.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/number/SI.java
@@ -0,0 +1,112 @@
+/*
+ * utils - SI.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.number;
+
+/**
+ * Formats a decimal number with SI units.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SI {
+
+	/** The units (up to 2^24). */
+	private static final String[] units = { "", "K", "M", "G", "T", "P", "E", "Z", "Y" };
+
+	/**
+	 * Formats the specified number using 1000-based units.
+	 *
+	 * @param number
+	 *            The number to encode
+	 * @return The converted number with a unit postfix
+	 */
+	public static String format(long number) {
+		return format(number, false, false);
+	}
+
+	/**
+	 * Formats the specified number using 1024-based units.
+	 *
+	 * @param number
+	 *            The number to encode
+	 * @return The converted number with a unit postfix
+	 */
+	public static String formatBinary(long number) {
+		return format(number, true, false);
+	}
+
+	/**
+	 * Formats the specified number using the specified units. If
+	 * <code>useBinaryUnits</code> is <code>true</code>, 1024-based units
+	 * (marked by an 'i' after the unit character, e.g. 'Ki' for 1024) will be
+	 * used; if it is <code>false</code>, 1000-based units will be used.
+	 *
+	 * @param number
+	 *            The number to encode
+	 * @param useBinaryUnits
+	 *            Whether to use binary or decimal units
+	 * @return The converted number with a unit postfix
+	 */
+	public static String format(long number, boolean useBinaryUnits) {
+		return format(number, 0, useBinaryUnits, false);
+	}
+
+	/**
+	 * Formats the specified number using the specified units. If
+	 * <code>useBinaryUnits</code> is <code>true</code>, 1024-based units
+	 * (marked by an 'i' after the unit character, e.g. 'Ki' for 1024) will be
+	 * used; if it is <code>false</code>, 1000-based units will be used.
+	 *
+	 * @param number
+	 *            The number to encode
+	 * @param useBinaryUnits
+	 *            Whether to use binary or decimal units
+	 * @param addSpace
+	 *            Whether to add a space between the number and the unit
+	 * @return The converted number with a unit postfix
+	 */
+	public static String format(long number, boolean useBinaryUnits, boolean addSpace) {
+		return format(number, 0, useBinaryUnits, addSpace);
+	}
+
+	/**
+	 * Formats the specified number using the specified units. If
+	 * <code>useBinaryUnits</code> is <code>true</code>, 1024-based units
+	 * (marked by an 'i' after the unit character, e.g. 'Ki' for 1024) will be
+	 * used; if it is <code>false</code>, 1000-based units will be used.
+	 *
+	 * @param number
+	 *            The number to encode
+	 * @param digits
+	 *            The number of digits after the decimal point
+	 * @param useBinaryUnits
+	 *            Whether to use binary or decimal units
+	 * @param addSpace
+	 *            Whether to add a space between the number and the unit
+	 * @return The converted number with a unit postfix
+	 */
+	public static String format(long number, int digits, boolean useBinaryUnits, boolean addSpace) {
+		int unit = 0;
+		double realNumber = number;
+		while ((unit < units.length) && (realNumber >= (useBinaryUnits ? 1024 : 1000))) {
+			realNumber /= (useBinaryUnits ? 1024 : 1000);
+			unit++;
+		}
+		return Digits.formatFractions(realNumber, digits, false) + (addSpace ? " " : "") + units[unit] + ((useBinaryUnits && (unit > 0)) ? "i" : "");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/AbstractService.java b/alien/src/net/pterodactylus/util/service/AbstractService.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/AbstractService.java
@@ -0,0 +1,625 @@
+/*
+ * utils - AbstractService.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.thread.DumpingThreadFactory;
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractService implements Service, Runnable {
+
+	/** Logger. */
+	private static final Logger logger = Logging.getLogger(AbstractService.class.getName());
+
+	/** Listener support. */
+	private final ServiceListenerManager serviceListenerSupport = new ServiceListenerManager(this);
+
+	/** The shutdown hook. */
+	private final ShutdownHook shutdownHook = new ShutdownHook();
+
+	/** Counter for unnamed instances. */
+	private static int counter = 0;
+
+	/** Object used for synchronization. */
+	protected final Object syncObject = new Object();
+
+	/** Whether this method should stop. */
+	private boolean shouldStop = false;
+
+	/** The name of the service. */
+	private final String name;
+
+	/** The current state of the service. */
+	private State state = State.offline;
+
+	/** The current action of the service. */
+	private String action = "";
+
+	/** The thread factory to use. */
+	private ThreadFactory threadFactory;
+
+	/** The service attributes. */
+	private final Map<String, Object> serviceAttributes = new HashMap<String, Object>();
+
+	/** Whether to register the shutdown hook. */
+	private final boolean registerShutdownHook;
+
+	/**
+	 * Constructs a new abstract service with an anonymous name.
+	 */
+	protected AbstractService() {
+		this("AbstractService-" + counter++);
+	}
+
+	/**
+	 * Constructs a new abstract service with the given name.
+	 *
+	 * @param name
+	 *            The name of the service
+	 */
+	protected AbstractService(String name) {
+		this(name, true);
+	}
+
+	/**
+	 * Constructs a new abstract service with the given name.
+	 *
+	 * @param name
+	 *            The name of the service
+	 * @param threadFactory
+	 *            The thread factory used to create the service thread
+	 */
+	protected AbstractService(String name, ThreadFactory threadFactory) {
+		this(name, true, threadFactory);
+	}
+
+	/**
+	 * Constructs a new abstract service with the given name.
+	 *
+	 * @param name
+	 *            The name of the service
+	 * @param registerShutdownHook
+	 *            <code>true</code> to register shutdown hook for this service,
+	 *            <code>false</code> to not register a shutdown hook
+	 */
+	protected AbstractService(String name, boolean registerShutdownHook) {
+		this(name, registerShutdownHook, new DumpingThreadFactory(name + " ", false));
+	}
+
+	/**
+	 * Constructs a new abstract service with the given name.
+	 *
+	 * @param name
+	 *            The name of the service
+	 * @param registerShutdownHook
+	 *            <code>true</code> to register shutdown hook for this service,
+	 *            <code>false</code> to not register a shutdown hook
+	 * @param threadFactory
+	 *            The thread factory used to create the service thread
+	 */
+	protected AbstractService(String name, boolean registerShutdownHook, ThreadFactory threadFactory) {
+		this.registerShutdownHook = registerShutdownHook;
+		Validation.begin().isNotNull("name", name).isNotNull("threadFactory", threadFactory).check();
+		this.name = name;
+		this.threadFactory = threadFactory;
+	}
+
+	//
+	// EVENT MANAGEMENT
+	//
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.service.Service#addServiceListener(net.pterodactylus.util.service.ServiceListener)
+	 */
+	@Override
+	public void addServiceListener(ServiceListener serviceListener) {
+		serviceListenerSupport.addListener(serviceListener);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.service.Service#removeServiceListener(net.pterodactylus.util.service.ServiceListener)
+	 */
+	@Override
+	public void removeServiceListener(ServiceListener serviceListener) {
+		serviceListenerSupport.removeListener(serviceListener);
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Sets the thread factory that this service uses to spawn new threads.
+	 *
+	 * @param threadFactory
+	 *            The thread factory for new threads
+	 */
+	public void setThreadFactory(ThreadFactory threadFactory) {
+		Validation.begin().isNotNull("threadFactory", threadFactory).check();
+		this.threadFactory = threadFactory;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.service.Service#getState()
+	 */
+	@Override
+	public State getState() {
+		synchronized (syncObject) {
+			return state;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	@Deprecated
+	public String getStateReason() {
+		synchronized (syncObject) {
+			return action;
+		}
+	}
+
+	/**
+	 * Returns the current action of the service.
+	 *
+	 * @return The current action of the service
+	 */
+	@Override
+	public String getAction() {
+		synchronized (syncObject) {
+			return action;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see net.pterodactylus.util.service.Service#getName()
+	 */
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return name;
+	}
+
+	/**
+	 * @see net.pterodactylus.util.service.Service#getServiceAttribute(java.lang.String)
+	 */
+	@Override
+	public Object getServiceAttribute(String attributeName) {
+		synchronized (syncObject) {
+			return serviceAttributes.get(attributeName);
+		}
+	}
+
+	/**
+	 * @see net.pterodactylus.util.service.Service#hasServiceAttribute(java.lang.String)
+	 */
+	@Override
+	public boolean hasServiceAttribute(String attributeName) {
+		synchronized (syncObject) {
+			return serviceAttributes.containsKey(attributeName);
+		}
+	}
+
+	/**
+	 * @see net.pterodactylus.util.service.Service#setServiceAttribute(java.lang.String,
+	 *      java.lang.Object)
+	 */
+	@Override
+	public void setServiceAttribute(String attributeName, Object attributeValue) {
+		synchronized (syncObject) {
+			serviceAttributes.put(attributeName, attributeValue);
+		}
+	}
+
+	//
+	// PROTECTED ACCESSORS
+	//
+
+	/**
+	 * Sets the new state of this service without changing the action. Calling
+	 * this method is equivalent to calling {@link #setState(State, String)
+	 * setState(newState, null)}.
+	 *
+	 * @param newState
+	 *            The new state of this service
+	 * @see #setState(State, String)
+	 */
+	protected void setState(State newState) {
+		setState(newState, null);
+	}
+
+	/**
+	 * Sets the action for the current state.
+	 *
+	 * @param action
+	 *            The new action of the service, or <code>null</code> to not
+	 *            change the action
+	 * @deprecated Use {@link #setAction(String)} instead
+	 */
+	@Deprecated
+	protected void setStateReason(String action) {
+		setAction(action);
+	}
+
+	/**
+	 * Sets the action of the service.
+	 *
+	 * @param action
+	 *            The new action of the service, or <code>null</code> to not
+	 *            change the action
+	 */
+	protected void setAction(String action) {
+		if (action != null) {
+			synchronized (syncObject) {
+				this.action = action;
+			}
+		}
+	}
+
+	/**
+	 * Sets the new state of this service. If the current state is different
+	 * from the given new state, a
+	 * {@link ServiceListener#serviceStateChanged(Service, State, State)
+	 * Service-State-Changed} event is fired. If the given action is
+	 * <code>null</code>, the current action will not be changed.
+	 *
+	 * @param newState
+	 *            The new state of the service
+	 * @param action
+	 *            The current action of the service, or <code>null</code> if the
+	 *            action should not be changed
+	 */
+	protected void setState(State newState, String action) {
+		State oldState = null;
+		synchronized (syncObject) {
+			oldState = state;
+			state = newState;
+			if (action != null) {
+				this.action = action;
+			}
+		}
+		if (oldState != newState) {
+			serviceListenerSupport.fireServiceStateChanged(oldState, newState);
+		}
+	}
+
+	/**
+	 * Returns whether this service should stop.
+	 *
+	 * @return <code>true</code> if this service should stop, <code>false</code>
+	 *         otherwise
+	 */
+	protected boolean shouldStop() {
+		synchronized (syncObject) {
+			return shouldStop;
+		}
+	}
+
+	//
+	// SERVICE METHODS
+	//
+
+	/**
+	 * Empty initialization. If your service needs to initialize anything before
+	 * being started, simply override this method.
+	 *
+	 * @see #init()
+	 */
+	protected void serviceInit() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Initializes this services. This method returns immediately if the current
+	 * {@link #state} of this service is <strong>not</strong>
+	 * {@link State#offline}. Otherwise {@link #serviceInit()} is called for the
+	 * real initialization of this service.
+	 *
+	 * @see #serviceInit()
+	 * @see net.pterodactylus.util.service.Service#init()
+	 */
+	@Override
+	public final void init() {
+		if (state.getBasicState() != State.offline) {
+			logger.log(Level.WARNING, "will not init " + name + ", state is " + getState());
+			return;
+		}
+		serviceInit();
+	}
+
+	/**
+	 * Empty implementation. If your service needs to do something before the
+	 * service thread is started, simply override this method.
+	 *
+	 * @see #start()
+	 */
+	protected void serviceStart() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Attempts to start this service. This method returns immediately if the
+	 * current {@link #state} is <strong>not</strong> {@link State#offline}.
+	 * Otherwise {@link #serviceStart()} is called for further post-init,
+	 * pre-start initialization.
+	 *
+	 * @see #serviceStart()
+	 * @see net.pterodactylus.util.service.Service#start()
+	 */
+	@Override
+	public final void start() {
+		if (getState() != State.offline) {
+			logger.log(Level.WARNING, "will not start " + name + ", state is " + getState());
+			return;
+		}
+		if (registerShutdownHook) {
+			Runtime.getRuntime().addShutdownHook(shutdownHook);
+		}
+		serviceStart();
+		synchronized (syncObject) {
+			shouldStop = false;
+		}
+		setState(State.starting, "");
+		Thread serviceThread = threadFactory.newThread(this);
+		serviceThread.setName(name);
+		serviceThread.start();
+	}
+
+	/**
+	 * Empty implementation. If your service needs to do anything this method
+	 * needs to be overridden. Otherwise it will {@link #sleep()} until
+	 * {@link #stop()} is called.
+	 *
+	 * @see #run()
+	 */
+	protected void serviceRun() {
+		while (!shouldStop()) {
+			sleep(0);
+		}
+	}
+
+	/**
+	 * Runner for the main {@link #serviceRun()} method. This method sets the
+	 * {@link #state} to {@link State#online} and fires a
+	 * {@link ServiceListener#serviceStarted(Service) Service-Started} event
+	 * before {@link #serviceRun()} is called. When the {@link #serviceRun()}
+	 * method terminates the {@link #state} is set to {@link State#offline}
+	 * without changing the action. If the {@link #serviceRun()} method threw an
+	 * exception {@link #state} is set to {@link State#offline} with the
+	 * {@link Exception#getMessage() message} of the exception as action. In
+	 * both cases a {@link ServiceListener#serviceStopped(Service, Throwable)
+	 * Service-Stopped} event is fired.
+	 */
+	@Override
+	public final void run() {
+		Throwable cause = null;
+		try {
+			setState(State.online);
+			serviceListenerSupport.fireServiceStarted();
+			serviceRun();
+		} catch (Throwable t) {
+			cause = t;
+		} finally {
+			setState(State.offline, (cause != null) ? cause.getMessage() : null);
+			serviceListenerSupport.fireServiceStopped(cause);
+		}
+	}
+
+	/**
+	 * Empty implementation. If you need to perform actions on stopping, simply
+	 * override this method.
+	 *
+	 * @see #stop()
+	 */
+	protected void serviceStop() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Stops this service. This method returns immediately if the basic state of
+	 * {@link #state} is not {@link State#online} or {@link State#starting}.
+	 * Otherwise {@link #shouldStop} is set to <code>true</code>,
+	 * {@link #serviceStop} is called, and sleeping threads (if any) are
+	 * notified.
+	 *
+	 * @see net.pterodactylus.util.service.Service#stop()
+	 */
+	@Override
+	public final void stop() {
+		synchronized (syncObject) {
+			shouldStop = true;
+			syncObject.notify();
+		}
+		if (registerShutdownHook) {
+			Runtime.getRuntime().removeShutdownHook(shutdownHook);
+		}
+		serviceStop();
+	}
+
+	/**
+	 * Empty implementation. If you need to free resources used in a service,
+	 * simply override this method.
+	 *
+	 * @see #destroy()
+	 */
+	protected void serviceDestroy() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Destroys this service, freeing all used resources. This method will
+	 * return immediately if {@link #state} is not {@link State#offline}.
+	 * Otherwise {@link #serviceDestroy} is called and internal resources are
+	 * freed.
+	 *
+	 * @see net.pterodactylus.util.service.Service#destroy()
+	 */
+	@Override
+	public final void destroy() {
+		serviceDestroy();
+	}
+
+	//
+	// PROTECTED ACTIONS
+	//
+
+	/**
+	 * Sleeps until {@link #syncObject} gets {@link #notify()}'ed.
+	 */
+	protected void sleep() {
+		sleep(0);
+	}
+
+	/**
+	 * Sleeps until {@link #syncObject} gets {@link #notify()}'ed or the
+	 * specified timeout (in milliseconds) has elapsed. This method will return
+	 * immediately if the service has already been told to {@link #stop()}.
+	 *
+	 * @param timeout
+	 *            The number of milliseconds to wait
+	 */
+	protected void sleep(long timeout) {
+		synchronized (syncObject) {
+			if (!shouldStop) {
+				try {
+					syncObject.wait(timeout);
+				} catch (InterruptedException ie1) {
+					/* FIXME - ignore. */
+				}
+			}
+		}
+	}
+
+	/**
+	 * {@link Object #notify() Notifies} the {@link #syncObject} to interrupt a
+	 * {@link #sleep()}.
+	 */
+	protected void notifySyncObject() {
+		synchronized (syncObject) {
+			syncObject.notify();
+		}
+	}
+
+	/**
+	 * Shutdown hook that is run when the VM is told to exit.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private class ShutdownHook extends Thread implements ServiceListener {
+
+		/** Object used for synchronization. */
+		@SuppressWarnings("hiding")
+		private final Object syncObject = new Object();
+
+		/** Whether the service has stopped. */
+		private boolean stopped = true;
+
+		/**
+		 * Creates a new shutdown hook.
+		 */
+		public ShutdownHook() {
+			super("Shutdown Hook for " + AbstractService.this);
+			AbstractService.this.addServiceListener(this);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		public void run() {
+			logger.log(Level.INFO, "shutdown hook for " + AbstractService.this + " started.");
+			synchronized (syncObject) {
+				if (!stopped) {
+					AbstractService.this.stop();
+				}
+				while (!stopped) {
+					logger.log(Level.FINER, "waiting for " + AbstractService.this + " to stop...");
+					try {
+						syncObject.wait();
+					} catch (InterruptedException ie1) {
+						/* ignore, continue waiting. */
+					}
+				}
+			}
+			logger.log(Level.INFO, "shutdown hook for " + AbstractService.this + " finished.");
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		public void serviceStarted(Service service) {
+			logger.log(Level.FINER, AbstractService.this + " started.");
+			synchronized (syncObject) {
+				stopped = false;
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void serviceStateChanged(Service service, net.pterodactylus.util.service.State oldState, net.pterodactylus.util.service.State newState) {
+			/* ignore. */
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		@SuppressWarnings("synthetic-access")
+		public void serviceStopped(Service service, Throwable cause) {
+			logger.log(Level.FINE, AbstractService.this + " stopped.", cause);
+			synchronized (syncObject) {
+				stopped = true;
+				syncObject.notify();
+			}
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/Service.java b/alien/src/net/pterodactylus/util/service/Service.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/Service.java
@@ -0,0 +1,133 @@
+/*
+ * utils - Service.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+/**
+ * Interface for services. Services are the workhorses of every application. On
+ * application startup, services are started and begin interacting with each
+ * other, thus forming the application. Services can also contain “service
+ * attributes” which are basically {@link Object}s mapped to {@link String}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Service extends ServiceMBean {
+
+	/**
+	 * Returns the name of this service.
+	 *
+	 * @return The name of this service
+	 */
+	public String getName();
+
+	/**
+	 * Returns the current state of this service.
+	 *
+	 * @return The current state of this service
+	 */
+	@Override
+	public State getState();
+
+	/**
+	 * Returns the reason for the current state. The reason will never be
+	 * <code>null</code>.
+	 *
+	 * @deprecated Use {@link #getAction()} instead
+	 * @return The reason for the current state
+	 */
+	@Deprecated
+	public String getStateReason();
+
+	/**
+	 * Returns the current action of the service.
+	 *
+	 * @see net.pterodactylus.util.service.ServiceMBean#getAction()
+	 * @return The current action of the service
+	 */
+	@Override
+	public String getAction();
+
+	/**
+	 * Adds the given service listener to the list of service listeners that
+	 * will be notified on service events.
+	 *
+	 * @param serviceListener
+	 *            The service listener to add
+	 */
+	public void addServiceListener(ServiceListener serviceListener);
+
+	/**
+	 * Removes the given service listeners from the list of service listeners.
+	 *
+	 * @param serviceListener
+	 *            The service listener to remove
+	 */
+	public void removeServiceListener(ServiceListener serviceListener);
+
+	/**
+	 * Initializes the service.
+	 */
+	public void init();
+
+	/**
+	 * Starts the service.
+	 */
+	@Override
+	public void start();
+
+	/**
+	 * Stops the service.
+	 */
+	@Override
+	public void stop();
+
+	/**
+	 * Destroys the service and frees all used resources.
+	 */
+	public void destroy();
+
+	/**
+	 * Sets the service attribute with the given name to the given value.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute
+	 * @param attributeValue
+	 *            The value of the attribute
+	 */
+	public void setServiceAttribute(String attributeName, Object attributeValue);
+
+	/**
+	 * Returns the value of the service attribute with the given name.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute
+	 * @return The value of the attribute
+	 */
+	public Object getServiceAttribute(String attributeName);
+
+	/**
+	 * Returns whether this service contains a service attribute with the given
+	 * name.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute
+	 * @return <code>true</code> if this service has a service attribute with
+	 *         the given name, <code>false</code> otherwise
+	 */
+	public boolean hasServiceAttribute(String attributeName);
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/ServiceListener.java b/alien/src/net/pterodactylus/util/service/ServiceListener.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/ServiceListener.java
@@ -0,0 +1,58 @@
+/*
+ * utils - ServiceListener.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+import java.util.EventListener;
+
+/**
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ServiceListener extends EventListener {
+
+	/**
+	 * Notifies listeners that a {@link Service} has been started.
+	 *
+	 * @param service
+	 *            The service that started
+	 */
+	public void serviceStarted(Service service);
+
+	/**
+	 * Notifies listeners that a {@link Service} has changed its {@link State}.
+	 *
+	 * @param service
+	 *            The service that changed its state
+	 * @param oldState
+	 *            The old state of the service
+	 * @param newState
+	 *            The new state of the service
+	 */
+	public void serviceStateChanged(Service service, State oldState, State newState);
+
+	/**
+	 * Notifies listeners that a {@link Service} has been stopped.
+	 *
+	 * @param service
+	 *            The service that stopped
+	 * @param cause
+	 *            The cause for stopping, will be <code>null</code> if service
+	 *            stopped normally
+	 */
+	public void serviceStopped(Service service, Throwable cause);
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/ServiceListenerManager.java b/alien/src/net/pterodactylus/util/service/ServiceListenerManager.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/ServiceListenerManager.java
@@ -0,0 +1,75 @@
+/*
+ * utils - ServiceListenerManager.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Listener manager for {@link ServiceListener}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ServiceListenerManager extends AbstractListenerManager<Service, ServiceListener> {
+
+	/**
+	 * Creates a new listener manager for {@link ServiceListener}s.
+	 *
+	 * @param service
+	 *            The source service
+	 */
+	public ServiceListenerManager(Service service) {
+		super(service);
+	}
+
+	/**
+	 * Notifies listeners that a {@link Service} has been started.
+	 */
+	public void fireServiceStarted() {
+		for (ServiceListener serviceListener : getListeners()) {
+			serviceListener.serviceStarted(getSource());
+		}
+	}
+
+	/**
+	 * Notifies listeners that a {@link Service} has changed its {@link State}.
+	 *
+	 * @param oldState
+	 *            The old state of the service
+	 * @param newState
+	 *            The new state of the service
+	 */
+	public void fireServiceStateChanged(State oldState, State newState) {
+		for (ServiceListener serviceListener : getListeners()) {
+			serviceListener.serviceStateChanged(getSource(), oldState, newState);
+		}
+	}
+
+	/**
+	 * Notifies listeners that a {@link Service} has been stopped.
+	 *
+	 * @param cause
+	 *            The cause for stopping, will be <code>null</code> if service
+	 *            stopped normally
+	 */
+	public void fireServiceStopped(Throwable cause) {
+		for (ServiceListener serviceListener : getListeners()) {
+			serviceListener.serviceStopped(getSource(), cause);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/ServiceMBean.java b/alien/src/net/pterodactylus/util/service/ServiceMBean.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/ServiceMBean.java
@@ -0,0 +1,51 @@
+/*
+ * utils - ServiceMBean.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+/**
+ * MBean interface for all {@link Service} implementations.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ServiceMBean {
+
+	/**
+	 * Starts the service.
+	 */
+	public void start();
+
+	/**
+	 * Stops the service.
+	 */
+	public void stop();
+
+	/**
+	 * Returns the state of the service.
+	 *
+	 * @return The state of the service
+	 */
+	public State getState();
+
+	/**
+	 * Returns the current action of the service.
+	 *
+	 * @return The current action of the service
+	 */
+	public String getAction();
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/State.java b/alien/src/net/pterodactylus/util/service/State.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/State.java
@@ -0,0 +1,144 @@
+/*
+ * utils - State.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.service;
+
+/**
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class State implements Comparable<State> {
+
+	/** Basic State for services that are not running. */
+	public static final State offline = new State("offline", 0);
+
+	/** Basic State for services that are starting. */
+	public static final State starting = new State("starting", 1);
+
+	/** Basic State for services that are running. */
+	public static final State online = new State("online", 2);
+
+	/** Basic state for services that are stopping. */
+	public static final State stopping = new State("stopping", 3);
+
+	/** Basic static for services in an unknown state. */
+	public static final State unknown = new State("unknown", 4);
+
+	/** The basic state of this state. */
+	private final State basicState;
+
+	/** The name of the state. */
+	private final String name;
+
+	/** The ordinal number of the state. */
+	private final int ordinal;
+
+	/**
+	 * Constructs a new basic state with the given name and ordinal.
+	 *
+	 * @param name
+	 *            The name of the new basic state
+	 * @param ordinal
+	 *            The ordinal of the new basic state
+	 */
+	private State(String name, int ordinal) {
+		this.basicState = this;
+		this.name = name;
+		this.ordinal = ordinal;
+	}
+
+	/**
+	 * Constructs a new state that is derived from the given basic state and has
+	 * the given name.
+	 *
+	 * @param basicState
+	 *            The basic state of the new state
+	 * @param name
+	 *            The name of the new state
+	 */
+	public State(State basicState, String name) {
+		if (basicState == null) {
+			throw new IllegalArgumentException("basic state must not be null");
+		}
+		this.basicState = basicState;
+		this.name = name;
+		this.ordinal = basicState.ordinal;
+	}
+
+	/**
+	 * Returns the basic state of this state. If this state is one of the
+	 * predefined basic state, the state itself is returned so that this method
+	 * <em>never</em> returns <code>null</code>.
+	 *
+	 * @return The basic state of this state
+	 */
+	public State getBasicState() {
+		return basicState;
+	}
+
+	/**
+	 * Returns the name of this state.
+	 *
+	 * @return The name of this state
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Returns the ordinal number of this state. If this state is a derived
+	 * state the ordinal of the basic state is returned.
+	 *
+	 * @return The ordinal of this state
+	 */
+	public int getOrdinal() {
+		return ordinal;
+	}
+
+	//
+	// INTERFACE Comparable<State>
+	//
+
+	/**
+	 * Compares this state to the given state. A State is considered as smaller
+	 * than another state if the basic state’s ordinal of the first state is
+	 * smaller than the second state’s basic state’s ordinal.
+	 *
+	 * @param state
+	 *            The state that should be compared with this state
+	 * @return A negative number if this state’s ordinal is smaller than the
+	 *         ordinal of the given state’s basic state
+	 */
+	@Override
+	public int compareTo(State state) {
+		return this.basicState.ordinal - state.basicState.ordinal;
+	}
+
+	/**
+	 * Returns a textual representation of this state, consisting of the name of
+	 * this state and, if different, the name of its basic state.
+	 *
+	 * @return A textual representation of this state
+	 */
+	@Override
+	public String toString() {
+		if (this != basicState) {
+			return name + " (" + basicState + ")";
+		}
+		return name;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/service/package-info.java b/alien/src/net/pterodactylus/util/service/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/service/package-info.java
@@ -0,0 +1,56 @@
+/**
+ * Basic interface and implementation for services that can be started, stopped
+ * and run asynchronously in the background. <h2>Usage of the Service Interface</h2>
+ * <p>
+ * Using a {@link net.pterodactylus.util.service.Service} is pretty straight-forward:
+ *
+ * <pre>
+ *  Service someService = new SomeService();
+ *  someService.init();
+ *  someService.setParameter1(...);
+ *  someService.setParameter2(...);
+ *  // some more preparations
+ *  // and finally...
+ *  someService.start();
+ * </pre>
+ *
+ * <h2>Implementing Own Services</h2>
+ * <p>
+ * If you want to implement your own service it is recommended that you override
+ * the {@link net.pterodactylus.util.service.AbstractService} base class as it takes care
+ * of a couple of things for you, like creating the thread that runs your
+ * service, managing the {@link net.pterodactylus.util.service.Service#stop()} method,
+ * providing synchronized access to state variables, and other things.
+ * </p>
+ * <p>
+ * The {@link net.pterodactylus.util.service.AbstractService} base class adds a
+ * <code>service</code> method for each of the
+ * {@link net.pterodactylus.util.service.Service#init()},
+ * {@link net.pterodactylus.util.service.Service#start()},
+ * {@link net.pterodactylus.util.service.Service#stop()}, and
+ * {@link net.pterodactylus.util.service.Service#destroy()} methods that let you take care
+ * of things in your own service without having to interfere with the stuff at
+ * {@link net.pterodactylus.util.service.AbstractService} does for you. The
+ * {@link net.pterodactylus.util.service.AbstractService#serviceRun()} method is the place
+ * to implement your magic then. It should be designed as an infinite loop like
+ * this:
+ *
+ * <pre>
+ * protected void serviceRun() {
+ * 	while (!shouldStop()) {
+ * 		doStuff();
+ * 		doLengthyStuff();
+ * 		if (shouldStop()) {
+ * 			continue;
+ * 		}
+ * 		doMoreLengthyStuff();
+ * 	}
+ * }
+ * </pre>
+ *
+ * Checking the {@link net.pterodactylus.util.service.AbstractService#shouldStop()} every
+ * once in a while ensures that your service stops within a short time after
+ * {@link net.pterodactylus.util.service.Service#stop()} has been called.
+ *
+ */
+package net.pterodactylus.util.service;
\ No newline at end of file
diff --git a/alien/src/net/pterodactylus/util/swing/ComboBoxModelList.java b/alien/src/net/pterodactylus/util/swing/ComboBoxModelList.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/ComboBoxModelList.java
@@ -0,0 +1,64 @@
+/*
+ * utils - ComboBoxModelList.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.swing;
+
+import java.util.List;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.JComboBox;
+
+/**
+ * Implementation of a {@link List} that doubles as a {@link ComboBoxModel},
+ * e.g. for use with a {@link JComboBox}.
+ *
+ * @param <E>
+ *            The type of the elements
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ComboBoxModelList<E> extends ListModelList<E> implements ComboBoxModel {
+
+	/** The selected item. */
+	private Object selectedItem;
+
+	/**
+	 * Creates a new combo box model list that wraps the given list.
+	 *
+	 * @param originalList
+	 *            The original list to wrap
+	 */
+	public ComboBoxModelList(List<E> originalList) {
+		super(originalList);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getSelectedItem() {
+		return selectedItem;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setSelectedItem(Object anItem) {
+		selectedItem = anItem;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/swing/ListModelList.java b/alien/src/net/pterodactylus/util/swing/ListModelList.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/ListModelList.java
@@ -0,0 +1,354 @@
+/*
+ * utils - ListListModel.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.swing;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.JComboBox;
+import javax.swing.JList;
+import javax.swing.ListModel;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+
+/**
+ * Wrapper around a {@link List} that doubles as a {@link ListModel}, e.g. for a
+ * {@link JComboBox} or a {@link JList}.
+ *
+ * @param <E>
+ *            The type of the elements
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListModelList<E> implements ListModel, List<E> {
+
+	/** The wrapped list. */
+	private final List<E> wrappedList;
+
+	/** The list data listeners. */
+	private final List<ListDataListener> listDataListeners = new CopyOnWriteArrayList<ListDataListener>();
+
+	/**
+	 * Creates a new list model list by wrapping the given list.
+	 *
+	 * @param originalList
+	 *            The list to wrap
+	 */
+	public ListModelList(List<E> originalList) {
+		this.wrappedList = originalList;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void addListDataListener(ListDataListener listDataListener) {
+		listDataListeners.add(listDataListener);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public E getElementAt(int index) {
+		return wrappedList.get(index);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getSize() {
+		return wrappedList.size();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeListDataListener(ListDataListener listDataListener) {
+		listDataListeners.remove(listDataListener);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean add(E element) {
+		boolean added = wrappedList.add(element);
+		int position = wrappedList.size() - 1;
+		ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, position, position);
+		for (ListDataListener listDataListener : listDataListeners) {
+			listDataListener.intervalAdded(listDataEvent);
+		}
+		return added;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void add(int index, E element) {
+		wrappedList.add(element);
+		ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index);
+		for (ListDataListener listDataListener : listDataListeners) {
+			listDataListener.intervalAdded(listDataEvent);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean addAll(Collection<? extends E> collection) {
+		int firstPosition = wrappedList.size();
+		boolean changed = wrappedList.addAll(collection);
+		if (changed) {
+			ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, firstPosition, wrappedList.size() - 1);
+			for (ListDataListener listDataListener : listDataListeners) {
+				listDataListener.intervalAdded(listDataEvent);
+			}
+		}
+		return changed;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean addAll(int index, Collection<? extends E> collection) {
+		boolean changed = wrappedList.addAll(collection);
+		if (changed) {
+			ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index + collection.size() - 1);
+			for (ListDataListener listDataListener : listDataListeners) {
+				listDataListener.intervalAdded(listDataEvent);
+			}
+		}
+		return changed;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void clear() {
+		if (!wrappedList.isEmpty()) {
+			int size = wrappedList.size();
+			wrappedList.clear();
+			ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
+			for (ListDataListener listDataListener : listDataListeners) {
+				listDataListener.intervalRemoved(listDataEvent);
+			}
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean contains(Object object) {
+		return wrappedList.contains(object);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean containsAll(Collection<?> collection) {
+		return wrappedList.containsAll(collection);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public E get(int index) {
+		return wrappedList.get(index);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int indexOf(Object object) {
+		return wrappedList.indexOf(object);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean isEmpty() {
+		return wrappedList.isEmpty();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Iterator<E> iterator() {
+		return wrappedList.iterator();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int lastIndexOf(Object object) {
+		return wrappedList.lastIndexOf(object);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public ListIterator<E> listIterator() {
+		return wrappedList.listIterator();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public ListIterator<E> listIterator(int index) {
+		return wrappedList.listIterator(index);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean remove(Object object) {
+		int index = wrappedList.indexOf(object);
+		if (index != -1) {
+			wrappedList.remove(object);
+			ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index);
+			for (ListDataListener listDataListener : listDataListeners) {
+				listDataListener.intervalRemoved(listDataEvent);
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public E remove(int index) {
+		E removedElement = wrappedList.remove(index);
+		ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index);
+		for (ListDataListener listDataListener : listDataListeners) {
+			listDataListener.intervalRemoved(listDataEvent);
+		}
+		return removedElement;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean removeAll(Collection<?> collection) {
+		int lowestPosition = Integer.MAX_VALUE;
+		int highestPosition = Integer.MIN_VALUE;
+		for (Object element : collection) {
+			int position = wrappedList.indexOf(element);
+			if (position == -1) {
+				continue;
+			}
+			if (position < lowestPosition) {
+				lowestPosition = position;
+			}
+			if (position > highestPosition) {
+				highestPosition = position;
+			}
+		}
+		if (lowestPosition < Integer.MAX_VALUE) {
+			for (Object element : collection) {
+				wrappedList.remove(element);
+			}
+			ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, lowestPosition, highestPosition);
+			for (ListDataListener listDataListener : listDataListeners) {
+				listDataListener.contentsChanged(listDataEvent);
+			}
+		}
+		return (lowestPosition < Integer.MAX_VALUE);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean retainAll(Collection<?> collection) {
+		int size = wrappedList.size();
+		boolean changed = wrappedList.retainAll(collection);
+		ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, size - 1);
+		for (ListDataListener listDataListener : listDataListeners) {
+			listDataListener.contentsChanged(listDataEvent);
+		}
+		return changed;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public E set(int index, E element) {
+		E oldElement = wrappedList.set(index, element);
+		ListDataEvent listDataEvent = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, index, index);
+		for (ListDataListener listDataListener : listDataListeners) {
+			listDataListener.contentsChanged(listDataEvent);
+		}
+		return oldElement;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int size() {
+		return wrappedList.size();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<E> subList(int fromIndex, int toIndex) {
+		return wrappedList.subList(fromIndex, toIndex);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object[] toArray() {
+		return wrappedList.toArray();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public <T> T[] toArray(T[] a) {
+		return wrappedList.toArray(a);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/swing/SortableTreeNode.java b/alien/src/net/pterodactylus/util/swing/SortableTreeNode.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/SortableTreeNode.java
@@ -0,0 +1,274 @@
+/*
+ * utils - SortableTreeNode.java - Copyright © 2008-2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.swing;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+
+import javax.swing.tree.MutableTreeNode;
+import javax.swing.tree.TreeNode;
+
+/**
+ * {@link MutableTreeNode} subclass that allows to sort its children.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SortableTreeNode implements MutableTreeNode {
+
+	/** The parent node. */
+	private MutableTreeNode parentNode;
+
+	/** The user-defined object. */
+	private Object userObject;
+
+	/** Whether this node allows children. */
+	private boolean allowsChildren;
+
+	/** The children of this node. */
+	private List<MutableTreeNode> children = new ArrayList<MutableTreeNode>();
+
+	/**
+	 * Creates a new sortable tree node.
+	 *
+	 * @param allowsChildren
+	 *            <code>true</code> if this node allows children,
+	 *            <code>false</code> otherwise
+	 */
+	public SortableTreeNode(boolean allowsChildren) {
+		this(null, allowsChildren);
+	}
+
+	/**
+	 * Creates a new sortable tree node that contains the given user-defined
+	 * object.
+	 *
+	 * @param userObject
+	 *            The user-defined object
+	 */
+	public SortableTreeNode(Object userObject) {
+		this(userObject, true);
+	}
+
+	/**
+	 * Creates a new sortable tree node that contains the given user-defined
+	 * object.
+	 *
+	 * @param userObject
+	 *            The user-defined object
+	 * @param allowsChildren
+	 *            <code>true</code> if this node allows children,
+	 *            <code>false</code> otherwise
+	 */
+	public SortableTreeNode(Object userObject, boolean allowsChildren) {
+		this.allowsChildren = allowsChildren;
+		this.userObject = userObject;
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean getAllowsChildren() {
+		return allowsChildren;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public TreeNode getChildAt(int childIndex) {
+		return children.get(childIndex);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getChildCount() {
+		return children.size();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public int getIndex(TreeNode node) {
+		return children.indexOf(node);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public TreeNode getParent() {
+		return parentNode;
+	}
+
+	/**
+	 * Returns the user-defined object.
+	 *
+	 * @return The user-defined object
+	 */
+	public Object getUserObject() {
+		return userObject;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean isLeaf() {
+		return children.isEmpty();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Enumeration<?> children() {
+		return Collections.enumeration(children);
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Adds the given node to this node as a child.
+	 *
+	 * @param child
+	 *            The child node to add
+	 */
+	public void add(MutableTreeNode child) {
+		children.add(child);
+		child.setParent(this);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void insert(MutableTreeNode child, int index) {
+		children.add(index, child);
+		child.setParent(this);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void remove(int index) {
+		children.remove(index).setParent(null);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void remove(MutableTreeNode node) {
+		children.remove(node);
+		node.setParent(null);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void removeFromParent() {
+		if (parentNode != null) {
+			parentNode.remove(this);
+			parentNode = null;
+		}
+	}
+
+	/**
+	 * Removes all children of this node.
+	 */
+	public void removeAll() {
+		for (MutableTreeNode childNode : children) {
+			childNode.setParent(null);
+		}
+		children.clear();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setParent(MutableTreeNode newParent) {
+		parentNode = newParent;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setUserObject(Object userObject) {
+		this.userObject = userObject;
+	}
+
+	/**
+	 * Sorts the children of this node.
+	 */
+	public void sort() {
+		Collections.sort(children, new Comparator<MutableTreeNode>() {
+
+			/**
+			 * {@inheritDoc}
+			 */
+			@Override
+			@SuppressWarnings( { "synthetic-access", "unchecked" })
+			public int compare(MutableTreeNode firstNode, MutableTreeNode secondNode) {
+				if (!(firstNode instanceof SortableTreeNode) || !(secondNode instanceof SortableTreeNode)) {
+					return 0;
+				}
+				SortableTreeNode firstSortableNode = (SortableTreeNode) firstNode;
+				SortableTreeNode secondSortableNode = (SortableTreeNode) secondNode;
+				if ((firstSortableNode.userObject == null) && (secondSortableNode.userObject == null)) {
+					return 0;
+				}
+				if ((firstSortableNode.userObject == null) && (secondSortableNode.userObject != null)) {
+					return -1;
+				}
+				if ((firstSortableNode.userObject != null) && (secondSortableNode.userObject == null)) {
+					return 1;
+				}
+				if (!(firstSortableNode.userObject instanceof Comparable) || !(secondSortableNode.userObject instanceof Comparable)) {
+					return 0;
+				}
+				return ((Comparable<Object>) firstSortableNode.userObject).compareTo(secondSortableNode.userObject);
+			}
+		});
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String toString() {
+		return (userObject != null) ? userObject.toString() : null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/swing/StatusBar.java b/alien/src/net/pterodactylus/util/swing/StatusBar.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/StatusBar.java
@@ -0,0 +1,135 @@
+/*
+ * utils - StatusBar.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.EtchedBorder;
+
+/**
+ * Status bar component that can be added to the {@link BorderLayout#SOUTH} area
+ * of a {@link JFrame}’s {@link JFrame#getContentPane() content pane}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+/* TODO - make it possible to add further components. */
+public class StatusBar extends JPanel {
+
+	/** The layout. */
+	private GridBagLayout layout = new GridBagLayout();
+
+	/** The label. */
+	private JLabel statusLabel = new JLabel(" ");
+
+	/** Addition components. */
+	private List<Component> sideComponents = new ArrayList<Component>();
+
+	/**
+	 * Creates a new status bar.
+	 */
+	public StatusBar() {
+		setLayout(layout);
+		statusLabel.setBorder(new CompoundBorder(new EtchedBorder(EtchedBorder.LOWERED), new EmptyBorder(0, 3, 0, 0)));
+		add(statusLabel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+	}
+
+	/**
+	 * Clears the status bar.
+	 */
+	public void clear() {
+		statusLabel.setText(" ");
+	}
+
+	/**
+	 * Sets the text of the label.
+	 *
+	 * @param text
+	 *            The text of the label
+	 */
+	public void setText(String text) {
+		statusLabel.setText(text);
+	}
+
+	/**
+	 * Adds a side component to the right side of the status bar, pushing all
+	 * previously added side components to the left.
+	 *
+	 * @param component
+	 *            The component to add
+	 */
+	public void addSideComponent(Component component) {
+		sideComponents.add(component);
+		int newIndex = sideComponents.size();
+		add(component, new GridBagConstraints(newIndex, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 2, 0, 0), 0, 0));
+		validate();
+	}
+
+	/**
+	 * Returns the number of side components.
+	 *
+	 * @return The number of side components
+	 */
+	public int getSideComponentCount() {
+		return sideComponents.size();
+	}
+
+	/**
+	 * Returns all side components in order.
+	 *
+	 * @return All side components
+	 */
+	public List<Component> getSideComponents() {
+		return sideComponents;
+	}
+
+	/**
+	 * Removes the side component with the given index.
+	 *
+	 * @param sideComponentIndex
+	 *            The index of the side component to remove
+	 */
+	public void removeSideComponent(int sideComponentIndex) {
+		Component sideComponent = sideComponents.remove(sideComponentIndex);
+		remove(sideComponent);
+		validate();
+	}
+
+	/**
+	 * Removes the given side component.
+	 *
+	 * @param sideComponent
+	 *            The side component to remove
+	 */
+	public void removeSideComponent(Component sideComponent) {
+		sideComponents.remove(sideComponent);
+		remove(sideComponent);
+		validate();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/swing/SwingUtils.java b/alien/src/net/pterodactylus/util/swing/SwingUtils.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/SwingUtils.java
@@ -0,0 +1,85 @@
+/*
+ * utils - SwingUtils.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.swing;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.Window;
+
+/**
+ * Collection of Swing-related helper methods.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SwingUtils {
+
+	/**
+	 * Centers the given window on the primary screen.
+	 *
+	 * @param window
+	 *            The window to center
+	 */
+	public static void center(Window window) {
+		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+		Dimension windowSize = window.getSize();
+		window.setLocation((screenSize.width - windowSize.width) / 2, (screenSize.height - windowSize.height) / 2);
+	}
+
+	/**
+	 * Centers the given window over its owner window.
+	 *
+	 * @param window
+	 *            The window to center
+	 * @param ownerWindow
+	 *            The window to center the other window over
+	 */
+	public static void center(Window window, Window ownerWindow) {
+		Point ownerWindowLocation = ownerWindow.getLocation();
+		Dimension ownerWindowDimension = ownerWindow.getSize();
+		Dimension windowSize = window.getSize();
+		window.setLocation(ownerWindowLocation.x + (ownerWindowDimension.width - windowSize.width) / 2, ownerWindowLocation.y + (ownerWindowDimension.height - windowSize.height) / 2);
+	}
+
+	/**
+	 * {@link Window#pack() Packs} the given window and positions it so that its
+	 * center stays the same.
+	 *
+	 * @param window
+	 *            The window to pack and recenter
+	 */
+	public static void repackCentered(Window window) {
+		Point center = getCenter(window.getBounds());
+		window.pack();
+		window.setLocation((center.x - window.getWidth() / 2), (center.y - window.getHeight() / 2));
+		window.repaint();
+	}
+
+	/**
+	 * Returns the center of the given rectangle.
+	 *
+	 * @param bounds
+	 *            The rectangle which center to get
+	 * @return The center of the rectangle
+	 */
+	private static Point getCenter(Rectangle bounds) {
+		return new Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/swing/ToolTipList.java b/alien/src/net/pterodactylus/util/swing/ToolTipList.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/swing/ToolTipList.java
@@ -0,0 +1,84 @@
+
+package net.pterodactylus.util.swing;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.event.MouseEvent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.JComponent;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListModel;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Extension of {@link JList} that retrieves the tool tip text to display from
+ * the cell renderer. This can be used to let a custom {@link ListCellRenderer}
+ * return e.g. a {@link JPanel} with multiple components that can return
+ * different tool tips.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ToolTipList extends JList {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(ToolTipList.class);
+
+	/** The underlying list model. */
+	private final ListModel listModel;
+
+	/**
+	 * Creates a new tool tip list.
+	 *
+	 * @param listModel
+	 *            The underlying list model
+	 */
+	public ToolTipList(ListModel listModel) {
+		super(listModel);
+		this.listModel = listModel;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String getToolTipText(MouseEvent event) {
+		logger.log(Level.FINEST, "Getting Tooltip for " + event + "…");
+		int row = locationToIndex(event.getPoint());
+		logger.log(Level.FINEST, "Mouse is over row " + row + ".");
+		if (row < 0) {
+			return null;
+		}
+		Object value = listModel.getElementAt(row);
+		logger.log(Level.FINEST, "Getting Tooltip for “" + value + "”…");
+		if (value == null) {
+			return null;
+		}
+		Component cell = getCellRenderer().getListCellRendererComponent(this, value, row, false, false);
+		logger.log(Level.FINEST, "CellRenderer gave us Cell " + cell + ".");
+		if (cell == null) {
+			return null;
+		}
+		int cellX = cell.getX();
+		int cellY = cell.getY();
+		cell.setSize(new Dimension(-cellX, -cellY));
+		cell.setLocation(0, 0);
+		Point cellLocation = indexToLocation(row);
+		Point mousePosition = event.getPoint();
+		mousePosition.translate(-(int) cellLocation.getX(), -(int) cellLocation.getY());
+		logger.log(Level.FINEST, "Mouse Position translates to " + mousePosition + ".");
+		Component toolTipComponent = cell.getComponentAt(mousePosition);
+		cell.setLocation(cellX, cellY);
+		logger.log(Level.FINEST, "Component under Mouse is " + toolTipComponent + ".");
+		if (toolTipComponent instanceof JComponent) {
+			return ((JComponent) toolTipComponent).getToolTipText();
+		}
+		return null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/AbstractCommand.java b/alien/src/net/pterodactylus/util/telnet/AbstractCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/AbstractCommand.java
@@ -0,0 +1,115 @@
+/*
+ * utils - AbstractCommand.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstract base implementation of a {@link Command}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractCommand implements Command {
+
+	/** The name of this command. */
+	private final String name;
+
+	/** The brief description of this command. */
+	private final String briefDescription;
+
+	/** The detailed description of this command. */
+	private final List<String> detailedDescription = new ArrayList<String>();
+
+	/**
+	 * Creates a new command with the given name and description.
+	 *
+	 * @param name
+	 *            The name of the command
+	 * @param briefDescription
+	 *            The brief description of this command.
+	 */
+	protected AbstractCommand(String name, String briefDescription) {
+		this.name = name;
+		this.briefDescription = briefDescription;
+	}
+
+	/**
+	 * Creates a new command with the given name, brief description and detailed
+	 * description.
+	 *
+	 * @param name
+	 *            The name of the command
+	 * @param briefDescription
+	 *            The brief description of this command.
+	 * @param detailedDescriptions
+	 *            The detailed descriptions of this command
+	 */
+	protected AbstractCommand(String name, String briefDescription, String... detailedDescriptions) {
+		this.name = name;
+		this.briefDescription = briefDescription;
+		for (String detailedDescription : detailedDescriptions) {
+			this.detailedDescription.add(detailedDescription);
+		}
+	}
+
+	/**
+	 * Creates a new command with the given name, brief description and detailed
+	 * description.
+	 *
+	 * @param name
+	 *            The name of the command
+	 * @param briefDescription
+	 *            The brief description of this command.
+	 * @param detailedDescription
+	 *            The detailed description of this command
+	 */
+	protected AbstractCommand(String name, String briefDescription, List<String> detailedDescription) {
+		this(name, briefDescription);
+		this.detailedDescription.addAll(detailedDescription);
+	}
+
+	//
+	// INTERFACE Command
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String getBriefDescription() {
+		return briefDescription;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public List<String> getDetailedDescription() {
+		return detailedDescription;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/Command.java b/alien/src/net/pterodactylus/util/telnet/Command.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/Command.java
@@ -0,0 +1,168 @@
+/*
+ * utils - Command.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Basic structure of a command that can be sent to a {@link TelnetControl}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Command {
+
+	/**
+	 * Returns the name of this command. The name is parsed case-insensitively.
+	 *
+	 * @return The name of this command
+	 */
+	public String getName();
+
+	/**
+	 * Returns a brief description of this command.
+	 *
+	 * @return The brief description of this command
+	 */
+	public String getBriefDescription();
+
+	/**
+	 * Returns a detailed description of this command.
+	 *
+	 * @return The detailed description of this command
+	 */
+	public List<String> getDetailedDescription();
+
+	/**
+	 * Executes the command.
+	 *
+	 * @param parameters
+	 *            The parameter of this command
+	 * @return The reply of the command
+	 */
+	public Reply execute(List<String> parameters);
+
+	/**
+	 * A reply to a {@link Command}.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public class Reply {
+
+		/** The “OK” status. */
+		public static final int OK = 200;
+
+		/** The “multiple choices” status. */
+		public static final int MULTIPLE_CHOICES = 300;
+
+		/** The “bad request” status. */
+		public static final int BAD_REQUEST = 400;
+
+		/** The “not found” status. */
+		public static final int NOT_FOUND = 404;
+
+		/** The “internal server error” status. */
+		public static final int INTERNAL_SERVER_ERROR = 500;
+
+		/** The status of the reply. */
+		private final int status;
+
+		/** The lines that make up the reply. */
+		private final List<String> lines = new ArrayList<String>();
+
+		/**
+		 * Creates a new reply with the given status.
+		 *
+		 * @param status
+		 *            The status of the reply
+		 */
+		public Reply(int status) {
+			this.status = status;
+		}
+
+		/**
+		 * Creates a new reply with the given status and line.
+		 *
+		 * @param status
+		 *            The status of the reply
+		 * @param line
+		 *            The line of the reply
+		 */
+		public Reply(int status, String line) {
+			this(status, new String[] { line });
+		}
+
+		/**
+		 * Creates a new reply with the given status and lines.
+		 *
+		 * @param status
+		 *            The status of the reply
+		 * @param lines
+		 *            The lines of the reply
+		 */
+		public Reply(int status, String... lines) {
+			this(status, Arrays.asList(lines));
+		}
+
+		/**
+		 * Creates a new reply with the given status and lines.
+		 *
+		 * @param status
+		 *            The status of the reply
+		 * @param lines
+		 *            The lines of the reply
+		 */
+		public Reply(int status, List<String> lines) {
+			this.status = status;
+			this.lines.addAll(lines);
+		}
+
+		/**
+		 * Adds a line to the reply.
+		 *
+		 * @param line
+		 *            The line to add
+		 * @return This reply (for method invocation chaining)
+		 */
+		public Reply addLine(String line) {
+			lines.add(line);
+			return this;
+		}
+
+		/**
+		 * Returns the status of this command.
+		 *
+		 * @return The status of this command
+		 */
+		public int getStatus() {
+			return status;
+		}
+
+		/**
+		 * Returns the lines that make up this reply.
+		 *
+		 * @return The lines of this reply
+		 */
+		public List<String> getLines() {
+			return lines;
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/ControlConnection.java b/alien/src/net/pterodactylus/util/telnet/ControlConnection.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/ControlConnection.java
@@ -0,0 +1,241 @@
+
+package net.pterodactylus.util.telnet;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.service.AbstractService;
+import net.pterodactylus.util.telnet.Command.Reply;
+import net.pterodactylus.util.text.StringEscaper;
+import net.pterodactylus.util.text.TextException;
+
+/**
+ * Handles a single client connection.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ControlConnection extends AbstractService {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(ControlConnection.class.getName());
+
+	/** The line break. */
+	private static final String LINEFEED = "\r\n";
+
+	/** The client’s input stream. */
+	private final InputStream clientInputStream;
+
+	/** The client’s output stream. */
+	private final OutputStream clientOutputStream;
+
+	/** The output stream writer. */
+	private final PrintWriter outputStreamWriter;
+
+	/** Mapping from command names to commands. */
+	Map<String, Command> commands = new HashMap<String, Command>();
+
+	/** Mapping from internal command names to commands. */
+	Map<String, Command> internalCommands = new HashMap<String, Command>();
+
+	/**
+	 * Creates a new connection handler for a client on the given socket.
+	 *
+	 * @param clientInputStream
+	 *            The client input stream
+	 * @param clientOutputStream
+	 *            The client output stream
+	 */
+	public ControlConnection(InputStream clientInputStream, OutputStream clientOutputStream) {
+		this.clientInputStream = clientInputStream;
+		this.clientOutputStream = clientOutputStream;
+		this.outputStreamWriter = new PrintWriter(clientOutputStream);
+		addCommand(new QuitCommand());
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Adds the given command to this control.
+	 *
+	 * @param command
+	 *            The command to add
+	 */
+	public void addCommand(Command command) {
+		commands.put(command.getName().toLowerCase(), command);
+		internalCommands.put("help", new HelpCommand(commands.values()));
+	}
+
+	//
+	// ACTIONS
+	//
+
+	/**
+	 * Prints the given line to the output stream.
+	 *
+	 * @param line
+	 *            The line to print
+	 */
+	public void addOutputLine(String line) {
+		addOutputLines(line);
+	}
+
+	/**
+	 * Prints the given lines to the output stream.
+	 *
+	 * @param lines
+	 *            The lines to print
+	 */
+	public void addOutputLines(String... lines) {
+		synchronized (outputStreamWriter) {
+			for (String line : lines) {
+				outputStreamWriter.println(line);
+			}
+			outputStreamWriter.flush();
+		}
+	}
+
+	//
+	// SERVICE METHODS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void serviceRun() {
+		InputStreamReader inputStreamReader = null;
+		BufferedReader bufferedReader = null;
+		try {
+			inputStreamReader = new InputStreamReader(clientInputStream);
+			bufferedReader = new BufferedReader(inputStreamReader);
+			String line;
+			boolean finished = false;
+			while (!finished && ((line = bufferedReader.readLine()) != null)) {
+				line = line.trim();
+				if (line.length() == 0) {
+					continue;
+				}
+				List<String> words;
+				try {
+					words = StringEscaper.parseLine(line);
+				} catch (TextException te1) {
+					writeReply(new Reply(Reply.BAD_REQUEST).addLine("Syntax error."));
+					continue;
+				}
+				if (words.isEmpty()) {
+					continue;
+				}
+				String commandName = words.remove(0).toLowerCase();
+				List<Command> foundCommands = findCommand(commandName);
+				if (foundCommands.isEmpty()) {
+					writeReply(new Reply(Reply.NOT_FOUND).addLine("Command not found."));
+				} else if (foundCommands.size() == 1) {
+					Command command = foundCommands.get(0);
+					try {
+						Reply commandReply = command.execute(words);
+						writeReply(commandReply);
+					} catch (IOException ioe1) {
+						throw ioe1;
+					} catch (Throwable t1) {
+						writeReply(new Reply(Reply.INTERNAL_SERVER_ERROR).addLine("Internal server error: " + t1.getMessage()));
+					}
+					if (command instanceof QuitCommand) {
+						finished = true;
+					}
+				} else {
+					Reply reply = new Reply(Reply.MULTIPLE_CHOICES, "Multiple choices found:");
+					for (Command command : foundCommands) {
+						reply.addLine(command.getName());
+					}
+					writeReply(reply);
+				}
+			}
+		} catch (IOException ioe1) {
+			logger.log(Level.INFO, "could not handle connection", ioe1);
+		} finally {
+			Closer.close(outputStreamWriter);
+			Closer.close(bufferedReader);
+			Closer.close(inputStreamReader);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void serviceStop() {
+		Closer.close(clientInputStream);
+		Closer.close(clientOutputStream);
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Searches both internal and user commands for a command. A command must
+	 * have a name that equals or starts with the given name to be a match.
+	 *
+	 * @param name
+	 *            The name of the command
+	 * @return All found commands
+	 */
+	private List<Command> findCommand(String name) {
+		List<Command> foundCommands = new ArrayList<Command>();
+		for (Command command : internalCommands.values()) {
+			if (command.getName().toLowerCase().startsWith(name.toLowerCase())) {
+				foundCommands.add(command);
+			}
+		}
+		for (Command command : commands.values()) {
+			if (command.getName().toLowerCase().startsWith(name.toLowerCase())) {
+				foundCommands.add(command);
+			}
+		}
+		return foundCommands;
+	}
+
+	/**
+	 * Writes the given reply to the client’s output stream. The
+	 * <code>reply</code> may be <code>null</code> in which case an appropriate
+	 * error message is written.
+	 *
+	 * @param reply
+	 *            The reply to send
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private void writeReply(Reply reply) throws IOException {
+		synchronized (outputStreamWriter) {
+			if (reply == null) {
+				outputStreamWriter.write("500 Internal server error." + LINEFEED);
+				outputStreamWriter.flush();
+				return;
+			}
+			int status = reply.getStatus();
+			List<String> lines = reply.getLines();
+			for (int lineIndex = 0, lineCount = lines.size(); lineIndex < lineCount; lineIndex++) {
+				outputStreamWriter.write(status + ((lineIndex < (lineCount - 1)) ? "-" : " ") + lines.get(lineIndex) + LINEFEED);
+			}
+			if (lines.size() == 0) {
+				outputStreamWriter.write("200 OK." + LINEFEED);
+			}
+			outputStreamWriter.flush();
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/GarbageCollectionCommand.java b/alien/src/net/pterodactylus/util/telnet/GarbageCollectionCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/GarbageCollectionCommand.java
@@ -0,0 +1,28 @@
+package net.pterodactylus.util.telnet;
+
+import java.util.List;
+
+/**
+ * Command that performs a garbage collection.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GarbageCollectionCommand extends AbstractCommand {
+
+	/**
+	 * Creates a new garbage collection command.
+	 */
+	public GarbageCollectionCommand() {
+		super("GC", "Performs a garbage collection.");
+	}
+
+	/**
+	 * @see net.pterodactylus.util.telnet.Command#execute(java.util.List)
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		System.gc();
+		return new Reply(200, "Garbage Collection suggested.");
+	}
+
+}
\ No newline at end of file
diff --git a/alien/src/net/pterodactylus/util/telnet/HelpCommand.java b/alien/src/net/pterodactylus/util/telnet/HelpCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/HelpCommand.java
@@ -0,0 +1,78 @@
+/*
+ * utils - HelpCommand - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Command that outputs help information about commands.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HelpCommand extends AbstractCommand {
+
+	/** The list of commands to show help for. */
+	private final Collection<Command> commands;
+
+	/**
+	 * Creates a new HELP command.
+	 *
+	 * @param commands
+	 *            The commands to show help about
+	 */
+	public HelpCommand(Collection<Command> commands) {
+		super("HELP", "outputs help of all commands");
+		this.commands = commands;
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	//
+	// INTERFACE Command
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		Reply reply = new Reply(200);
+
+		if (parameters.isEmpty()) {
+			for (Command command : commands) {
+				reply.addLine(command.getName().toUpperCase() + ": " + command.getBriefDescription());
+			}
+		} else {
+			String commandName = parameters.get(0).toLowerCase();
+			for (Command command : commands) {
+				if (command.getName().toLowerCase().startsWith(commandName)) {
+					reply.addLine(command.getName().toUpperCase() + ": " + command.getBriefDescription());
+					for (String detailDescription : command.getDetailedDescription()) {
+						reply.addLine("  " + detailDescription);
+					}
+				}
+			}
+		}
+
+		return reply;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/MemoryCommand.java b/alien/src/net/pterodactylus/util/telnet/MemoryCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/MemoryCommand.java
@@ -0,0 +1,56 @@
+/*
+ * utils - MemoryCommand.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.pterodactylus.util.number.Digits;
+import net.pterodactylus.util.number.SI;
+
+/**
+ * Command that outputs some memory statistics.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MemoryCommand extends AbstractCommand {
+
+	/**
+	 * Creates a new memory command.
+	 */
+	public MemoryCommand() {
+		super("MEMORY", "Shows memory statistics.");
+	}
+
+	/**
+	 * @see net.pterodactylus.util.telnet.Command#execute(java.util.List)
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		long freeMemory = Runtime.getRuntime().freeMemory();
+		long totalMemory = Runtime.getRuntime().totalMemory();
+		long maxMemory = Runtime.getRuntime().maxMemory();
+		long usedMemory = totalMemory - freeMemory;
+		List<String> lines = new ArrayList<String>();
+		lines.add("Used Memory: " + SI.format(usedMemory, 1, true, true) + "B");
+		lines.add("Reversed Memory: " + SI.format(totalMemory, 1, true, true) + "B, " + Digits.formatFractions(usedMemory * 100.0 / totalMemory, 1, false) + "% used");
+		lines.add("Maximum Memory: " + SI.format(maxMemory, 1, true, true) + "B, " + Digits.formatFractions(usedMemory * 100.0 / maxMemory, 1, false) + "% used");
+		return new Reply(200, lines);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/QuitCommand.java b/alien/src/net/pterodactylus/util/telnet/QuitCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/QuitCommand.java
@@ -0,0 +1,44 @@
+/*
+ * utils - QuitCommand.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.List;
+
+/**
+ * Special command that closes the connection to the telnet control.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class QuitCommand extends AbstractCommand {
+
+	/**
+	 * Creates a new quit command.
+	 */
+	public QuitCommand() {
+		super("QUIT", "Closes the connection.");
+	}
+
+	/**
+	 * @see net.pterodactylus.util.telnet.Command#execute(java.util.List)
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		return new Reply(200, "Goodbye.");
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/TelnetControl.java b/alien/src/net/pterodactylus/util/telnet/TelnetControl.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/TelnetControl.java
@@ -0,0 +1,141 @@
+/*
+ * utils - TelnetControl.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.service.AbstractService;
+
+/**
+ * TODO
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TelnetControl extends AbstractService {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(TelnetControl.class.getName());
+
+	/** The server socket. */
+	private ServerSocket serverSocket;
+
+	/** The port to listen on. */
+	private int listenPort = 20013;
+
+	/** Mapping from command names to commands. */
+	private final Map<String, Command> commands = new HashMap<String, Command>();
+
+	/**
+	 * Creates a new telnet control.
+	 */
+	public TelnetControl() {
+		super("Telnet Control");
+	}
+
+	//
+	// ACCESSORS
+	//
+
+	/**
+	 * Sets the port to listen on.
+	 *
+	 * @param listenPort
+	 *            The port to listen on
+	 */
+	public void setListenPort(int listenPort) {
+		this.listenPort = listenPort;
+	}
+
+	/**
+	 * Adds the given command to this control.
+	 *
+	 * @param command
+	 *            The command to add
+	 */
+	public void addCommand(Command command) {
+		commands.put(command.getName().toLowerCase(), command);
+	}
+
+	//
+	// SERVICE METHODS
+	//
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void serviceRun() {
+		logger.log(Level.INFO, "starting telnet control main loop");
+		try {
+			serverSocket = new ServerSocket(listenPort);
+		} catch (IOException ioe1) {
+			logger.log(Level.SEVERE, "could not create server socket on port " + listenPort, ioe1);
+			return;
+		}
+		while (!shouldStop()) {
+			try {
+				Socket clientSocket = serverSocket.accept();
+				logger.log(Level.INFO, "acception client connection on " + clientSocket.getRemoteSocketAddress());
+				ControlConnection controlConnection = new ControlConnection(clientSocket.getInputStream(), clientSocket.getOutputStream());
+				for (Command command : commands.values()) {
+					controlConnection.addCommand(command);
+				}
+				controlConnection.start();
+			} catch (IOException ioe1) {
+				if (!shouldStop()) {
+					logger.log(Level.WARNING, "could not accept client connection", ioe1);
+				}
+			}
+		}
+		logger.log(Level.INFO, "stopped telnet control main loop.");
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void serviceStop() {
+		Closer.close(serverSocket);
+	}
+
+	/**
+	 * VM entry point for testing.
+	 *
+	 * @param arguments
+	 *            Command-line arguments
+	 * @throws InterruptedException
+	 *             if {@link Thread#sleep(long)} is interrupted
+	 */
+	public static void main(String... arguments) throws InterruptedException {
+		TelnetControl telnetControl = new TelnetControl();
+		telnetControl.init();
+		telnetControl.addCommand(new MemoryCommand());
+		telnetControl.addCommand(new GarbageCollectionCommand());
+		telnetControl.start();
+		Thread.sleep(120 * 1000);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/UptimeCommand.java b/alien/src/net/pterodactylus/util/telnet/UptimeCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/UptimeCommand.java
@@ -0,0 +1,49 @@
+/*
+ * utils - UptimeCommand.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.List;
+
+import net.pterodactylus.util.time.Duration;
+
+/**
+ * Command that prints out the current uptime.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UptimeCommand extends AbstractCommand {
+
+	/**
+	 * Creates a new uptime command.
+	 */
+	public UptimeCommand() {
+		super("UPTIME", "Prints uptime information");
+	}
+
+	/** The startup time. */
+	private static final long startupTime = System.currentTimeMillis();
+
+	/**
+	 * @see net.pterodactylus.util.telnet.Command#execute(java.util.List)
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		return new Reply(200, new Duration(System.currentTimeMillis() - startupTime).toString(false));
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/telnet/VersionCommand.java b/alien/src/net/pterodactylus/util/telnet/VersionCommand.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/telnet/VersionCommand.java
@@ -0,0 +1,57 @@
+/*
+ * utils - VersionCommand.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.telnet;
+
+import java.util.List;
+
+/**
+ * Replies with the name of the application and the version number.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class VersionCommand extends AbstractCommand {
+
+	/** The name of the application. */
+	private final String application;
+
+	/** The version of the application. */
+	private final String version;
+
+	/**
+	 * Creates a new version command.
+	 *
+	 * @param application
+	 *            The name of the application
+	 * @param version
+	 *            The version of the application
+	 */
+	public VersionCommand(String application, String version) {
+		super("VERSION", "Shows version information about " + application + ".");
+		this.application = application;
+		this.version = version;
+	}
+
+	/**
+	 * @see net.pterodactylus.util.telnet.Command#execute(java.util.List)
+	 */
+	@Override
+	public Reply execute(List<String> parameters) {
+		return new Reply(200, application + " " + version);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/Accessor.java b/alien/src/net/pterodactylus/util/template/Accessor.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/Accessor.java
@@ -0,0 +1,62 @@
+/*
+ * utils - Accessor.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * An accessor can access member variables of objects of a given type.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Accessor {
+
+	/** Accessor for {@link Map}s. */
+	public static final Accessor MAP_ACCESSOR = new MapAccessor();
+
+	/**
+	 * Returns the member with the given name.
+	 *
+	 * @param dataProvider
+	 *            The current data provider
+	 * @param object
+	 *            The object to access
+	 * @param member
+	 *            The name of the member
+	 * @return The member, or {@code null} if the member does not exist
+	 */
+	public Object get(DataProvider dataProvider, Object object, String member);
+
+}
+
+/**
+ * {@link Accessor} implementation that can access values in a {@link Map}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class MapAccessor implements Accessor {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object get(DataProvider dataProvider, Object object, String member) {
+		return ((Map<?, ?>) object).get(member);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/ConditionalPart.java b/alien/src/net/pterodactylus/util/template/ConditionalPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/ConditionalPart.java
@@ -0,0 +1,521 @@
+/*
+ * utils - ConditionalPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * {@link ContainerPart} implementation that determines at render time whether
+ * it should be rendered or not.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class ConditionalPart extends ContainerPart {
+
+	/** The condition. */
+	private final Condition condition;
+
+	/**
+	 * Creates a new conditional part.
+	 *
+	 * @param condition
+	 *            The condition
+	 */
+	public ConditionalPart(Condition condition) {
+		super();
+		this.condition = condition;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		if (condition.isAllowed(dataProvider)) {
+			super.render(dataProvider, writer);
+		}
+	}
+
+	/**
+	 * Condition that decides whether a {@link ConditionalPart} is rendered at
+	 * render time.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public interface Condition {
+
+		/**
+		 * Returns whether the condition is fulfilled.
+		 *
+		 * @param dataProvider
+		 *            The data provider
+		 * @return {@code true} if the condition is fulfilled, {@code false}
+		 *         otherwise
+		 * @throws TemplateException
+		 *             if a template variable can not be parsed or evaluated
+		 */
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException;
+
+	}
+
+	/**
+	 * {@link Condition} implements that inverts another condition.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class NotCondition implements Condition {
+
+		/** The condition to invert. */
+		private final Condition condition;
+
+		/**
+		 * Creates a inverting condition.
+		 *
+		 * @param condition
+		 *            The condition to invert
+		 */
+		public NotCondition(Condition condition) {
+			this.condition = condition;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			return !condition.isAllowed(dataProvider);
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that only returns true if all its
+	 * conditions are true also.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class AndCondition implements Condition {
+
+		/** The conditions. */
+		private final Collection<Condition> conditions;
+
+		/**
+		 * Creates a new AND condition.
+		 *
+		 * @param conditions
+		 *            The conditions
+		 */
+		public AndCondition(Condition... conditions) {
+			this(Arrays.asList(conditions));
+		}
+
+		/**
+		 * Creates a new AND condition.
+		 *
+		 * @param conditions
+		 *            The conditions
+		 */
+		public AndCondition(Collection<Condition> conditions) {
+			this.conditions = conditions;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			for (Condition condition : conditions) {
+				if (!condition.isAllowed(dataProvider)) {
+					return false;
+				}
+			}
+			return true;
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that only returns false if all its
+	 * conditions are false also.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class OrCondition implements Condition {
+
+		/** The conditions. */
+		private final Collection<Condition> conditions;
+
+		/**
+		 * Creates a new OR condition.
+		 *
+		 * @param conditions
+		 *            The conditions
+		 */
+		public OrCondition(Condition... conditions) {
+			this(Arrays.asList(conditions));
+		}
+
+		/**
+		 * Creates a new OR condition.
+		 *
+		 * @param conditions
+		 *            The conditions
+		 */
+		public OrCondition(Collection<Condition> conditions) {
+			this.conditions = conditions;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			for (Condition condition : conditions) {
+				if (condition.isAllowed(dataProvider)) {
+					return true;
+				}
+			}
+			return false;
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that asks the {@link DataProvider} for a
+	 * {@link Boolean} value.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class DataCondition implements Condition {
+
+		/** Whether to invert the result. */
+		private final boolean invert;
+
+		/** The name of the data item to check. */
+		private final String itemName;
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param itemName
+		 *            The name of the item to check
+		 */
+		public DataCondition(String itemName) {
+			this(itemName, false);
+		}
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param itemName
+		 *            The name of the item to check
+		 * @param invert
+		 *            {@code true} to invert the result, {@code false} otherwise
+		 */
+		public DataCondition(String itemName, boolean invert) {
+			this.invert = invert;
+			this.itemName = itemName;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			return Boolean.valueOf(String.valueOf(dataProvider.getData(itemName))) ^ invert;
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that checks a given text for a
+	 * {@link Boolean} value.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class DataTextCondition implements Condition {
+
+		/** Whether to invert the result. */
+		private final boolean invert;
+
+		/** The text to check. */
+		private final String text;
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param text
+		 *            The text to check
+		 */
+		public DataTextCondition(String text) {
+			this(text, false);
+		}
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param text
+		 *            The text to check
+		 * @param invert
+		 *            {@code true} to invert the result, {@code false} otherwise
+		 */
+		public DataTextCondition(String text, boolean invert) {
+			this.invert = invert;
+			this.text = text;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			return Boolean.valueOf(text) ^ invert;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public String toString() {
+			return "(" + text + " = " + !invert + ")";
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that asks the {@link DataProvider} for a
+	 * {@link Boolean} value and checks whether it’s {@code null} or not.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class NullDataCondition implements Condition {
+
+		/** Whether to invert the result. */
+		private final boolean invert;
+
+		/** The name of the data item to check. */
+		private final String itemName;
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param itemName
+		 *            The name of the item to check
+		 */
+		public NullDataCondition(String itemName) {
+			this(itemName, false);
+		}
+
+		/**
+		 * Creates a new data condition.
+		 *
+		 * @param itemName
+		 *            The name of the item to check
+		 * @param invert
+		 *            {@code true} to invert the result, {@code false} otherwise
+		 */
+		public NullDataCondition(String itemName, boolean invert) {
+			this.invert = invert;
+			this.itemName = itemName;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			return (dataProvider.getData(itemName) == null) ^ invert;
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that filters the value from the data
+	 * provider before checking whether it matches “true.”
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public static class FilterCondition implements Condition {
+
+		/** The name of the item. */
+		private final String itemName;
+
+		/** The filters. */
+		private final Collection<Filter> filters;
+
+		/** The filter parameters. */
+		private final Map<Filter, Map<String, String>> filterParameters;
+
+		/** Whether to invert the result. */
+		private final boolean invert;
+
+		/**
+		 * Creates a new filter condition.
+		 *
+		 * @param itemName
+		 *            The name of the item
+		 * @param filters
+		 *            The filters to run through
+		 * @param filterParameters
+		 *            The filter parameters
+		 */
+		public FilterCondition(String itemName, Collection<Filter> filters, Map<Filter, Map<String, String>> filterParameters) {
+			this(itemName, filters, filterParameters, false);
+		}
+
+		/**
+		 * Creates a new filter condition.
+		 *
+		 * @param itemName
+		 *            The name of the item
+		 * @param filters
+		 *            The filters to run through
+		 * @param filterParameters
+		 *            The filter parameters
+		 * @param invert
+		 *            {@code true} to invert the result
+		 */
+		public FilterCondition(String itemName, Collection<Filter> filters, Map<Filter, Map<String, String>> filterParameters, boolean invert) {
+			this.itemName = itemName;
+			this.filters = filters;
+			this.filterParameters = filterParameters;
+			this.invert = invert;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			Object data = dataProvider.getData(itemName);
+			for (Filter filter : filters) {
+				data = filter.format(dataProvider, data, filterParameters.get(filter));
+			}
+			return Boolean.valueOf(String.valueOf(data)) ^ invert;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public String toString() {
+			StringBuilder stringBuilder = new StringBuilder();
+			stringBuilder.append("([").append(itemName).append("]");
+			for (Filter filter : filters) {
+				stringBuilder.append("|").append(filter.getClass().getSimpleName());
+				if (filterParameters.containsKey(filter)) {
+					for (Entry<String, String> filterParameter : filterParameters.get(filter).entrySet()) {
+						stringBuilder.append(" ").append(filterParameter.getKey()).append("=").append(filterParameter.getValue());
+					}
+				}
+			}
+			return stringBuilder.append(" = ").append(!invert).append(")").toString();
+		}
+
+	}
+
+	/**
+	 * {@link Condition} implementation that filters a given text before
+	 * checking whether it matches “true.”
+	 *
+	 * @author <a href="mailto:david.roden@sysart.de">David Roden</a>
+	 */
+	public static class FilterTextCondition implements Condition {
+
+		/** The text to filter. */
+		private final String text;
+
+		/** The filters. */
+		private final Collection<Filter> filters;
+
+		/** The filter parameters. */
+		private final Map<Filter, Map<String, String>> filterParameters;
+
+		/** Whether to invert the result. */
+		private final boolean invert;
+
+		/**
+		 * Creates a new filter text condition.
+		 *
+		 * @param text
+		 *            The text to filter
+		 * @param filters
+		 *            The filters to run through
+		 * @param filterParameters
+		 *            The filter parameters
+		 */
+		public FilterTextCondition(String text, Collection<Filter> filters, Map<Filter, Map<String, String>> filterParameters) {
+			this(text, filters, filterParameters, false);
+		}
+
+		/**
+		 * Creates a new filter text condition.
+		 *
+		 * @param text
+		 *            The text to filter
+		 * @param filters
+		 *            The filters to run through
+		 * @param filterParameters
+		 *            The filter parameters
+		 * @param invert
+		 *            {@code true} to invert the result
+		 */
+		public FilterTextCondition(String text, Collection<Filter> filters, Map<Filter, Map<String, String>> filterParameters, boolean invert) {
+			this.text = text;
+			this.filters = filters;
+			this.filterParameters = filterParameters;
+			this.invert = invert;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public boolean isAllowed(DataProvider dataProvider) throws TemplateException {
+			Object data = text;
+			for (Filter filter : filters) {
+				data = filter.format(dataProvider, data, filterParameters.get(filter));
+			}
+			return Boolean.valueOf(String.valueOf(data)) ^ invert;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public String toString() {
+			StringBuilder stringBuilder = new StringBuilder();
+			stringBuilder.append("(").append(text);
+			for (Filter filter : filters) {
+				stringBuilder.append("|").append(filter.getClass().getSimpleName());
+				if (filterParameters.containsKey(filter)) {
+					for (Entry<String, String> filterParameter : filterParameters.get(filter).entrySet()) {
+						stringBuilder.append(" ").append(filterParameter.getKey()).append("=").append(filterParameter.getValue());
+					}
+				}
+			}
+			return stringBuilder.append(" = ").append(!invert).append(")").toString();
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/ContainerPart.java b/alien/src/net/pterodactylus/util/template/ContainerPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/ContainerPart.java
@@ -0,0 +1,80 @@
+/*
+ * utils - ContainerPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A {@link Part} that can contain multiple other {@code Part}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class ContainerPart extends Part implements Iterable<Part> {
+
+	/** The parts this part contains. */
+	protected final List<Part> parts = new ArrayList<Part>();
+
+	/**
+	 * Creates a new container part that contains the given parts
+	 */
+	public ContainerPart() {
+		/* do nothing. */
+	}
+
+	/**
+	 * /** Creates a new container part that contains the given parts
+	 *
+	 * @param parts
+	 *            The parts the container part contains
+	 */
+	public ContainerPart(List<Part> parts) {
+		this.parts.addAll(parts);
+	}
+
+	/**
+	 * Adds the given part.
+	 *
+	 * @param part
+	 *            The part to add
+	 */
+	public void add(Part part) {
+		parts.add(part);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		for (Part part : parts) {
+			part.render(dataProvider, writer);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Iterator<Part> iterator() {
+		return parts.iterator();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DataProvider.java b/alien/src/net/pterodactylus/util/template/DataProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DataProvider.java
@@ -0,0 +1,158 @@
+/*
+ * utils - DataProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * Interface for objects that need to supply data to a {@link Template}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DataProvider {
+
+	/** Object store. */
+	private final DataStore dataStore;
+
+	/** Accessors. */
+	private final Map<Class<?>, Accessor> classAccessors = new HashMap<Class<?>, Accessor>();
+
+	/**
+	 * Creates a new data provider.
+	 */
+	public DataProvider() {
+		this(new DataStore.MapDataStore());
+	}
+
+	/**
+	 * Creates a new data provider using the given data store as backend.
+	 *
+	 * @param dataStore
+	 *            The data store
+	 */
+	public DataProvider(DataStore dataStore) {
+		this.dataStore = dataStore;
+		classAccessors.put(Map.class, Accessor.MAP_ACCESSOR);
+	}
+
+	/**
+	 * Returns the data provider’s data store.
+	 *
+	 * @return The data store
+	 */
+	protected DataStore getDataStore() {
+		return dataStore;
+	}
+
+	/**
+	 * Adds an accessor for objects of the given class.
+	 *
+	 * @param clazz
+	 *            The class of the objects to handle with the accessor
+	 * @param accessor
+	 *            The accessor to handle the objects with
+	 */
+	public void addAccessor(Class<?> clazz, Accessor accessor) {
+		classAccessors.put(clazz, accessor);
+	}
+
+	/**
+	 * Finds an accessor that can handle the given class. If
+	 * {@link #classAccessors} does not contain a perfect match, a match to a
+	 * superclass or superinterface is searched.
+	 *
+	 * @param clazz
+	 *            The class to get an accessor for
+	 * @return The accessor for the given class, or {@code null} if no accessor
+	 *         could be found
+	 */
+	protected Accessor findAccessor(Class<?> clazz) {
+		if (classAccessors.containsKey(clazz)) {
+			return classAccessors.get(clazz);
+		}
+		for (Class<?> interfaceClass : clazz.getInterfaces()) {
+			if (classAccessors.containsKey(interfaceClass)) {
+				return classAccessors.get(interfaceClass);
+			}
+		}
+		Class<?> classToCheck = clazz.getSuperclass();
+		while (classToCheck != null) {
+			if (classAccessors.containsKey(classToCheck)) {
+				return classAccessors.get(classToCheck);
+			}
+			for (Class<?> interfaceClass : classToCheck.getInterfaces()) {
+				if (classAccessors.containsKey(interfaceClass)) {
+					return classAccessors.get(interfaceClass);
+				}
+			}
+			classToCheck = classToCheck.getSuperclass();
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the object stored under the given name. The name can contain
+	 * hierarchical structures separated by a dot (“.”), such as “loop.count” in
+	 * which case a {@link Map} must be stored under “loop”.
+	 *
+	 * @param name
+	 *            The name of the object to get
+	 * @return The object
+	 * @throws TemplateException
+	 *             if the name or some objects can not be parsed or evaluated
+	 */
+	public Object getData(String name) throws TemplateException {
+		if (name.indexOf('.') == -1) {
+			return getDataStore().get(name);
+		}
+		StringTokenizer nameTokens = new StringTokenizer(name, ".");
+		Object object = null;
+		while (nameTokens.hasMoreTokens()) {
+			String nameToken = nameTokens.nextToken();
+			if (object == null) {
+				object = getDataStore().get(nameToken);
+			} else {
+				Accessor accessor = findAccessor(object.getClass());
+				if (accessor != null) {
+					object = accessor.get(this, object, nameToken);
+				} else {
+					throw new TemplateException("no accessor found for " + object.getClass());
+				}
+			}
+			if (object == null) {
+				return null;
+			}
+		}
+		return object;
+	}
+
+	/**
+	 * Sets data in this data provider.
+	 *
+	 * @param name
+	 *            The key under which to store the data
+	 * @param data
+	 *            The data to store
+	 */
+	public void setData(String name, Object data) {
+		getDataStore().set(name, data);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DataProviderPart.java b/alien/src/net/pterodactylus/util/template/DataProviderPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DataProviderPart.java
@@ -0,0 +1,63 @@
+/*
+ * utils - DataProviderPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import net.pterodactylus.util.io.Renderable;
+
+/**
+ * A {@link Part} whose content is dynamically fetched from a
+ * {@link DataProvider}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class DataProviderPart extends Part {
+
+	/** The name of the object to get. */
+	private final String name;
+
+	/**
+	 * Creates a new data provider part.
+	 *
+	 * @param name
+	 *            The name of the object
+	 */
+	public DataProviderPart(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		Object output = dataProvider.getData(name);
+		try {
+			if (output instanceof Renderable) {
+				((Renderable) output).render(writer);
+			} else {
+				writer.write((output != null) ? String.valueOf(output) : "");
+			}
+		} catch (IOException ioe1) {
+			throw new TemplateException("Can not render part.", ioe1);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DataStore.java b/alien/src/net/pterodactylus/util/template/DataStore.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DataStore.java
@@ -0,0 +1,77 @@
+/*
+ * utils - DataStore.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Interface for {@link DataProvider} backends.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface DataStore {
+
+	/**
+	 * Returns the object with the given key.
+	 *
+	 * @param name
+	 *            The key of the data
+	 * @return The data, or {@code null} if there is no data under the given key
+	 */
+	public Object get(String name);
+
+	/**
+	 * Stores an object under the given key.
+	 *
+	 * @param name
+	 *            The key under which to store the object
+	 * @param data
+	 *            The object to store
+	 */
+	public void set(String name, Object data);
+
+	/**
+	 * Default {@link Map}-based implementation of a {@link DataStore}.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	public class MapDataStore implements DataStore {
+
+		/** The backing store. */
+		private final Map<String, Object> objectStore = new HashMap<String, Object>();
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object get(String name) {
+			return objectStore.get(name);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(String name, Object data) {
+			objectStore.put(name, data);
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DataTemplateProvider.java b/alien/src/net/pterodactylus/util/template/DataTemplateProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DataTemplateProvider.java
@@ -0,0 +1,50 @@
+/*
+ * utils - DataTemplateProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+/**
+ * {@link TemplateProvider} implementation that retrieves a {@link Template}
+ * from a {@link DataProvider}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DataTemplateProvider implements TemplateProvider {
+
+	/** The data provider. */
+	private final DataProvider dataProvider;
+
+	/**
+	 * Creates a new {@link DataProvider}-based {@link TemplateProvider}.
+	 *
+	 * @param dataProvider
+	 *            The underlying data provider
+	 */
+	public DataTemplateProvider(DataProvider dataProvider) {
+		this.dataProvider = dataProvider;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Template getTemplate(String templateName) {
+		Object templateObject = dataProvider.getData(templateName);
+		return (templateObject instanceof Template) ? (Template) templateObject : null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DateFilter.java b/alien/src/net/pterodactylus/util/template/DateFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DateFilter.java
@@ -0,0 +1,77 @@
+/*
+ * utils - DateFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link Filter} implementation that formats a date. The date may be given
+ * either as a {@link Date} or a {@link Long} object.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DateFilter implements Filter {
+
+	/** The date format cache. */
+	private static final Map<String, DateFormat> dateFormats = new HashMap<String, DateFormat>();
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		String format = parameters.get("format");
+		DateFormat dateFormat = getDateFormat(format);
+		if (data instanceof Date) {
+			return dateFormat.format((Date) data);
+		} else if (data instanceof Long) {
+			return dateFormat.format(new Date((Long) data));
+		}
+		return "";
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Returns a {@link DateFormat} for the given format. If the format is
+	 * {@code null} or an empty {@link String}, a default {@link DateFormat}
+	 * instance is returned.
+	 *
+	 * @param format
+	 *            The format of the formatter
+	 * @return A suitable date format
+	 */
+	private DateFormat getDateFormat(String format) {
+		if ((format == null) || (format.trim().length() == 0)) {
+			return DateFormat.getInstance();
+		}
+		DateFormat dateFormat = dateFormats.get(format);
+		if (dateFormat == null) {
+			dateFormat = new SimpleDateFormat(format);
+			dateFormats.put(format, dateFormat);
+		}
+		return dateFormat;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DefaultFilter.java b/alien/src/net/pterodactylus/util/template/DefaultFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DefaultFilter.java
@@ -0,0 +1,42 @@
+/*
+ * utils - DefaultFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * {@link Filter} implementation that can return fixed values when the input
+ * value is {@code null}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultFilter implements Filter {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		String defaultValue = parameters.get("value");
+		if (data == null) {
+			return defaultValue;
+		}
+		return data;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/DefaultTemplateFactory.java b/alien/src/net/pterodactylus/util/template/DefaultTemplateFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/DefaultTemplateFactory.java
@@ -0,0 +1,224 @@
+/*
+ * utils - DefaultTemplateFactory.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Default {@link TemplateFactory} implementation that creates {@link Template}s
+ * with {@link HtmlFilter}s and {@link ReplaceFilter}s added.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultTemplateFactory implements TemplateFactory {
+
+	/** The default instance. */
+	private static DefaultTemplateFactory instance;
+
+	/** Filters that will be added to all created templates. */
+	private final Map<String, Filter> filters = new HashMap<String, Filter>();
+
+	/** Plugins that will be added to all created templates. */
+	private final Map<String, Plugin> plugins = new HashMap<String, Plugin>();
+
+	/** Accessors that will be added to all created templates. */
+	private final Map<Class<?>, Accessor> accessors = new HashMap<Class<?>, Accessor>();
+
+	/** The template provider for all created templates. */
+	private TemplateProvider templateProvider;
+
+	/** Additional objects to set in all templates. */
+	private final Map<String, Object> templateObjects = new HashMap<String, Object>();
+
+	/**
+	 * Creates a new default template factory that adds both an
+	 * {@link HtmlFilter} and a {@link ReplaceFilter} to created templates.
+	 */
+	public DefaultTemplateFactory() {
+		this(true, true);
+	}
+
+	/**
+	 * Creates a new default template factory.
+	 *
+	 * @param addHtmlFilter
+	 *            {@code true} to add an {@link HtmlFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addReplaceFilter
+	 *            {@code true} to add a {@link ReplaceFilter} to created
+	 *            templates, {@code false} otherwise
+	 */
+	public DefaultTemplateFactory(boolean addHtmlFilter, boolean addReplaceFilter) {
+		this(addHtmlFilter, addReplaceFilter, true, true);
+	}
+
+	/**
+	 * Creates a new default template factory.
+	 *
+	 * @param addHtmlFilter
+	 *            {@code true} to add an {@link HtmlFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addReplaceFilter
+	 *            {@code true} to add a {@link ReplaceFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addStoreFilter
+	 *            {@code true} to add a {@link StoreFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addInsertFilter
+	 *            {@code true} to add a {@link InsertFilter} to created
+	 *            templates, {@code false} otherwise
+	 */
+	public DefaultTemplateFactory(boolean addHtmlFilter, boolean addReplaceFilter, boolean addStoreFilter, boolean addInsertFilter) {
+		this(addHtmlFilter, addReplaceFilter, addStoreFilter, addInsertFilter, true);
+	}
+
+	/**
+	 * Creates a new default template factory.
+	 *
+	 * @param addHtmlFilter
+	 *            {@code true} to add an {@link HtmlFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addReplaceFilter
+	 *            {@code true} to add a {@link ReplaceFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addStoreFilter
+	 *            {@code true} to add a {@link StoreFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addInsertFilter
+	 *            {@code true} to add a {@link InsertFilter} to created
+	 *            templates, {@code false} otherwise
+	 * @param addDefaultFilter
+	 *            {@code true} to add a {@link DefaultFilter} to created
+	 *            templates, {@code false} otherwise
+	 */
+	public DefaultTemplateFactory(boolean addHtmlFilter, boolean addReplaceFilter, boolean addStoreFilter, boolean addInsertFilter, boolean addDefaultFilter) {
+		if (addHtmlFilter) {
+			filters.put("html", new HtmlFilter());
+		}
+		if (addReplaceFilter) {
+			filters.put("replace", new ReplaceFilter());
+		}
+		if (addStoreFilter) {
+			filters.put("store", new StoreFilter());
+		}
+		if (addInsertFilter) {
+			filters.put("insert", new InsertFilter());
+		}
+		if (addDefaultFilter) {
+			filters.put("default", new DefaultFilter());
+		}
+	}
+
+	/**
+	 * Adds an accessor that will be added to all created templates.
+	 *
+	 * @param clazz
+	 *            The class to add the accessor for
+	 * @param accessor
+	 *            The accessor to add
+	 */
+	public void addAccessor(Class<?> clazz, Accessor accessor) {
+		accessors.put(clazz, accessor);
+	}
+
+	/**
+	 * Adds the given filter to all created templates.
+	 *
+	 * @param name
+	 *            The name of the filter
+	 * @param filter
+	 *            The filter to add
+	 */
+	public void addFilter(String name, Filter filter) {
+		filters.put(name, filter);
+	}
+
+	/**
+	 * Adds the given plugin to all created templates.
+	 *
+	 * @param name
+	 *            The name of the plugin
+	 * @param plugin
+	 *            The plugin to add
+	 */
+	public void addPlugin(String name, Plugin plugin) {
+		plugins.put(name, plugin);
+	}
+
+	/**
+	 * Sets the template provider that is set on all created templates.
+	 *
+	 * @param templateProvider
+	 *            The template provider to set
+	 */
+	public void setTemplateProvider(TemplateProvider templateProvider) {
+		this.templateProvider = templateProvider;
+	}
+
+	/**
+	 * Adds an object that will be stored in all created templates.
+	 *
+	 * @param name
+	 *            The name of the template variable
+	 * @param object
+	 *            The object to store
+	 */
+	public void addTemplateObject(String name, Object object) {
+		templateObjects.put(name, object);
+	}
+
+	/**
+	 * Returns the static default instance of this template factory.
+	 *
+	 * @return The default template factory
+	 */
+	public synchronized static TemplateFactory getInstance() {
+		if (instance == null) {
+			instance = new DefaultTemplateFactory();
+		}
+		return instance;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Template createTemplate(Reader templateSource) {
+		Template template = new Template(templateSource);
+		for (Entry<String, Filter> filterEntry : filters.entrySet()) {
+			template.addFilter(filterEntry.getKey(), filterEntry.getValue());
+		}
+		for (Entry<String, Plugin> pluginEntry : plugins.entrySet()) {
+			template.addPlugin(pluginEntry.getKey(), pluginEntry.getValue());
+		}
+		for (Entry<Class<?>, Accessor> accessorEntry : accessors.entrySet()) {
+			template.addAccessor(accessorEntry.getKey(), accessorEntry.getValue());
+		}
+		if (templateProvider != null) {
+			template.setTemplateProvider(templateProvider);
+		}
+		for (Entry<String, Object> objectEntry : templateObjects.entrySet()) {
+			template.set(objectEntry.getKey(), objectEntry.getValue());
+		}
+		return template;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/EmptyLoopPart.java b/alien/src/net/pterodactylus/util/template/EmptyLoopPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/EmptyLoopPart.java
@@ -0,0 +1,58 @@
+/*
+ * utils - EmptyLoopPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+import java.util.Collection;
+
+/**
+ * {@ContainerPart} implementation that only renders its childrens if a
+ * {@link Collection} in the template is empty. In combination with
+ * {@link LoopPart} this can be used to implements {@code foreach}/{@code
+ * foreachelse} loops.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class EmptyLoopPart extends ContainerPart {
+
+	/** The name of the collection. */
+	private final String collectionName;
+
+	/**
+	 * Creates a new empty loop part.
+	 *
+	 * @param collectionName
+	 *            The name of the collection
+	 */
+	public EmptyLoopPart(String collectionName) {
+		this.collectionName = collectionName;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		Collection<?> collection = (Collection<?>) dataProvider.getData(collectionName);
+		if ((collection != null) && !collection.isEmpty()) {
+			return;
+		}
+		super.render(dataProvider, writer);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/Filter.java b/alien/src/net/pterodactylus/util/template/Filter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/Filter.java
@@ -0,0 +1,43 @@
+/*
+ * utils - Filter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * Filters can be used to transform the contents of a variable into some other
+ * representation.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Filter {
+
+	/**
+	 * Formats the given data object.
+	 *
+	 * @param dataProvider
+	 *            The current data provider
+	 * @param data
+	 *            The data to format
+	 * @param parameters
+	 *            Parameters for the filter
+	 * @return The formatted data
+	 */
+	public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters);
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/FilteredPart.java b/alien/src/net/pterodactylus/util/template/FilteredPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/FilteredPart.java
@@ -0,0 +1,81 @@
+/*
+ * utils - FilteredPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+import net.pterodactylus.util.io.Renderable;
+
+/**
+ * {@link Part} implementation that runs the output of another part through one
+ * or more {@link Filter}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class FilteredPart extends Part {
+
+	/** The name of the data object to filter. */
+	private final String name;
+
+	/** The filters to apply. */
+	private final Collection<Filter> filters;
+
+	/** Parameters for all filters. */
+	private final Map<Filter, Map<String, String>> allFilterParameters;
+
+	/**
+	 * Creates a new filtered part.
+	 *
+	 * @param name
+	 *            The name of the data object
+	 * @param filters
+	 *            The filters to apply
+	 * @param allFilterParameters
+	 *            All filters’ parameters
+	 */
+	public FilteredPart(String name, Collection<Filter> filters, Map<Filter, Map<String, String>> allFilterParameters) {
+		this.name = name;
+		this.filters = filters;
+		this.allFilterParameters = allFilterParameters;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		Object data = dataProvider.getData(name);
+		Object output = data;
+		for (Filter filter : filters) {
+			data = output = filter.format(dataProvider, data, allFilterParameters.get(filter));
+		}
+		try {
+			if (output instanceof Renderable) {
+				((Renderable) output).render(writer);
+			} else {
+				writer.write((output != null) ? String.valueOf(output) : "");
+			}
+		} catch (IOException ioe1) {
+			throw new TemplateException("Can not render part.", ioe1);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/FilteredTextPart.java b/alien/src/net/pterodactylus/util/template/FilteredTextPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/FilteredTextPart.java
@@ -0,0 +1,80 @@
+/*
+ * utils - FilteredTextPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+import net.pterodactylus.util.io.Renderable;
+
+/**
+ * {@link Part} implementation that runs a predefined text through one or more
+ * {@link Filter}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class FilteredTextPart extends Part {
+
+	/** The text to filter. */
+	private final String text;
+
+	/** The filters to apply. */
+	private final Collection<Filter> filters;
+
+	/** Parameters for all filters. */
+	private final Map<Filter, Map<String, String>> allFilterParameters;
+
+	/**
+	 * Creates a new filtered part.
+	 *
+	 * @param text
+	 *            The text to filter
+	 * @param filters
+	 *            The filters to apply
+	 * @param allFilterParameters
+	 *            Parameters for all filters
+	 */
+	public FilteredTextPart(String text, Collection<Filter> filters, Map<Filter, Map<String, String>> allFilterParameters) {
+		this.text = text;
+		this.filters = filters;
+		this.allFilterParameters = allFilterParameters;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		Object output = text;
+		for (Filter filter : filters) {
+			output = filter.format(dataProvider, output, allFilterParameters.get(filter));
+		}
+		try {
+			if (output instanceof Renderable) {
+				((Renderable) output).render(writer);
+			} else {
+				writer.write((output != null) ? String.valueOf(output) : "");
+			}
+		} catch (IOException ioe1) {
+			throw new TemplateException("Can not render part.", ioe1);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/HtmlFilter.java b/alien/src/net/pterodactylus/util/template/HtmlFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/HtmlFilter.java
@@ -0,0 +1,309 @@
+/*
+ * utils - HtmlFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filters HTML by replacing all characters that match a defined HTML entity by
+ * that entity. Unknown characters that are outside of the US-ASCII range (0 to
+ * 127) are encoded using the {@code &#1234;} syntax.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HtmlFilter implements Filter {
+
+	/** Map of defined HTML entities. */
+	private static final Map<Character, String> htmlEntities = new HashMap<Character, String>();
+
+	static {
+		htmlEntities.put('Â', "Acirc");
+		htmlEntities.put('â', "acirc");
+		htmlEntities.put('´', "acute");
+		htmlEntities.put('Æ', "AElig");
+		htmlEntities.put('æ', "aelig");
+		htmlEntities.put('À', "Agrave");
+		htmlEntities.put('à', "agrave");
+		htmlEntities.put('ℵ', "alefsym");
+		htmlEntities.put('Α', "alpha");
+		htmlEntities.put('α', "alpha");
+		htmlEntities.put('&', "amp");
+		htmlEntities.put('∧', "and");
+		htmlEntities.put('∠', "ang");
+		htmlEntities.put('\'', "apos");
+		htmlEntities.put('Å', "Aring");
+		htmlEntities.put('å', "aring");
+		htmlEntities.put('≈', "asymp");
+		htmlEntities.put('Ã', "Atilde");
+		htmlEntities.put('ã', "atilde");
+		htmlEntities.put('Ä', "Auml");
+		htmlEntities.put('ä', "auml");
+		htmlEntities.put('„', "bdquo");
+		htmlEntities.put('Β', "Beta");
+		htmlEntities.put('β', "beta");
+		htmlEntities.put('¦', "brvbar");
+		htmlEntities.put('•', "bull");
+		htmlEntities.put('∩', "cap");
+		htmlEntities.put('Ç', "Ccedil");
+		htmlEntities.put('ç', "ccedil");
+		htmlEntities.put('¸', "cedil");
+		htmlEntities.put('¢', "cent");
+		htmlEntities.put('Χ', "Chi");
+		htmlEntities.put('χ', "chi");
+		htmlEntities.put('ˆ', "circ");
+		htmlEntities.put('♣', "clubs");
+		htmlEntities.put('≅', "cong");
+		htmlEntities.put('©', "copy");
+		htmlEntities.put('↵', "crarr");
+		htmlEntities.put('∪', "cup");
+		htmlEntities.put('¤', "curren");
+		htmlEntities.put('‡', "Dagger");
+		htmlEntities.put('†', "dagger");
+		htmlEntities.put('⇓', "dArr");
+		htmlEntities.put('↓', "darr");
+		htmlEntities.put('°', "deg");
+		htmlEntities.put('Δ', "Delta");
+		htmlEntities.put('δ', "delta");
+		htmlEntities.put('♦', "diams");
+		htmlEntities.put('÷', "divide");
+		htmlEntities.put('É', "Eacute");
+		htmlEntities.put('é', "eacute");
+		htmlEntities.put('Ê', "Ecirc");
+		htmlEntities.put('ê', "ecirc");
+		htmlEntities.put('È', "Egrave");
+		htmlEntities.put('è', "egrave");
+		htmlEntities.put('∅', "empty");
+		htmlEntities.put('\u2003', "emsp");
+		htmlEntities.put('\u2002', "ensp");
+		htmlEntities.put('Ε', "Epsilon");
+		htmlEntities.put('ε', "epsilon");
+		htmlEntities.put('≡', "equiv");
+		htmlEntities.put('Η', "Eta");
+		htmlEntities.put('η', "eta");
+		htmlEntities.put('Ð', "ETH");
+		htmlEntities.put('ð', "eth");
+		htmlEntities.put('Ë', "Euml");
+		htmlEntities.put('ë', "euml");
+		htmlEntities.put('€', "euro");
+		htmlEntities.put('∃', "exist");
+		htmlEntities.put('ƒ', "fnof");
+		htmlEntities.put('∀', "forall");
+		htmlEntities.put('½', "frac12");
+		htmlEntities.put('¼', "frac14");
+		htmlEntities.put('¾', "frac34");
+		htmlEntities.put('⁄', "frasl");
+		htmlEntities.put('Γ', "Gamma");
+		htmlEntities.put('γ', "gamma");
+		htmlEntities.put('≥', "ge");
+		htmlEntities.put('>', "gt");
+		htmlEntities.put('⇔', "hArr");
+		htmlEntities.put('↔', "harr");
+		htmlEntities.put('♥', "hearts");
+		htmlEntities.put('…', "hellip");
+		htmlEntities.put('Í', "Iacute");
+		htmlEntities.put('í', "iacute");
+		htmlEntities.put('Î', "Icirc");
+		htmlEntities.put('î', "icirc");
+		htmlEntities.put('¡', "iexcl");
+		htmlEntities.put('Ì', "Igrave");
+		htmlEntities.put('ì', "igrave");
+		htmlEntities.put('ℑ', "image");
+		htmlEntities.put('∞', "infin");
+		htmlEntities.put('∫', "int");
+		htmlEntities.put('Ι', "Iota");
+		htmlEntities.put('ι', "iota");
+		htmlEntities.put('¿', "iquest");
+		htmlEntities.put('∈', "isin");
+		htmlEntities.put('Ï', "Iuml");
+		htmlEntities.put('ï', "iuml");
+		htmlEntities.put('Κ', "Kappa");
+		htmlEntities.put('κ', "kappa");
+		htmlEntities.put('Λ', "Lambda");
+		htmlEntities.put('λ', "lambda");
+		htmlEntities.put('〈', "lang");
+		htmlEntities.put('«', "laquo");
+		htmlEntities.put('⇐', "lArr");
+		htmlEntities.put('←', "larr");
+		htmlEntities.put('⌈', "lceil");
+		htmlEntities.put('“', "ldquo");
+		htmlEntities.put('≤', "le");
+		htmlEntities.put('⌊', "lfloor");
+		htmlEntities.put('∗', "lowast");
+		htmlEntities.put('◊', "loz");
+		htmlEntities.put('\u200e', "lrm");
+		htmlEntities.put('‹', "lsaquo");
+		htmlEntities.put('‘', "lsquo");
+		htmlEntities.put('<', "lt");
+		htmlEntities.put('¯', "macr");
+		htmlEntities.put('—', "mdash");
+		htmlEntities.put('µ', "micro");
+		htmlEntities.put('·', "middot");
+		htmlEntities.put('−', "minus");
+		htmlEntities.put('Μ', "Mu");
+		htmlEntities.put('μ', "mu");
+		htmlEntities.put('∇', "nabla");
+		htmlEntities.put('\u00a0', "nbsp");
+		htmlEntities.put('–', "ndash");
+		htmlEntities.put('≠', "ne");
+		htmlEntities.put('∋', "ni");
+		htmlEntities.put('¬', "not");
+		htmlEntities.put('∉', "notin");
+		htmlEntities.put('⊄', "nsub");
+		htmlEntities.put('Ñ', "Ntilde");
+		htmlEntities.put('ñ', "ntilde");
+		htmlEntities.put('Ν', "Nu");
+		htmlEntities.put('ν', "nu");
+		htmlEntities.put('Ó', "Oacute");
+		htmlEntities.put('ó', "oacute");
+		htmlEntities.put('Ô', "Ocirc");
+		htmlEntities.put('ô', "ocirc");
+		htmlEntities.put('Œ', "OElig");
+		htmlEntities.put('œ', "oelig");
+		htmlEntities.put('Ò', "Ograve");
+		htmlEntities.put('ò', "ograve");
+		htmlEntities.put('‾', "oline");
+		htmlEntities.put('Ω', "Omega");
+		htmlEntities.put('ω', "omega");
+		htmlEntities.put('Ο', "Omicron");
+		htmlEntities.put('ο', "omicron");
+		htmlEntities.put('⊕', "oplus");
+		htmlEntities.put('∨', "or");
+		htmlEntities.put('ª', "ordf");
+		htmlEntities.put('º', "ordm");
+		htmlEntities.put('Ø', "Oslash");
+		htmlEntities.put('ø', "oslash");
+		htmlEntities.put('Õ', "Otilde");
+		htmlEntities.put('õ', "otilde");
+		htmlEntities.put('⊗', "otimes");
+		htmlEntities.put('Ö', "Ouml");
+		htmlEntities.put('ö', "ouml");
+		htmlEntities.put('¶', "para");
+		htmlEntities.put('∂', "part");
+		htmlEntities.put('‰', "permil");
+		htmlEntities.put('⊥', "perp");
+		htmlEntities.put('Φ', "Phi");
+		htmlEntities.put('φ', "phi");
+		htmlEntities.put('Π', "pi");
+		htmlEntities.put('π', "pi");
+		htmlEntities.put('ϖ', "piv");
+		htmlEntities.put('±', "plusmn");
+		htmlEntities.put('£', "pound");
+		htmlEntities.put('″', "Prime");
+		htmlEntities.put('′', "prime");
+		htmlEntities.put('∏', "prod");
+		htmlEntities.put('∝', "prop");
+		htmlEntities.put('Ψ', "Psi");
+		htmlEntities.put('ψ', "psi");
+		htmlEntities.put('"', "quot");
+		htmlEntities.put('√', "radic");
+		htmlEntities.put('〉', "rang");
+		htmlEntities.put('»', "raquo");
+		htmlEntities.put('⇒', "rArr");
+		htmlEntities.put('→', "rarr");
+		htmlEntities.put('⌉', "rceil");
+		htmlEntities.put('”', "rdquo");
+		htmlEntities.put('ℜ', "real");
+		htmlEntities.put('®', "reg");
+		htmlEntities.put('⌋', "rfloor");
+		htmlEntities.put('Ρ', "Rho");
+		htmlEntities.put('ρ', "rho");
+		htmlEntities.put('\u200f', "rlm");
+		htmlEntities.put('›', "rsaquo");
+		htmlEntities.put('’', "rsquo");
+		htmlEntities.put('‚', "sbquo");
+		htmlEntities.put('Š', "Scaron");
+		htmlEntities.put('š', "scaron");
+		htmlEntities.put('⋅', "sdot");
+		htmlEntities.put('§', "sect");
+		htmlEntities.put('\u00ad', "shy");
+		htmlEntities.put('Σ', "Sigma");
+		htmlEntities.put('σ', "sigma");
+		htmlEntities.put('ς', "sigmaf");
+		htmlEntities.put('∼', "sim");
+		htmlEntities.put('♠', "spades");
+		htmlEntities.put('⊂', "sub");
+		htmlEntities.put('⊆', "sube");
+		htmlEntities.put('∑', "sum");
+		htmlEntities.put('⊃', "sup");
+		htmlEntities.put('¹', "sup1");
+		htmlEntities.put('²', "sup2");
+		htmlEntities.put('³', "sup3");
+		htmlEntities.put('⊇', "supe");
+		htmlEntities.put('ß', "szlig");
+		htmlEntities.put('Τ', "Tau");
+		htmlEntities.put('τ', "tau");
+		htmlEntities.put('∴', "there4");
+		htmlEntities.put('Θ', "Theta");
+		htmlEntities.put('θ', "theta");
+		htmlEntities.put('ϑ', "thetasym");
+		htmlEntities.put('\u2009', "thinsp");
+		htmlEntities.put('Þ', "THORN");
+		htmlEntities.put('þ', "thorn");
+		htmlEntities.put('˜', "tilde");
+		htmlEntities.put('×', "times");
+		htmlEntities.put('™', "trade");
+		htmlEntities.put('Ú', "Uacute");
+		htmlEntities.put('ú', "uacute");
+		htmlEntities.put('⇑', "hArr");
+		htmlEntities.put('↑', "harr");
+		htmlEntities.put('Û', "Ucirc");
+		htmlEntities.put('û', "ucirc");
+		htmlEntities.put('Ù', "Ugrave");
+		htmlEntities.put('ù', "ugrave");
+		htmlEntities.put('¨', "uml");
+		htmlEntities.put('ϒ', "upsih");
+		htmlEntities.put('Υ', "Upsilon");
+		htmlEntities.put('υ', "upsilon");
+		htmlEntities.put('Ü', "Uuml");
+		htmlEntities.put('ü', "uuml");
+		htmlEntities.put('℘', "weierp");
+		htmlEntities.put('Ξ', "Xi");
+		htmlEntities.put('ξ', "xi");
+		htmlEntities.put('Ý', "Yacute");
+		htmlEntities.put('ý', "yacute");
+		htmlEntities.put('¥', "yen");
+		htmlEntities.put('Ÿ', "Yuml");
+		htmlEntities.put('ÿ', "yuml");
+		htmlEntities.put('Ζ', "Zeta");
+		htmlEntities.put('ζ', "zeta");
+		htmlEntities.put('\u200d', "zwj");
+		htmlEntities.put('\u200c', "zwnj");
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		StringBuilder htmlOutput = new StringBuilder();
+		for (char c : (data != null) ? String.valueOf(data).toCharArray() : new char[0]) {
+			if (htmlEntities.containsKey(c)) {
+				htmlOutput.append('&').append(htmlEntities.get(c)).append(';');
+				continue;
+			}
+			if (c > 127) {
+				htmlOutput.append("&#").append((int) c).append(';');
+				continue;
+			}
+			htmlOutput.append(c);
+		}
+		return htmlOutput.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/InsertFilter.java b/alien/src/net/pterodactylus/util/template/InsertFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/InsertFilter.java
@@ -0,0 +1,41 @@
+/*
+ * utils - InsertFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * {@link Filter} implementation that works like a {@link ReplaceFilter}, only
+ * that the actual replacement value is read from a template variable, which can
+ * be set either using {@link Template#set(String, Object)} or using a
+ * {@link StoreFilter}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class InsertFilter extends ReplaceFilter {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		parameters.put("replacement", String.valueOf(dataProvider.getData(parameters.get("key"))));
+		return super.format(dataProvider, data, parameters);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/ListAccessor.java b/alien/src/net/pterodactylus/util/template/ListAccessor.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/ListAccessor.java
@@ -0,0 +1,55 @@
+/*
+ * utils - ListAccessor.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.List;
+
+/**
+ * {@link Accessor} implementation that allows to access a {@link List} by
+ * index. “list.size” will return the size of the list, “list.isEmpty” will
+ * return the result of {@link List#isEmpty()}, and “list.0” will return the
+ * first element of the list, “list.1” the second, and so on.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListAccessor implements Accessor {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object get(DataProvider dataProvider, Object object, String member) {
+		List<?> list = (List<?>) object;
+		if ("size".equals(member)) {
+			return list.size();
+		} else if ("isEmpty".equals(member)) {
+			return list.isEmpty();
+		}
+		int index = -1;
+		try {
+			index = Integer.parseInt(member);
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		if ((index > -1) && (index < list.size())) {
+			return list.get(index);
+		}
+		return null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/LoopPart.java b/alien/src/net/pterodactylus/util/template/LoopPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/LoopPart.java
@@ -0,0 +1,221 @@
+/*
+ * utils - LoopPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link Part} implementation that loops over a {@link Collection}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class LoopPart extends ContainerPart {
+
+	/** Accessor for {@link LoopStructure}s. */
+	@SuppressWarnings("synthetic-access")
+	private final Accessor LOOP_STRUCTURE_ACCESSOR = new LoopStructureAccessor();
+
+	/** The name of the collection to loop over. */
+	private final String collectionName;
+
+	/** The name under which to store the current item. */
+	private final String itemName;
+
+	/** The name under which to store the loop structure. */
+	private final String loopName;
+
+	/**
+	 * Creates a new loop part.
+	 *
+	 * @param collectionName
+	 *            The name of the collection
+	 * @param itemName
+	 *            The name under which to store the current item
+	 */
+	public LoopPart(String collectionName, String itemName) {
+		this(collectionName, itemName, "loop");
+	}
+
+	/**
+	 * Creates a new loop part.
+	 *
+	 * @param collectionName
+	 *            The name of the collection
+	 * @param itemName
+	 *            The name under which to store the current item
+	 * @param loopName
+	 *            The name of the loop
+	 */
+	public LoopPart(String collectionName, String itemName, String loopName) {
+		this.collectionName = collectionName;
+		this.itemName = itemName;
+		this.loopName = loopName;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		Collection<?> collection = (Collection<?>) dataProvider.getData(collectionName);
+		if ((collection == null) || collection.isEmpty()) {
+			return;
+		}
+		LoopStructure loopStructure = new LoopStructure(collection.size());
+		Map<String, Object> overrideObjects = new HashMap<String, Object>();
+		overrideObjects.put(loopName, loopStructure);
+		for (Object object : collection) {
+			overrideObjects.put(itemName, object);
+			DataProvider loopDataProvider = new OverrideDataProvider(dataProvider, overrideObjects);
+			loopDataProvider.addAccessor(LoopStructure.class, LOOP_STRUCTURE_ACCESSOR);
+			for (Part part : parts) {
+				part.render(loopDataProvider, writer);
+			}
+			loopStructure.incCount();
+		}
+	}
+
+	/**
+	 * Container for information about a loop.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class LoopStructure {
+
+		/** The size of the loop. */
+		private final int size;
+
+		/** The current counter of the loop. */
+		private int count;
+
+		/**
+		 * Creates a new loop structure for a loop with the given size.
+		 *
+		 * @param size
+		 *            The size of the loop
+		 */
+		public LoopStructure(int size) {
+			this.size = size;
+		}
+
+		/**
+		 * Returns the size of the loop.
+		 *
+		 * @return The size of the loop
+		 */
+		public int getSize() {
+			return size;
+		}
+
+		/**
+		 * Returns the current counter of the loop.
+		 *
+		 * @return The current counter of the loop, in the range from {@code 0}
+		 *         to {@link #getSize() getSize() - 1}
+		 */
+		public int getCount() {
+			return count;
+		}
+
+		/**
+		 * Increments the current counter of the loop.
+		 */
+		public void incCount() {
+			++count;
+		}
+
+		/**
+		 * Returns whether the current iteration if the first one.
+		 *
+		 * @return {@code true} if the curren iteration is the first one,
+		 *         {@code false} otherwise
+		 */
+		public boolean isFirst() {
+			return count == 0;
+		}
+
+		/**
+		 * Returns whether the current iteration if the last one.
+		 *
+		 * @return {@code true} if the curren iteration is the last one, {@code
+		 *         false} otherwise
+		 */
+		public boolean isLast() {
+			return count == (size - 1);
+		}
+
+		/**
+		 * Returns whether the current loop count is odd, i.e. not divisible by
+		 * {@code 2}.
+		 *
+		 * @return {@code true} if the loop count is odd, {@code false}
+		 *         otherwise
+		 */
+		public boolean isOdd() {
+			return (count & 1) == 1;
+		}
+
+		/**
+		 * Returns whether the current loop count is even, i.e. divisible by
+		 * {@code 2}.
+		 *
+		 * @return {@code true} if the loop count is even, {@code false}
+		 *         otherwise
+		 */
+		public boolean isEven() {
+			return (count & 1) == 0;
+		}
+
+	}
+
+	/**
+	 * {@link Accessor} implementation that handles a {@link LoopStructure},
+	 * allowing access via the members “size”, “count”, “first”, and “last”.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class LoopStructureAccessor implements Accessor {
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object get(DataProvider dataProvider, Object object, String member) {
+			LoopStructure loopStructure = (LoopStructure) object;
+			if ("size".equals(member)) {
+				return loopStructure.getSize();
+			} else if ("count".equals(member)) {
+				return loopStructure.getCount();
+			} else if ("first".equals(member)) {
+				return loopStructure.isFirst();
+			} else if ("last".equals(member)) {
+				return loopStructure.isLast();
+			} else if ("odd".equals(member)) {
+				return loopStructure.isOdd();
+			} else if ("even".equals(member)) {
+				return loopStructure.isEven();
+			}
+			return null;
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/MatchFilter.java b/alien/src/net/pterodactylus/util/template/MatchFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/MatchFilter.java
@@ -0,0 +1,48 @@
+/*
+ * utils - MatchFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * {@link Filter} implementation that compares (for
+ * {@link Object#equals(Object) equality}) the data either with a {@link String}
+ * (given as parameter “value”) or an object from the {@link DataProvider}
+ * (whose name is given as parameter “key”).
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MatchFilter implements Filter {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		String key = parameters.get("key");
+		Object value = parameters.get("value");
+		if (value == null) {
+			value = dataProvider.getData(key);
+		}
+		if (value instanceof String) {
+			return value.equals(String.valueOf(data));
+		}
+		return (value != null) ? value.equals(data) : (data == null);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/MultipleDataProvider.java b/alien/src/net/pterodactylus/util/template/MultipleDataProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/MultipleDataProvider.java
@@ -0,0 +1,179 @@
+/*
+ * utils - MultipleDataProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * {@link DataProvider} implementation that can get its data from multiple other
+ * {@link DataProvider}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MultipleDataProvider extends DataProvider {
+
+	/** The source data providers. */
+	private final List<DataProvider> dataProviders = new ArrayList<DataProvider>();
+
+	/** The data stores. */
+	private final MultipleDataStore dataStore;
+
+	/**
+	 * Creates a new multiple data provider.
+	 *
+	 * @param dataProviders
+	 *            The source data providers
+	 */
+	public MultipleDataProvider(DataProvider... dataProviders) {
+		this.dataProviders.addAll(Arrays.asList(dataProviders));
+		List<DataStore> dataStores = new ArrayList<DataStore>();
+		for (DataProvider dataProvider : dataProviders) {
+			dataStores.add(dataProvider.getDataStore());
+		}
+		this.dataStore = new MultipleDataStore(dataStores);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getData(String name) throws TemplateException {
+		if (name.indexOf('.') == -1) {
+			for (DataProvider dataProvider : dataProviders) {
+				Object data = dataProvider.getDataStore().get(name);
+				if (data != null) {
+					return data;
+				}
+			}
+			return null;
+		}
+		StringTokenizer nameTokens = new StringTokenizer(name, ".");
+		Object object = null;
+		while (nameTokens.hasMoreTokens()) {
+			String nameToken = nameTokens.nextToken();
+			if (object == null) {
+				for (DataProvider dataProvider : dataProviders) {
+					object = dataProvider.getDataStore().get(nameToken);
+					if (object != null) {
+						break;
+					}
+				}
+			} else {
+				Accessor accessor = null;
+				for (DataProvider dataProvider : dataProviders) {
+					accessor = dataProvider.findAccessor(object.getClass());
+					if (accessor != null) {
+						break;
+					}
+				}
+				if (accessor != null) {
+					object = accessor.get(this, object, nameToken);
+				} else {
+					throw new TemplateException("no accessor found for " + object.getClass());
+				}
+			}
+			if (object == null) {
+				return null;
+			}
+		}
+		return object;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected DataStore getDataStore() {
+		return dataStore;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setData(String name, Object data) {
+		for (DataProvider dataProvider : dataProviders) {
+			dataProvider.setData(name, data);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected Accessor findAccessor(Class<?> clazz) {
+		for (DataProvider dataProvider : dataProviders) {
+			Accessor accessor = dataProvider.findAccessor(clazz);
+			if (accessor != null) {
+				return accessor;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * A {@link DataStore} implementation that is backed by multiple other
+	 * {@link DataStore}s.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class MultipleDataStore implements DataStore {
+
+		/** The backing data stores. */
+		private List<DataStore> dataStores = new ArrayList<DataStore>();
+
+		/**
+		 * Creates a new multiple data store.
+		 *
+		 * @param dataStores
+		 *            The backing data stores
+		 */
+		public MultipleDataStore(List<DataStore> dataStores) {
+			this.dataStores.addAll(dataStores);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object get(String name) {
+			for (DataStore dataStore : dataStores) {
+				Object data = dataStore.get(name);
+				if (data != null) {
+					return data;
+				}
+			}
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(String name, Object data) {
+			for (DataStore dataStore : dataStores) {
+				dataStore.set(name, data);
+			}
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/OverrideDataProvider.java b/alien/src/net/pterodactylus/util/template/OverrideDataProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/OverrideDataProvider.java
@@ -0,0 +1,159 @@
+/*
+ * utils - OverrideDataProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link DataProvider} implementation that uses a parent data provider but can
+ * override objects.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class OverrideDataProvider extends DataProvider {
+
+	/** The parent data provider. */
+	private final DataProvider parentDataProvider;
+
+	/** Accessors. */
+	private final Map<Class<?>, Accessor> classAccessors = new HashMap<Class<?>, Accessor>();
+
+	/**
+	 * Creates a new override data provider.
+	 *
+	 * @param parentDataProvider
+	 *            The parent data provider
+	 * @param name
+	 *            The name of the object to override
+	 * @param object
+	 *            The object
+	 */
+	public OverrideDataProvider(DataProvider parentDataProvider, String name, Object object) {
+		super(new OverrideDataStore(parentDataProvider, name, object));
+		this.parentDataProvider = parentDataProvider;
+	}
+
+	/**
+	 * Creates a new override data provider.
+	 *
+	 * @param parentDataProvider
+	 *            The parent data provider
+	 * @param overrideObjects
+	 *            The override objects
+	 */
+	public OverrideDataProvider(DataProvider parentDataProvider, Map<String, Object> overrideObjects) {
+		super(new OverrideDataStore(parentDataProvider, overrideObjects));
+		this.parentDataProvider = parentDataProvider;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void addAccessor(Class<?> clazz, Accessor accessor) {
+		classAccessors.put(clazz, accessor);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected Accessor findAccessor(Class<?> clazz) {
+		if (classAccessors.containsKey(clazz)) {
+			return classAccessors.get(clazz);
+		}
+		for (Class<?> interfaceClass : clazz.getInterfaces()) {
+			if (classAccessors.containsKey(interfaceClass)) {
+				return classAccessors.get(interfaceClass);
+			}
+		}
+		Class<?> classToCheck = clazz.getSuperclass();
+		while (classToCheck != null) {
+			if (classAccessors.containsKey(classToCheck)) {
+				return classAccessors.get(classToCheck);
+			}
+			classToCheck = classToCheck.getSuperclass();
+		}
+		return parentDataProvider.findAccessor(clazz);
+	}
+
+	/**
+	 * {@link DataStore} implementation that can override objects and redirects
+	 * requests for not-overridden objects to a parent {@link DataProvider}.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class OverrideDataStore implements DataStore {
+
+		/** The parent data provider. */
+		private final DataProvider parentDataProvider;
+
+		/** The store containing the overridden objects. */
+		private final Map<String, Object> overrideDataStore = new HashMap<String, Object>();
+
+		/**
+		 * Creates a new overriding data store.
+		 *
+		 * @param parentDataProvider
+		 *            The parent data provider
+		 * @param name
+		 *            The key of the object to override
+		 * @param data
+		 *            The object to override
+		 */
+		public OverrideDataStore(DataProvider parentDataProvider, String name, Object data) {
+			this.parentDataProvider = parentDataProvider;
+			overrideDataStore.put(name, data);
+		}
+
+		/**
+		 * Creates a new overriding data store.
+		 *
+		 * @param parentDataProvider
+		 *            The parent data provider
+		 * @param overrideDataStore
+		 *            {@link Map} containing all overriding objects
+		 */
+		public OverrideDataStore(DataProvider parentDataProvider, Map<String, Object> overrideDataStore) {
+			this.parentDataProvider = parentDataProvider;
+			this.overrideDataStore.putAll(overrideDataStore);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object get(String name) {
+			if (overrideDataStore.containsKey(name)) {
+				return overrideDataStore.get(name);
+			}
+			return parentDataProvider.getData(name);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void set(String name, Object data) {
+			parentDataProvider.setData(name, data);
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/PaginationPlugin.java b/alien/src/net/pterodactylus/util/template/PaginationPlugin.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/PaginationPlugin.java
@@ -0,0 +1,67 @@
+/*
+ * utils - PaginationPlugin.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.util.collection.Pagination;
+
+/**
+ * {@link Plugin} implementation that takes care of paginating a {@link List} of
+ * items (parameter “list”).
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PaginationPlugin implements Plugin {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void execute(DataProvider dataProvider, Map<String, String> parameters) {
+		String listKey = parameters.get("list");
+		String pageSizeString = parameters.get("pagesize");
+		String pageKey = parameters.get("page");
+		String paginationKey = parameters.get("key");
+		if (pageKey == null) {
+			pageKey = "page";
+		}
+		if (paginationKey == null) {
+			paginationKey = "pagination";
+		}
+		String pageString = String.valueOf(dataProvider.getData(pageKey));
+		int page = 0;
+		try {
+			page = Integer.parseInt(pageString);
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		int pageSize = 25;
+		try {
+			pageSize = Integer.parseInt(pageSizeString);
+		} catch (NumberFormatException nfe1) {
+			/* ignore. */
+		}
+		List<?> list = (List<?>) dataProvider.getData(listKey);
+		@SuppressWarnings({ "unchecked", "rawtypes" })
+		Pagination<?> pagination = new Pagination((list == null) ? Collections.emptyList() : list, pageSize).setPage(page);
+		dataProvider.setData(paginationKey, pagination);
+	}
+}
diff --git a/alien/src/net/pterodactylus/util/template/Part.java b/alien/src/net/pterodactylus/util/template/Part.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/Part.java
@@ -0,0 +1,42 @@
+/*
+ * utils - Part.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+
+/**
+ * Interface for a part of a template that can be rendered without further
+ * parsing.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+abstract class Part {
+
+	/**
+	 * Renders this part.
+	 *
+	 * @param dataProvider
+	 *            The data provider for the part
+	 * @param writer
+	 *            The writer to render the part to
+	 * @throws TemplateException
+	 *             if a template variable can not be parsed
+	 */
+	public abstract void render(DataProvider dataProvider, Writer writer) throws TemplateException;
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/Plugin.java b/alien/src/net/pterodactylus/util/template/Plugin.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/Plugin.java
@@ -0,0 +1,41 @@
+/*
+ * utils - Plugin.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * Defines a template plugin. A plugin can be called just like the built-in
+ * functions, e.g. “<%plugin>”. It also can have parameters just like a filter,
+ * e.g. “<%plugin parameter=value>”.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Plugin {
+
+	/**
+	 * Executes the plugin.
+	 *
+	 * @param dataProvider
+	 *            The data provider
+	 * @param parameters
+	 *            The plugin parameters
+	 */
+	public void execute(DataProvider dataProvider, Map<String, String> parameters);
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/PluginPart.java b/alien/src/net/pterodactylus/util/template/PluginPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/PluginPart.java
@@ -0,0 +1,57 @@
+/*
+ * utils - PluginPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+import java.util.Map;
+
+/**
+ * {@link Part} implementation that executes a {@link Plugin}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class PluginPart extends Part {
+
+	/** The plugin to execute. */
+	private final Plugin plugin;
+
+	/** The plugin parameters. */
+	private final Map<String, String> pluginParameters;
+
+	/**
+	 * Creates a new plugin part.
+	 *
+	 * @param plugin
+	 *            The plugin to execute
+	 * @param pluginParameters
+	 *            The plugin parameters
+	 */
+	public PluginPart(Plugin plugin, Map<String, String> pluginParameters) {
+		this.plugin = plugin;
+		this.pluginParameters = pluginParameters;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		plugin.execute(dataProvider, pluginParameters);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/ReflectionAccessor.java b/alien/src/net/pterodactylus/util/template/ReflectionAccessor.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/ReflectionAccessor.java
@@ -0,0 +1,80 @@
+/*
+ * utils - ReflectionAccessor.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * {@link Accessor} implementation that checks the object for a method that
+ * looks like a getter for the requested member name. If “object.data” is
+ * requested, the methods “getData()” and “isData()” are checked,
+ * “object.realName” would search for the methods “getRealName()” and
+ * “isRealName()”.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReflectionAccessor implements Accessor {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object get(DataProvider dataProvider, Object object, String member) {
+		Method method = null;
+		String methodName = member.substring(0, 1).toUpperCase() + member.substring(1);
+		try {
+			method = object.getClass().getMethod("get" + methodName);
+		} catch (SecurityException se1) {
+			/* TODO - logging. */
+		} catch (NoSuchMethodException nsme1) {
+			/* swallow, method just doesn’t exist. */
+		}
+		if (method == null) {
+			try {
+				method = object.getClass().getMethod("is" + methodName);
+			} catch (SecurityException e) {
+				/* TODO - logging. */
+			} catch (NoSuchMethodException e) {
+				/* swallow, method just doesn’t exist. */
+			}
+		}
+		if (method == null) {
+			try {
+				method = object.getClass().getMethod(member);
+			} catch (SecurityException e) {
+				/* TODO - logging. */
+			} catch (NoSuchMethodException e) {
+				/* swallow, method just doesn’t exist. */
+			}
+		}
+		if (method != null) {
+			try {
+				return method.invoke(object);
+			} catch (IllegalArgumentException iae1) {
+				/* TODO - logging. */
+			} catch (IllegalAccessException iae1) {
+				/* TODO - logging. */
+			} catch (InvocationTargetException ite1) {
+				/* TODO - logging. */
+			}
+		}
+		return null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/ReplaceFilter.java b/alien/src/net/pterodactylus/util/template/ReplaceFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/ReplaceFilter.java
@@ -0,0 +1,44 @@
+/*
+ * utils - ReplaceFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * {@link Filter} implementation that replaces parts of a value.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReplaceFilter implements Filter {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		String input = String.valueOf(data);
+		String needle = parameters.get("needle");
+		String replacementKey = parameters.get("replacementKey");
+		String replacement = parameters.get("replacement");
+		if (replacement == null) {
+			replacement = String.valueOf(dataProvider.getData(replacementKey));
+		}
+		return input.replace(needle, replacement);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/StoreFilter.java b/alien/src/net/pterodactylus/util/template/StoreFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/StoreFilter.java
@@ -0,0 +1,40 @@
+/*
+ * utils - StoreFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.Map;
+
+/**
+ * Filter that temporarily stores the output of the chain in a
+ * {@link ThreadLocal}. It is used in conjunction with {@link InsertFilter}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StoreFilter implements Filter {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		String key = parameters.get("key");
+		dataProvider.setData(key, data);
+		return "";
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/Template.java b/alien/src/net/pterodactylus/util/template/Template.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/Template.java
@@ -0,0 +1,649 @@
+/*
+ * utils - TemplateImpl.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+import net.pterodactylus.util.template.ConditionalPart.AndCondition;
+import net.pterodactylus.util.template.ConditionalPart.Condition;
+import net.pterodactylus.util.template.ConditionalPart.DataCondition;
+import net.pterodactylus.util.template.ConditionalPart.DataTextCondition;
+import net.pterodactylus.util.template.ConditionalPart.FilterCondition;
+import net.pterodactylus.util.template.ConditionalPart.FilterTextCondition;
+import net.pterodactylus.util.template.ConditionalPart.NotCondition;
+import net.pterodactylus.util.template.ConditionalPart.NullDataCondition;
+import net.pterodactylus.util.template.ConditionalPart.OrCondition;
+
+/**
+ * Simple template system that is geared towards easy of use and high
+ * performance.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Template {
+
+	/** The template’s default data store. */
+	private DataProvider dataProvider = new DataProvider();
+
+	/** The template’s default template provider. */
+	private TemplateProvider templateProvider = new DataTemplateProvider(dataProvider);
+
+	/** The input of the template. */
+	private final Reader input;
+
+	/** The parsed template. */
+	private Part parsedTemplate;
+
+	/** Filters for the template. */
+	private final Map<String, Filter> filters = new HashMap<String, Filter>();
+
+	/** Plugins for the template. */
+	private final Map<String, Plugin> plugins = new HashMap<String, Plugin>();
+
+	/**
+	 * Creates a new template from the given input.
+	 *
+	 * @param input
+	 *            The template’s input source
+	 */
+	public Template(Reader input) {
+		this.input = input;
+	}
+
+	/**
+	 * Returns the object with the given name from the data provider.
+	 *
+	 * @param name
+	 *            The name of the object
+	 * @return The object, or {@code null} if no object could be found
+	 */
+	public Object get(String name) {
+		return dataProvider.getData(name);
+	}
+
+	/**
+	 * Sets the template object with the given name.
+	 *
+	 * @param name
+	 *            The name of the template object
+	 * @param object
+	 *            The object to store in the template
+	 */
+	public void set(String name, Object object) {
+		dataProvider.setData(name, object);
+	}
+
+	/**
+	 * Adds an accessor to the underlying {@link DataStore}.
+	 *
+	 * @param clazz
+	 *            The class for which to add an accessor
+	 * @param accessor
+	 *            The accessor to add
+	 */
+	public void addAccessor(Class<?> clazz, Accessor accessor) {
+		dataProvider.addAccessor(clazz, accessor);
+	}
+
+	/**
+	 * Adds a filter with the given name.
+	 *
+	 * @param name
+	 *            The name of the filter
+	 * @param filter
+	 *            The filter
+	 */
+	public void addFilter(String name, Filter filter) {
+		filters.put(name, filter);
+	}
+
+	/**
+	 * Adds a plugin with the given name.
+	 *
+	 * @param name
+	 *            The name of the plugin
+	 * @param plugin
+	 *            The plugin to add
+	 */
+	public void addPlugin(String name, Plugin plugin) {
+		plugins.put(name, plugin);
+	}
+
+	/**
+	 * Sets a custom template provider for this template.
+	 *
+	 * @param templateProvider
+	 *            The new template provider
+	 */
+	public void setTemplateProvider(TemplateProvider templateProvider) {
+		this.templateProvider = templateProvider;
+	}
+
+	/**
+	 * Exposes the data provider, e.g. for {@link TemplatePart}.
+	 *
+	 * @return The template’s data provider
+	 */
+	DataProvider getDataProvider() {
+		return dataProvider;
+	}
+
+	/**
+	 * Parses the input of the template if it wasn’t already parsed.
+	 *
+	 * @throws TemplateException
+	 *             if the template can not be parsed
+	 */
+	public synchronized void parse() throws TemplateException {
+		if (parsedTemplate == null) {
+			parsedTemplate = extractParts();
+		}
+	}
+
+	/**
+	 * Renders the template to the given writer.
+	 *
+	 * @param writer
+	 *            The write to render the template to
+	 * @throws TemplateException
+	 *             if the template can not be parsed
+	 */
+	public synchronized void render(Writer writer) throws TemplateException {
+		render(dataProvider, writer);
+	}
+
+	/**
+	 * Renders the template to the given writer.
+	 *
+	 * @param dataProvider
+	 *            The data provider for template variables
+	 * @param writer
+	 *            The write to render the template to
+	 * @throws TemplateException
+	 *             if the template can not be parsed
+	 */
+	public synchronized void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		parse();
+		parsedTemplate.render(dataProvider, writer);
+	}
+
+	//
+	// PRIVATE METHODS
+	//
+
+	/**
+	 * Parses the template and creates {@link Part}s of the input.
+	 *
+	 * @return The list of parts created from the template’s {@link #input}
+	 * @throws TemplateException
+	 *             if the template can not be parsed correctly
+	 */
+	private Part extractParts() throws TemplateException {
+		BufferedReader bufferedInputReader;
+		if (input instanceof BufferedReader) {
+			bufferedInputReader = (BufferedReader) input;
+		} else {
+			bufferedInputReader = new BufferedReader(input);
+		}
+		Stack<String> commandStack = new Stack<String>();
+		Stack<ContainerPart> partsStack = new Stack<ContainerPart>();
+		Stack<String> lastCollectionName = new Stack<String>();
+		Stack<String> lastLoopName = new Stack<String>();
+		Stack<Condition> lastCondition = new Stack<Condition>();
+		Stack<List<Condition>> lastConditions = new Stack<List<Condition>>();
+		Stack<String> lastIfCommand = new Stack<String>();
+		ContainerPart parts = new ContainerPart();
+		StringBuilder currentTextPart = new StringBuilder();
+		boolean gotLeftAngleBracket = false;
+		boolean inAngleBracket = false;
+		boolean inSingleQuotes = false;
+		boolean inDoubleQuotes = false;
+		while (true) {
+			int nextCharacter;
+			try {
+				nextCharacter = bufferedInputReader.read();
+			} catch (IOException ioe1) {
+				throw new TemplateException("Can not read template.", ioe1);
+			}
+			if (nextCharacter == -1) {
+				break;
+			}
+			if (inAngleBracket) {
+				if (inSingleQuotes) {
+					if (nextCharacter == '\'') {
+						inSingleQuotes = false;
+					}
+					currentTextPart.append((char) nextCharacter);
+				} else if (inDoubleQuotes) {
+					if (nextCharacter == '"') {
+						inDoubleQuotes = false;
+					}
+					currentTextPart.append((char) nextCharacter);
+				} else if (nextCharacter == '\'') {
+					inSingleQuotes = true;
+					currentTextPart.append((char) nextCharacter);
+				} else if (nextCharacter == '"') {
+					inDoubleQuotes = true;
+					currentTextPart.append((char) nextCharacter);
+				} else if (nextCharacter == '>') {
+					inAngleBracket = false;
+					String tagContent = currentTextPart.toString().trim();
+					currentTextPart.setLength(0);
+					Iterator<String> tokens = parseTag(tagContent).iterator();
+					if (!tokens.hasNext()) {
+						throw new TemplateException("empty tag found");
+					}
+					String function = tokens.next();
+					if (function.startsWith("/")) {
+						String lastFunction = commandStack.pop();
+						if (!("/" + lastFunction).equals(function)) {
+							throw new TemplateException("unbalanced template, /" + lastFunction + " expected, " + function + " found");
+						}
+						if (lastFunction.equals("foreach")) {
+							ContainerPart innerParts = parts;
+							parts = partsStack.pop();
+							lastCollectionName.pop();
+							lastLoopName.pop();
+							parts.add(innerParts);
+						} else if (lastFunction.equals("first") || lastFunction.equals("notfirst") || lastFunction.equals("last") || lastFunction.equals("notlast") || lastFunction.equals("odd") || lastFunction.equals("even")) {
+							ContainerPart innerParts = parts;
+							parts = partsStack.pop();
+							parts.add(innerParts);
+						} else if (lastFunction.equals("if")) {
+							ContainerPart innerParts = parts;
+							parts = partsStack.pop();
+							lastCondition.pop();
+							lastConditions.pop();
+							parts.add(innerParts);
+							lastIfCommand.pop();
+						}
+					} else if (function.equals("foreach")) {
+						if (!tokens.hasNext()) {
+							throw new TemplateException("foreach requires at least one parameter");
+						}
+						String collectionName = tokens.next();
+						String itemName = null;
+						if (tokens.hasNext()) {
+							itemName = tokens.next();
+						}
+						String loopName = "loop";
+						if (tokens.hasNext()) {
+							loopName = tokens.next();
+						}
+						partsStack.push(parts);
+						parts = new LoopPart(collectionName, itemName, loopName);
+						commandStack.push("foreach");
+						lastCollectionName.push(collectionName);
+						lastLoopName.push(loopName);
+					} else if (function.equals("foreachelse")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("foreachelse is only allowed in foreach");
+						}
+						partsStack.peek().add(parts);
+						parts = new EmptyLoopPart(lastCollectionName.peek());
+					} else if (function.equals("first")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("first is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".first"));
+						commandStack.push("first");
+					} else if (function.equals("notfirst")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("notfirst is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".first", true));
+						commandStack.push("notfirst");
+					} else if (function.equals("last")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("last is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".last"));
+						commandStack.push("last");
+					} else if (function.equals("notlast")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("notlast is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".last", true));
+						commandStack.push("notlast");
+					} else if (function.equals("odd")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("odd is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".odd"));
+						commandStack.push("odd");
+					} else if (function.equals("even")) {
+						if (!"foreach".equals(commandStack.peek())) {
+							throw new TemplateException("even is only allowed in foreach");
+						}
+						partsStack.push(parts);
+						final String loopName = lastLoopName.peek();
+						parts = new ConditionalPart(new ConditionalPart.DataCondition(loopName + ".even"));
+						commandStack.push("even");
+					} else if (function.equals("if") || function.equals("ifnull")) {
+						if (!tokens.hasNext()) {
+							throw new TemplateException("if requires one or two parameters");
+						}
+						String itemName = tokens.next();
+						boolean checkForNull = function.equals("ifnull");
+						boolean invert = false;
+						if (itemName.equals("!")) {
+							invert = true;
+							if (!tokens.hasNext()) {
+								throw new TemplateException("if ! requires one parameter");
+							}
+							itemName = tokens.next();
+						} else {
+							if (itemName.startsWith("!")) {
+								invert = true;
+								itemName = itemName.substring(1);
+							}
+						}
+						boolean directText = false;
+						if (itemName.startsWith("=")) {
+							if (checkForNull) {
+								throw new TemplateException("direct text ('=') with ifnull is not allowed");
+							}
+							itemName = itemName.substring(1);
+							directText = true;
+						}
+						Map<Filter, Map<String, String>> allFilterParameters = parseFilters(tokens);
+						partsStack.push(parts);
+						Condition condition = allFilterParameters.isEmpty() ? (checkForNull ? new NullDataCondition(itemName, invert) : (directText ? new DataTextCondition(itemName, invert) : new DataCondition(itemName, invert))) : (directText ? new FilterTextCondition(itemName, allFilterParameters.keySet(), allFilterParameters, invert) : new FilterCondition(itemName, allFilterParameters.keySet(), allFilterParameters, invert));
+						parts = new ConditionalPart(condition);
+						commandStack.push("if");
+						lastCondition.push(condition);
+						lastConditions.push(new ArrayList<Condition>(Arrays.asList(condition)));
+						lastIfCommand.push("if");
+					} else if (function.equals("else")) {
+						if (!"if".equals(commandStack.peek())) {
+							throw new TemplateException("else is only allowed in if");
+						}
+						if (!"if".equals(lastIfCommand.peek()) && !"elseif".equals(lastIfCommand.peek())) {
+							throw new TemplateException("else may only follow if or elseif");
+						}
+						partsStack.peek().add(parts);
+						Condition condition = new NotCondition(new OrCondition(lastConditions.peek()));
+						parts = new ConditionalPart(condition);
+						lastIfCommand.pop();
+						lastIfCommand.push("else");
+					} else if (function.equals("elseif") || function.equals("elseifnull")) {
+						if (!"if".equals(commandStack.peek())) {
+							throw new TemplateException("elseif is only allowed in if");
+						}
+						if (!"if".equals(lastIfCommand.peek()) && !"elseif".equals(lastIfCommand.peek())) {
+							throw new TemplateException("elseif is only allowed after if or elseif");
+						}
+						if (!tokens.hasNext()) {
+							throw new TemplateException("elseif requires one or two parameters");
+						}
+						String itemName = tokens.next();
+						boolean checkForNull = function.equals("elseifnull");
+						boolean invert = false;
+						if (itemName.equals("!")) {
+							invert = true;
+							if (!tokens.hasNext()) {
+								throw new TemplateException("if ! requires one parameter");
+							}
+							itemName = tokens.next();
+						} else {
+							if (itemName.startsWith("!")) {
+								invert = true;
+								itemName = itemName.substring(1);
+							}
+						}
+						Map<Filter, Map<String, String>> allFilterParameters = parseFilters(tokens);
+						partsStack.peek().add(parts);
+						Condition condition = new AndCondition(new NotCondition(lastCondition.pop()), allFilterParameters.isEmpty() ? (checkForNull ? new NullDataCondition(itemName, invert) : new DataCondition(itemName, invert)) : new FilterCondition(itemName, allFilterParameters.keySet(), allFilterParameters, invert));
+						parts = new ConditionalPart(condition);
+						lastCondition.push(condition);
+						lastConditions.peek().add(condition);
+						lastIfCommand.pop();
+						lastIfCommand.push("elseif");
+					} else if (function.equals("include")) {
+						if (!tokens.hasNext()) {
+							throw new TemplateException("include requires one parameter");
+						}
+						String templateName = tokens.next();
+						Template includedTemplate = templateProvider.getTemplate(templateName);
+						if (includedTemplate != null) {
+							parts.add(new TemplatePart(includedTemplate));
+						}
+					} else if (plugins.containsKey(function)) {
+						Map<String, String> pluginParameters = parseParameters(tokens);
+						parts.add(new PluginPart(plugins.get(function), pluginParameters));
+					} else {
+						boolean directText = false;
+						String itemName = function;
+						if (function.equals("=")) {
+							if (!tokens.hasNext()) {
+								throw new TemplateException("empty tag found");
+							}
+							itemName = tokens.next();
+							directText = true;
+						} else if (function.startsWith("=")) {
+							itemName = function.substring(1);
+							directText = true;
+						}
+						Map<Filter, Map<String, String>> allFilterParameters = parseFilters(tokens);
+						if (directText) {
+							parts.add(new FilteredTextPart(itemName, allFilterParameters.keySet(), allFilterParameters));
+						} else {
+							parts.add(new FilteredPart(itemName, allFilterParameters.keySet(), allFilterParameters));
+						}
+					}
+				} else {
+					currentTextPart.append((char) nextCharacter);
+				}
+				continue;
+			}
+			if (gotLeftAngleBracket) {
+				if (nextCharacter == '%') {
+					inAngleBracket = true;
+					if (currentTextPart.length() > 0) {
+						parts.add(new TextPart(currentTextPart.toString()));
+						currentTextPart.setLength(0);
+					}
+				} else {
+					currentTextPart.append('<').append((char) nextCharacter);
+				}
+				gotLeftAngleBracket = false;
+				continue;
+			}
+			if (nextCharacter == '<') {
+				gotLeftAngleBracket = true;
+				continue;
+			}
+			currentTextPart.append((char) nextCharacter);
+		}
+		if (currentTextPart.length() > 0) {
+			parts.add(new TextPart(currentTextPart.toString()));
+		}
+		if (!partsStack.isEmpty()) {
+			throw new TemplateException("Unbalanced template.");
+		}
+		return parts;
+	}
+
+	/**
+	 * Parses filters from the rest of the tokens.
+	 *
+	 * @param tokens
+	 *            The tokens to parse
+	 * @return The parsed filters
+	 */
+	private Map<Filter, Map<String, String>> parseFilters(Iterator<String> tokens) {
+		Map<Filter, Map<String, String>> allFilterParameters = new LinkedHashMap<Filter, Map<String, String>>();
+		if (tokens.hasNext() && (tokens.next() != null)) {
+			throw new TemplateException("expected \"|\" token");
+		}
+		while (tokens.hasNext()) {
+			String filterName = tokens.next();
+			Filter filter = filters.get(filterName);
+			if (filter == null) {
+				throw new TemplateException("unknown filter: " + filterName);
+			}
+			filter = new FilterWrapper(filter);
+			Map<String, String> filterParameters = parseParameters(tokens);
+			allFilterParameters.put(filter, filterParameters);
+		}
+		return allFilterParameters;
+	}
+
+	/**
+	 * Parses parameters from the given tokens.
+	 *
+	 * @param tokens
+	 *            The tokens to parse the parameters from
+	 * @return The parsed parameters
+	 * @throws TemplateException
+	 *             if an invalid parameter declaration is found
+	 */
+	private Map<String, String> parseParameters(Iterator<String> tokens) throws TemplateException {
+		Map<String, String> parameters = new HashMap<String, String>();
+		while (tokens.hasNext()) {
+			String parameterToken = tokens.next();
+			if (parameterToken == null) {
+				break;
+			}
+			int equals = parameterToken.indexOf('=');
+			if (equals == -1) {
+				throw new TemplateException("found parameter without \"=\" sign");
+			}
+			String key = parameterToken.substring(0, equals).trim();
+			String value = parameterToken.substring(equals + 1);
+			parameters.put(key, value);
+		}
+		return parameters;
+	}
+
+	/**
+	 * Parses the content of a tag into words, obeying syntactical rules about
+	 * separators and quotes. Separators are parsed as {@code null}.
+	 *
+	 * @param tagContent
+	 *            The content of the tag to parse
+	 * @return The parsed words
+	 */
+	static List<String> parseTag(String tagContent) {
+		List<String> expressions = new ArrayList<String>();
+		boolean inSingleQuotes = false;
+		boolean inDoubleQuotes = false;
+		boolean inBackslash = false;
+		StringBuilder currentExpression = new StringBuilder();
+		for (char c : tagContent.toCharArray()) {
+			if (inSingleQuotes) {
+				if (c == '\'') {
+					inSingleQuotes = false;
+				} else {
+					currentExpression.append(c);
+				}
+			} else if (inBackslash) {
+				currentExpression.append(c);
+				inBackslash = false;
+			} else if (inDoubleQuotes) {
+				if (c == '"') {
+					inDoubleQuotes = false;
+				} else if (c == '\\') {
+					inBackslash = true;
+				} else {
+					currentExpression.append(c);
+				}
+			} else {
+				if (c == '\'') {
+					inSingleQuotes = true;
+				} else if (c == '"') {
+					inDoubleQuotes = true;
+				} else if (c == '\\') {
+					inBackslash = true;
+				} else if (c == '|') {
+					if (currentExpression.toString().trim().length() > 0) {
+						expressions.add(currentExpression.toString());
+						currentExpression.setLength(0);
+					}
+					expressions.add(null);
+				} else {
+					if (c == ' ') {
+						if (currentExpression.length() > 0) {
+							expressions.add(currentExpression.toString());
+							currentExpression.setLength(0);
+						}
+					} else {
+						currentExpression.append(c);
+					}
+				}
+			}
+		}
+		if (currentExpression.length() > 0) {
+			expressions.add(currentExpression.toString());
+		}
+		return expressions;
+	}
+
+	/**
+	 * Wrapper around a {@link Filter} that allows adding several instances of a
+	 * filter to a single tag.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class FilterWrapper implements Filter {
+
+		/** The original filter. */
+		private final Filter originalFilter;
+
+		/**
+		 * Creates a new filter wrapper.
+		 *
+		 * @param originalFilter
+		 *            The filter to wrap
+		 */
+		public FilterWrapper(Filter originalFilter) {
+			this.originalFilter = originalFilter;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+			return originalFilter.format(dataProvider, data, parameters);
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/TemplateException.java b/alien/src/net/pterodactylus/util/template/TemplateException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/TemplateException.java
@@ -0,0 +1,71 @@
+/*
+ * utils - TemplateException.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+/**
+ * Exception that is thrown when a {@link Template} can not be rendered because
+ * its input can not be parsed correctly, or when its template variables can not
+ * be parsed or evaluated.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemplateException extends RuntimeException {
+
+	/**
+	 * Creates a new template exception.
+	 */
+	public TemplateException() {
+		super();
+	}
+
+	/**
+	 * Creates a new template exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public TemplateException(String message) {
+		super(message);
+
+	}
+
+	/**
+	 * Creates a new template exception.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public TemplateException(Throwable cause) {
+		super(cause);
+
+	}
+
+	/**
+	 * Creates a new template exception.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public TemplateException(String message, Throwable cause) {
+		super(message, cause);
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/TemplateFactory.java b/alien/src/net/pterodactylus/util/template/TemplateFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/TemplateFactory.java
@@ -0,0 +1,40 @@
+/*
+ * utils - TemplateFactory.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Reader;
+
+/**
+ * Interface for factories that can create templates with pre-defined settings,
+ * e.g. a template factory that creates templates with a default
+ * {@link HtmlFilter} added.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface TemplateFactory {
+
+	/**
+	 * Creates a template that is read from the given source.
+	 *
+	 * @param templateSource
+	 *            The source of the template
+	 * @return A template that is created from the given source
+	 */
+	public Template createTemplate(Reader templateSource);
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/TemplatePart.java b/alien/src/net/pterodactylus/util/template/TemplatePart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/TemplatePart.java
@@ -0,0 +1,50 @@
+/*
+ * utils - TemplatePart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.Writer;
+
+/**
+ * A {@link Part} that includes a complete {@link Template}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemplatePart extends Part {
+
+	/** The template to include. */
+	private final Template template;
+
+	/**
+	 * Creates a new template part.
+	 *
+	 * @param template
+	 *            The template to render
+	 */
+	public TemplatePart(Template template) {
+		this.template = template;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		template.render(new MultipleDataProvider(template.getDataProvider(), new UnmodifiableDataProvider(dataProvider)), writer);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/TemplateProvider.java b/alien/src/net/pterodactylus/util/template/TemplateProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/TemplateProvider.java
@@ -0,0 +1,41 @@
+/*
+ * utils - TemplateProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+/**
+ * A template provider is used to load templates that are included in other
+ * templates when rendering a template.
+ * <p>
+ * Templates have a default template provider installed that tries to retrieve
+ * the requsted template from the template’s data provider.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface TemplateProvider {
+
+	/**
+	 * Retrieves the template with the given name.
+	 *
+	 * @param templateName
+	 *            The name of the template
+	 * @return The template with the given name, or {@code null} if there is no
+	 *         template with the requested name
+	 */
+	public Template getTemplate(String templateName);
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/TextPart.java b/alien/src/net/pterodactylus/util/template/TextPart.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/TextPart.java
@@ -0,0 +1,55 @@
+/*
+ * utils - TextPart.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * A {@link Part} that contains only text.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class TextPart extends Part {
+
+	/** The text of the part. */
+	private final String text;
+
+	/**
+	 * Creates a new text part.
+	 *
+	 * @param text
+	 *            The text of the part
+	 */
+	public TextPart(String text) {
+		this.text = text;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void render(DataProvider dataProvider, Writer writer) throws TemplateException {
+		try {
+			writer.write(text);
+		} catch (IOException ioe1) {
+			throw new TemplateException("Can not render part.", ioe1);
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/UnmodifiableDataProvider.java b/alien/src/net/pterodactylus/util/template/UnmodifiableDataProvider.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/UnmodifiableDataProvider.java
@@ -0,0 +1,82 @@
+/*
+ * utils - UnmodifiableDataProvider.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+/**
+ * Wrapper around a {@link DataProvider} that prevents modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnmodifiableDataProvider extends DataProvider {
+
+	/** The wrapped data provider. */
+	private final DataProvider dataProvider;
+
+	/**
+	 * Creates a new unmodifiable data provider backed by the given data
+	 * provider.
+	 *
+	 * @param dataProvider
+	 *            The data provider to wrap
+	 */
+	public UnmodifiableDataProvider(DataProvider dataProvider) {
+		this.dataProvider = dataProvider;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void addAccessor(Class<?> clazz, Accessor accessor) {
+		/* ignore. */
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected Accessor findAccessor(Class<?> clazz) {
+		return dataProvider.findAccessor(clazz);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected DataStore getDataStore() {
+		/* TODO - return an unmodifiable data store here? */
+		return dataProvider.getDataStore();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Object getData(String name) throws TemplateException {
+		return dataProvider.getData(name);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setData(String name, Object data) {
+		/* ignore. */
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/XmlFilter.java b/alien/src/net/pterodactylus/util/template/XmlFilter.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/XmlFilter.java
@@ -0,0 +1,59 @@
+/*
+ * utils - XmlFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.template;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filters XML by replacing double quotes characters, apostrophes, the less-than
+ * character, the greater-than characters, and the ampersand by their respective
+ * XML entities.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class XmlFilter implements Filter {
+
+	/** Map of defined XML entities. */
+	private static final Map<Character, String> xmlEntities = new HashMap<Character, String>();
+
+	static {
+		xmlEntities.put('&', "amp");
+		xmlEntities.put('\'', "apos");
+		xmlEntities.put('>', "gt");
+		xmlEntities.put('<', "lt");
+		xmlEntities.put('"', "quot");
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+		StringBuilder xmlOutput = new StringBuilder();
+		for (char c : (data != null) ? String.valueOf(data).toCharArray() : new char[0]) {
+			if (xmlEntities.containsKey(c)) {
+				xmlOutput.append('&').append(xmlEntities.get(c)).append(';');
+				continue;
+			}
+			xmlOutput.append(c);
+		}
+		return xmlOutput.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/template/package-info.java b/alien/src/net/pterodactylus/util/template/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/template/package-info.java
@@ -0,0 +1,360 @@
+/*
+ * utils - package-info.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Light-weight template system.
+ *
+ * <h3>Writing Templates</h3>
+ *
+ * <p>
+ * Inserting values and control structures such as loops into a template uses
+ * a syntax that is well-known by other preprocessors, such es PHP or JSPs.
+ * For example:
+ * </p>
+ *
+ * <pre>
+ * <html>
+ * <head>
+ * <title>
+ * <% title >
+ * </title>
+ * </head>
+ * </html>
+ * </pre>
+ *
+ * <p>
+ * This will insert the value of the template variable named “title” into the
+ * template.
+ * </p>
+ *
+ * <h3>Setting Template Variables</h3>
+ *
+ * <p>
+ * Variables in a template are set using the
+ * {@link net.pterodactylus.util.template.Template#set(String, Object)} method.
+ * </p>
+ *
+ * <pre>
+ * Template template = new Template(new FileReader("template.html"));
+ * template.set("variable", "value");
+ * template.set("items", Arrays.asList("foo", "bar", "baz"));
+ * template.render(outputWriter);
+ * </pre>
+ *
+ * <h3>Looping Over Collections</h3>
+ *
+ * <p>
+ * You can set the value of a template variable to an instance of
+ * {@link java.util.Collection}. This allows you to iterate over the items in said collection:
+ * </p>
+ *
+ * <pre>
+ * <ul>
+ * <%foreach pointCollection pointItem>
+ * <li>
+ * Item: <% pointItem>
+ * </li>
+ * <%/foreach>
+ * </ul>
+ * </pre>
+ *
+ * This will output the value of each item in the collection.
+ *
+ * <h3>Loop Properties</h3>
+ *
+ * <p>
+ * Each iteration of a loop has numerous properties, such as being the first
+ * iteration, or the last, or neither of them. These properties can be
+ * accessed in a template.
+ * </p>
+ *
+ * <pre>
+ * <%foreach pointCollection pointItem>
+ * <%first>
+ * The collection contains the following items:
+ * <ul>
+ * <%/first>
+ * <li>
+ * Item: <% pointItem>
+ * </li>
+ * <%last>
+ * </ul>
+ * <%/last>
+ * <%/foreach>
+ * </pre>
+ *
+ * <p>
+ * The loop properties that can be accessed in this way are: {@code first},
+ * {@code last}, {@code notfirst}, {@code notlast}, {@code odd}, {@code even}.
+ * </p>
+ *
+ * <h3>Item Properties</h3>
+ *
+ * <p>
+ * Template variable names can specify a hierarchy: “item.index” specifies the
+ * member “index” of the value of template variable “item”. The default
+ * template implementation can only handle getting members of template
+ * variables that contain instances of  {@link java.util.Map}; it is possible
+ * to define member accessors for your own types (see below).
+ * </p>
+ *
+ * <pre>
+ * <ul>
+ * <%foreach itemCollection item>
+ * <li>
+ * Item: <a href="item?id=<% item.id>"><% item.name></a>
+ * </li>
+ * <%/foreach>
+ * </ul>
+ * </pre>
+ *
+ * <p>
+ * When {@code itemCollection} is properly set up this will print each item’s
+ * name with a link to an item page that receives the ID of the item as
+ * parameter. If {@code item} does not refer to a {@link java.util.Map} instance,
+ * a custom accessor (see below) is necessary.
+ * </p>
+ *
+ * <h3>Handling Custom Types</h3>
+ *
+ * <p>
+ * The template system can be extended using
+ * {@link net.pterodactylus.util.template.Accessor}s. An accessor is used to
+ * allow template variable syntax like “object.foo”. Depending on the type of
+ * {@code object}  the appropriate accessor is used to find the value of the
+ * member “foo” (which can e.g. be retrieved by calling a complicated operation
+ * on {@code object}).
+ * </p>
+ *
+ * <p>
+ * With a custom type {@code Item} that exposes both an ID and a name (using
+ * {@code getID()} and {@code getName()} methods), the following
+ * {@link net.pterodactylus.util.template.Accessor} will allow the above
+ * example to work.
+ * </p>
+ *
+ * <pre>
+ * public class ItemAccessor implements Accessor {
+ *     private final Item item;
+ *     public ItemAccessor(Item item) { this.item = item; }
+ *     public int getID() { return item.getID(); }
+ *     public String getName() { return item.getName(); }
+ * }
+ * </pre>
+ *
+ * <h3>Conditional Execution</h3>
+ *
+ * <p>
+ * With a loop and its constructs (e.g. <%first> or <%even>) you
+ * can already shape your formatted text in quite some ways. If for some
+ * reason this is not enough you do have another possibility.
+ * </p>
+ *
+ * <pre>
+ * <p>
+ *     The color is:
+ *     <%if color.black>
+ *         black
+ *     <%elseif color.red>
+ *         red
+ *     <%elseif ! color.green>
+ *         not green
+ *     <%else>
+ *         green
+ *     <%/if>
+ * </p>
+ * </pre>
+ *
+ * <p>
+ * At the moment the {@code <%if>} directive requires a single argument
+ * which has to evaluate to a {@link java.lang.Boolean} object. The object may
+ * be prepended by an exclamation point (“!”, either with or without
+ * whitespace following it) to signify that the condition should be reversed.
+ * Using a custom accessor this can easily be accomplished. Any further
+ * parsing (and expression evaluation) would make the template parser and
+ * renderer almost infinitely more complex (and very not-light-weight-anymore).
+ * </p>
+ *
+ * <h3>Filtering Output</h3>
+ *
+ * <p>
+ * One large issue when handling text in web pages is escaping the HTML code
+ * so that the content of a web page does not have the capability of inserting
+ * custom code into a web site, or destroying its design by unbalanced tags.
+ * The template supports filtering output but does not have any output filters
+ * added to it by default.
+ * </p>
+ *
+ * <pre>
+ * Template template = new Template(templateReader);
+ * template.addFilter("html", new HtmlFilter());
+ * </pre>
+ *
+ * <p>
+ * This will a filter for HTML to the list of available filters. If you want
+ * to escape some text in your template, apply it using the pipe character
+ * (“|”, with or without whitespace around it).
+ * </p>
+ *
+ * <pre>
+ * <div>Your name is <% name|html>, right?</div>
+ * </pre>
+ *
+ * <p>
+ * You can also use several filters for a single variable output. Those are
+ * executed in the order they are specified.
+ * </p>
+ *
+ * <pre>
+ * <div>Your name is <% name|html|replace needle=Frank replacement=Steve>, right?</div>
+ * </pre>
+ *
+ * <h3>Storing Values in the Template</h3>
+ *
+ * <p>
+ * Sometimes it can be necessary to store a value in the template for later
+ * use. In conjunction with a replacement filter this can be used to include
+ * template variables in strings that are output by other filters, e.g. an
+ * i18n filter.
+ * </p>
+ *
+ * <pre>
+ * <% user | html | store key='htmlUser'>
+ * <% HelloText | i18n | html | insert needle='${user}' key='htmlUser'>
+ * </pre>
+ *
+ * <p>
+ * The “insert” filter can also read variables directly from the template.
+ * </p>
+ *
+ * <h3>Internationalization / Localization (i18n, l10n)</h3>
+ *
+ * <p>
+ * When creating web pages for larger projects you often have to deal with
+ * multiple languages. One possibility to achieve multilingual support in a
+ * template system would be to simply ignore language support in the template
+ * system and create a new template for each language. However, this solution
+ * is not desirable for any of the participating parties: the programmer has
+ * to load a different template depending on the language; the designer has to
+ * copy a desgin change into every translated template again; the translator
+ * needs to copy and process a complete template, potentially missing
+ * translatable items in a sea of design markup.
+ * </p>
+ *
+ * <p>
+ * One possible solution is the possibility to hardcode values in the template
+ * and run them through arbitrary filters.
+ * </p>
+ *
+ * <pre>
+ * <div><%= Item.Name | i18n | html></div>
+ * <div><% item.name | html></div>
+ * <div><%= Item.ID | i18n | html></div>
+ * <div><% item.id | html></div>
+ * </pre>
+ *
+ * <p>
+ * In this example the strings “Item.Name” and “Item.ID” are run through a
+ * custom {@link net.pterodactylus.util.template.Filter} that replaces a
+ * language-independent key into a translated string. To prevent nasty
+ * surprises the translated string is also run through a HTML filter before
+ * the final value is printed on the web page.
+ * </p>
+ *
+ * <h3>Whitespace</h3>
+ *
+ * <p>
+ * Sometimes, e.g. when using {@code <%=} or filters, whitespace in a filter
+ * directive needs to be preserved. This can be accomplished by using single
+ * quotes, double quotes, or a backslash.
+ * </p>
+ *
+ * <h4>Single Quotes</h4>
+ *
+ * <p>
+ * Single quotes preserve all characters until another single quote character
+ * is encountered.
+ * </p>
+ *
+ * <h4>Double Quotes</h4>
+ *
+ * <p>
+ * Double quotes preserve all characters until either another double quote or
+ * a backslash is encountered.
+ * </p>
+ *
+ * <h4>Backslash</h4>
+ *
+ * <p>
+ * The backslash preserves the next character but is discarded itself.
+ * </p>
+ *
+ * <h4>Examples</h4>
+ *
+ * <pre>
+ * <%= foo | replace needle=foo replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will replace the text “foo” with the text “bar baz”.
+ * </p>
+ *
+ * <pre>
+ * <%= "foo bar" | replace needle=foo replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will replace the text “foo bar” with the text “bar baz bar”.
+ * </p>
+ *
+ * <pre>
+ * <%= "foo bar" | replace needle='foo bar' replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will replace the text “foo bar” with the text “bar baz”.
+ * </p>
+ *
+ * <pre>
+ * <%= "foo bar" | replace needle=foo\ bar replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will also replace the text “foo bar” with the text “bar baz”.
+ * </p>
+ *
+ * <pre>
+ * <%= "foo bar" | replace needle="foo\ bar" replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will also replace the text “foo bar” with the text “bar baz”.
+ * </p>
+ *
+ * <pre>
+ * <%= "foo bar" | replace needle='foo\ bar' replacement="bar baz">
+ * </pre>
+ *
+ * <p>
+ * This will not replace the text “foo bar” with anything because the text
+ * “foo\ bar” is not found.
+ * </p>
+ *
+ */
+
+package net.pterodactylus.util.template;
+
diff --git a/alien/src/net/pterodactylus/util/text/StringEscaper.java b/alien/src/net/pterodactylus/util/text/StringEscaper.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/text/StringEscaper.java
@@ -0,0 +1,227 @@
+/*
+ * utils - StringEscaper.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.text;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.util.number.Hex;
+
+/**
+ * Contains different methods to escape strings, e.g. for storage in a
+ * text-based medium like databases or an XML file.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StringEscaper {
+
+	/**
+	 * Converts the string into a form that is suitable for storage in
+	 * text-based medium.
+	 *
+	 * @param original
+	 *            The original string
+	 * @return A string that can be used for text-based persistence
+	 */
+	public static String persistString(String original) {
+		if (original == null) {
+			return "";
+		}
+		StringBuilder persistedString = new StringBuilder();
+		persistedString.append(original.length()).append(':');
+		for (char c : original.toCharArray()) {
+			persistedString.append(Hex.toHex(c, 4));
+		}
+		return persistedString.toString();
+	}
+
+	/**
+	 * Recovers the original string from a string that has been created with
+	 * {@link #persistString(String)}.
+	 *
+	 * @param persistedString
+	 *            The persisted string
+	 * @return The original string
+	 * @throws TextException
+	 *             if the persisted string can not be parsed
+	 */
+	public static String unpersistString(String persistedString) throws TextException {
+		if (persistedString.length() == 0) {
+			return null;
+		}
+		int colon = persistedString.indexOf(':');
+		if (colon == -1) {
+			throw new TextException("no colon in persisted string");
+		}
+		if (((persistedString.length() - (colon + 1)) % 4) != 0) {
+			throw new TextException("invalid length of persisted string");
+		}
+		int length = -1;
+		try {
+			length = Integer.parseInt(persistedString.substring(0, colon));
+		} catch (NumberFormatException nfe1) {
+			throw new TextException("could not parse length", nfe1);
+		}
+		if (length < 0) {
+			throw new TextException("invalid length: " + length);
+		}
+		StringBuilder unpersistedString = new StringBuilder(length);
+		try {
+			for (int charIndex = colon + 1; charIndex < persistedString.length(); charIndex += 4) {
+				char c = (char) Integer.parseInt(persistedString.substring(charIndex, charIndex + 4), 16);
+				unpersistedString.append(c);
+			}
+		} catch (NumberFormatException nfe1) {
+			throw new TextException("invalid character in persisted string", nfe1);
+		}
+		return unpersistedString.toString();
+	}
+
+	/**
+	 * Splits a string into words. Words are separated by space characters.
+	 *
+	 * @param line
+	 *            The line to parse
+	 * @return The parsed words
+	 * @throws TextException
+	 *             if quotes are not closed
+	 */
+	public static List<String> parseLine(String line) throws TextException {
+		boolean inDoubleQuote = false;
+		boolean inSingleQuote = false;
+		boolean backslashed = false;
+		List<String> words = new ArrayList<String>();
+		boolean wordEmpty = true;
+		StringBuilder currentWord = new StringBuilder();
+		for (char c : line.toCharArray()) {
+			if (c == '"') {
+				if (inSingleQuote || backslashed) {
+					currentWord.append(c);
+					backslashed = false;
+				} else {
+					inDoubleQuote ^= true;
+					wordEmpty = false;
+				}
+			} else if (c == '\'') {
+				if (inDoubleQuote || backslashed) {
+					currentWord.append(c);
+					backslashed = false;
+				} else {
+					inSingleQuote ^= true;
+					wordEmpty = false;
+				}
+			} else if (c == '\\') {
+				if (inSingleQuote || backslashed) {
+					currentWord.append(c);
+					backslashed = false;
+				} else {
+					backslashed = true;
+				}
+			} else if (c == ' ') {
+				if (inDoubleQuote || inSingleQuote || backslashed) {
+					currentWord.append(c);
+					backslashed = false;
+				} else {
+					if ((currentWord.length() > 0) || !wordEmpty) {
+						words.add(currentWord.toString());
+						currentWord.setLength(0);
+						wordEmpty = true;
+					}
+				}
+			} else {
+				if (backslashed && (c == 'n')) {
+					currentWord.append('\n');
+				} else {
+					currentWord.append(c);
+				}
+				backslashed = false;
+			}
+		}
+		if (inSingleQuote || inDoubleQuote || backslashed) {
+			throw new TextException("open quote");
+		}
+		if (currentWord.length() > 0) {
+			words.add(currentWord.toString());
+		}
+		return words;
+	}
+
+	/**
+	 * Escapes the given word in a way that {@link #parseLine(String)} can
+	 * correctly unescape it. The following rules are applied to the word:
+	 * <ul>
+	 * <li>If the word is the empty string, it is surrounded by double quotes.</li>
+	 * <li>If the word does not contain single quotes, double quotes,
+	 * backslashes, or space characters, it is returned as is.</li>
+	 * <li>If the word contains space characters and single quotes but none of
+	 * the other characters, it is surrounded by double quotes.</li>
+	 * <li>If the word contains space characters and double quotes and
+	 * backslashes, it is surrounded by single quotes.</li>
+	 * <li>Otherwise single quotes, double quotes, backslashes and space
+	 * characters are escaped by prefixing them with a backslash.</li>
+	 * </ul>
+	 *
+	 * @param word
+	 *            The word to escape
+	 * @return The escaped word
+	 */
+	public static String escapeWord(String word) {
+		if (word == null) {
+			return "";
+		}
+		if (word.length() == 0) {
+			return "\"\"";
+		}
+		boolean containsSingleQuote = word.indexOf('\'') != -1;
+		boolean containsDoubleQuote = word.indexOf('"') != -1;
+		boolean containsBackslash = word.indexOf('\\') != -1;
+		boolean containsSpace = word.indexOf(' ') != -1;
+		boolean containsLineBreak = word.indexOf('\n') != -1;
+		if (!containsSingleQuote && !containsDoubleQuote && !containsBackslash && !containsSpace) {
+			return word.replace("\n", "\\n");
+		}
+		if (!containsDoubleQuote && !containsBackslash) {
+			return "\"" + word.replace("\n", "\\n") + "\"";
+		}
+		if (!containsSingleQuote && !containsLineBreak) {
+			return "'" + word.replace("\n", "\\n") + "'";
+		}
+		return word.replace("\\", "\\\\").replace(" ", "\\ ").replace("\"", "\\\"").replace("'", "\\'").replace("\n", "\\n");
+	}
+
+	/**
+	 * Escapes all words in the given collection and joins them separated by a
+	 * space.
+	 *
+	 * @param words
+	 *            The words to escape
+	 * @return A line with all escaped word separated by a space
+	 */
+	public static String escapeWords(Collection<String> words) {
+		StringBuilder wordBuilder = new StringBuilder();
+		for (String word : words) {
+			if (wordBuilder.length() > 0) {
+				wordBuilder.append(' ');
+			}
+			wordBuilder.append(escapeWord(word));
+		}
+		return wordBuilder.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/text/TextException.java b/alien/src/net/pterodactylus/util/text/TextException.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/text/TextException.java
@@ -0,0 +1,69 @@
+/*
+ * utils - TextException.java - Copyright © 2008-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.text;
+
+import java.text.ParseException;
+
+/**
+ * Exception that signals an error when processing texts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TextException extends ParseException {
+
+	/**
+	 * Creates a new text exception.
+	 */
+	public TextException() {
+		this("");
+	}
+
+	/**
+	 * Creates a new text exception with the given message.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 */
+	public TextException(String message) {
+		super(message, -1);
+	}
+
+	/**
+	 * Creates a new text exception with the given cause.
+	 *
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public TextException(Throwable cause) {
+		this("", cause);
+	}
+
+	/**
+	 * Creates a new text exception with the given message and cause.
+	 *
+	 * @param message
+	 *            The message of the exception
+	 * @param cause
+	 *            The cause of the exception
+	 */
+	public TextException(String message, Throwable cause) {
+		this(message);
+		initCause(cause);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/thread/CurrentThreadExecutor.java b/alien/src/net/pterodactylus/util/thread/CurrentThreadExecutor.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/thread/CurrentThreadExecutor.java
@@ -0,0 +1,39 @@
+/*
+ * utils - CurrentThreadExecutor.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.thread;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An {@link Executor} that executes {@link Runnable}s in the current thread.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CurrentThreadExecutor implements Executor {
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.util.concurrent.Executor#execute(java.lang.Runnable)
+	 */
+	@Override
+	public void execute(Runnable command) {
+		command.run();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/thread/DumpingThread.java b/alien/src/net/pterodactylus/util/thread/DumpingThread.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/thread/DumpingThread.java
@@ -0,0 +1,114 @@
+/*
+ * utils - DumpingThread.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.thread;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Wrapper around {@link Thread} that catches throws exceptions and dumps them
+ * to the logfile.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DumpingThread extends Thread {
+
+	/** Logger. */
+	private static final Logger logger = Logging.getLogger(DumpingThread.class.getName());
+
+	/** Thread counter. */
+	private static int counter = 0;
+
+	/** Whether to log thread start and end. */
+	private boolean logThreadStartAndEnd;
+
+	/**
+	 * Creates a new dumping thread that executes the given {@link Runnable}.
+	 *
+	 * @param runnable
+	 *            The {@link Runnable} to execute
+	 */
+	public DumpingThread(Runnable runnable) {
+		this(runnable, "DumpingThread-" + counter++);
+	}
+
+	/**
+	 * Creates a new dumping thread that executes the given {@link Runnable} in
+	 * a thread with the given name.
+	 *
+	 * @param runnable
+	 *            The {@link Runnable} to execute
+	 * @param name
+	 *            The name of the thread
+	 */
+	public DumpingThread(Runnable runnable, String name) {
+		this(runnable, name, false);
+	}
+
+	/**
+	 * Creates a new dumping thread that executes the given {@link Runnable} in
+	 * a thread with the given name.
+	 *
+	 * @param runnable
+	 *            The {@link Runnable} to execute
+	 * @param name
+	 *            The name of the thread
+	 * @param logThreadStartAndEnd
+	 *            <code>true</code> if the thread should log its start and end
+	 */
+	public DumpingThread(Runnable runnable, String name, boolean logThreadStartAndEnd) {
+		super(runnable, name);
+		this.logThreadStartAndEnd = logThreadStartAndEnd;
+	}
+
+	/**
+	 * Sets whether the started thread should logs its start and end.
+	 *
+	 * @param logThreadStartAndEnd
+	 *            <code>true</code> if the thread should log its start and end,
+	 *            <code>false</code> otherwise
+	 */
+	public void setLogThreadStartAndEnd(boolean logThreadStartAndEnd) {
+		this.logThreadStartAndEnd = logThreadStartAndEnd;
+	}
+
+	/**
+	 * Executes the runnable in a try-catch block and dumps the thrown exception
+	 * to the {@link #logger}.
+	 *
+	 * @see java.lang.Runnable#run()
+	 */
+	@Override
+	public void run() {
+		if (logThreadStartAndEnd) {
+			logger.log(Level.INFO, "thread starting");
+		}
+		try {
+			super.run();
+		} catch (Throwable t) {
+			logger.log(Level.SEVERE, "***** THREAD EXITED UNEXPECTEDLY! *****", t);
+			throw new RuntimeException(t);
+		}
+		if (logThreadStartAndEnd) {
+			logger.log(Level.INFO, "thread exited.");
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/thread/DumpingThreadFactory.java b/alien/src/net/pterodactylus/util/thread/DumpingThreadFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/thread/DumpingThreadFactory.java
@@ -0,0 +1,103 @@
+/*
+ * utils - DumpingThreadFactory.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.thread;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * {@link ThreadFactory} implementation that creates {@link DumpingThread}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DumpingThreadFactory implements ThreadFactory {
+
+	/** The counter for thread factories. */
+	private static final AtomicInteger poolNumber = new AtomicInteger(0);
+
+	/** The counter for threads. */
+	private final AtomicInteger threadNumber = new AtomicInteger(0);
+
+	/** The name prefix. */
+	private final String namePrefix;
+
+	/** Whether to create daemon threads. */
+	private final boolean createDaemonThreads;
+
+	/**
+	 * Creates a new DumpingThread factory.
+	 */
+	public DumpingThreadFactory() {
+		this("DumpingThreadFactory-" + poolNumber.getAndIncrement() + "-Thread-");
+	}
+
+	/**
+	 * Creates a new dumping thread factory that uses the given name prefix and
+	 * a running counter appended to it as names for the created threads.
+	 *
+	 * @param namePrefix
+	 *            The name prefix, preferrable ending in a hyphen (
+	 *            <code>‘-’</code>) or space (<code>‘ ’</code>)
+	 */
+	public DumpingThreadFactory(String namePrefix) {
+		this(namePrefix, true);
+	}
+
+	/**
+	 * Creates a new DumpingThread factory.
+	 *
+	 * @param createDaemonThreads
+	 *            <code>true</code> to create daemon threads
+	 */
+	public DumpingThreadFactory(boolean createDaemonThreads) {
+		this("DumpingThreadFactory-" + poolNumber.getAndIncrement() + "-Thread-", createDaemonThreads);
+	}
+
+	/**
+	 * Creates a new dumping thread factory that uses the given name prefix and
+	 * a running counter appended to it as names for the created threads.
+	 *
+	 * @param namePrefix
+	 *            The name prefix, preferrable ending in a hyphen (
+	 *            <code>‘-’</code>) or space (<code>‘ ’</code>)
+	 * @param createDaemonThreads
+	 *            <code>true</code> to create daemon threads
+	 */
+	public DumpingThreadFactory(String namePrefix, boolean createDaemonThreads) {
+		this.namePrefix = namePrefix;
+		this.createDaemonThreads = createDaemonThreads;
+	}
+
+	/**
+	 * Creates a new {@link DumpingThread} that will execute the given runnable.
+	 *
+	 * @param r
+	 *            The runnable to execute in a new {@link DumpingThread}
+	 * @return The constructed {@link DumpingThread} that will execute the given
+	 *         runnable
+	 * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
+	 */
+	@Override
+	public Thread newThread(Runnable r) {
+		Thread thread = new DumpingThread(r);
+		thread.setDaemon(createDaemonThreads);
+		thread.setName(namePrefix + threadNumber.getAndIncrement());
+		return thread;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/thread/ObjectWrapper.java b/alien/src/net/pterodactylus/util/thread/ObjectWrapper.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/thread/ObjectWrapper.java
@@ -0,0 +1,94 @@
+/*
+ * utils - ObjectWrapper.java - Copyright © 2009-2010 David Roden
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package net.pterodactylus.util.thread;
+
+/**
+ * Wrapper around an object that can be set and retrieved. Its primary use is as
+ * a container for return values from anonymous classes.
+ *
+ * <pre>
+ * final ObjectWrapper<Object> objectWrapper = new ObjectWrapper<Object>();
+ * new Runnable() {
+ *     public void run() {
+ *         ...
+ *         objectWrapper.set(someResult);
+ *     }
+ * }.run();
+ * Object result = objectWrapper.get();
+ * </pre>
+ *
+ * @param <T>
+ *            The type of the wrapped object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ObjectWrapper<T> {
+
+	/** Object used for synchronization. */
+	private final Object syncObject = new Object();
+
+	/** The wrapped object. */
+	private T wrappedObject;
+
+	/** Whether the wrapped object has been set. */
+	private boolean set;
+
+	/**
+	 * Returns the wrapped object.
+	 *
+	 * @return The wrapped object
+	 */
+	public T get() {
+		synchronized (syncObject) {
+			while (!set) {
+				try {
+					syncObject.wait();
+				} catch (InterruptedException ie1) {
+					/* ignore. */
+				}
+			}
+			return wrappedObject;
+		}
+	}
+
+	/**
+	 * Returns whether the value has been set.
+	 *
+	 * @return {@code true} if the value was set, {@code false} otherwise
+	 */
+	public boolean isSet() {
+		synchronized (syncObject) {
+			return set;
+		}
+	}
+
+	/**
+	 * Sets the wrapped object.
+	 *
+	 * @param wrappedObject
+	 *            The wrapped object
+	 */
+	public void set(T wrappedObject) {
+		synchronized (syncObject) {
+			this.wrappedObject = wrappedObject;
+			set = true;
+			syncObject.notifyAll();
+		}
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/thread/Ticker.java b/alien/src/net/pterodactylus/util/thread/Ticker.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/thread/Ticker.java
@@ -0,0 +1,333 @@
+/*
+ * utils - Ticker.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.thread;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Executes threads at specified times in the future.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Ticker implements Runnable {
+
+	/** Logger. */
+	private static final Logger logger = Logging.getLogger(Ticker.class.getName());
+
+	/** A global instance. */
+	private static final Ticker globalInstance = new Ticker();
+
+	/** Counter for nameless events. */
+	private static int counter = 0;
+
+	/** Thread factory for new threads. */
+	private ThreadFactory threadFactory;
+
+	/** Synchronization object. */
+	private final Object syncObject = new Object();
+
+	/** Sorted list of execution times. */
+	private final Queue<EventIdentifier> executionTimes = new PriorityBlockingQueue<EventIdentifier>();
+
+	/** Mappings from execution time to runnables. */
+	private final Map<EventIdentifier, Runnable> runnables = Collections.synchronizedMap(new HashMap<EventIdentifier, Runnable>());
+
+	/** Whether the ticker thread is running. */
+	private boolean running = false;
+
+	/**
+	 * Creates a new ticker with a default thread factory (which uses
+	 * {@link DumpingThread}s).
+	 */
+	public Ticker() {
+		this(new DumpingThreadFactory());
+	}
+
+	/**
+	 * Creates a new ticker that uses the given thread factory to create new
+	 * threads.
+	 *
+	 * @param threadFactory
+	 *            The thread factory to use for new threads
+	 */
+	public Ticker(ThreadFactory threadFactory) {
+		this.threadFactory = threadFactory;
+	}
+
+	/**
+	 * Returns the global ticker instance.
+	 *
+	 * @return The global ticker instance
+	 */
+	public static Ticker getInstance() {
+		return globalInstance;
+	}
+
+	/**
+	 * Registers an unnamed thread that is executed at the specified time.
+	 *
+	 * @deprecated Use {@link #registerEvent(long, Runnable, String)} instead.
+	 * @param executionTime
+	 *            Time of execution (in milliseconds since the epoch)
+	 * @param thread
+	 *            The thread to execute
+	 * @return An object that identifies the created ticker event
+	 */
+	@Deprecated
+	public Object registerEvent(long executionTime, Runnable thread) {
+		synchronized (syncObject) {
+			return registerEvent(executionTime, thread, "Event-" + counter++);
+		}
+	}
+
+	/**
+	 * Registers a named thread that is executed at the specified time.
+	 *
+	 * @param executionTime
+	 *            Time of execution (in milliseconds since the epoch)
+	 * @param thread
+	 *            The thread to execute
+	 * @param eventName
+	 *            The name of the event
+	 * @return An object that identifies the created ticker event
+	 */
+	public Object registerEvent(long executionTime, Runnable thread, String eventName) {
+		synchronized (syncObject) {
+			logger.log(Level.INFO, "Ticker registered " + eventName + " at " + executionTime + ".");
+			EventIdentifier identifierObject = new EventIdentifier(executionTime, eventName);
+			runnables.put(identifierObject, thread);
+			executionTimes.add(identifierObject);
+			if (!running) {
+				running = true;
+				Thread tickerThread = threadFactory.newThread(this);
+				tickerThread.setName("Ticker Thread");
+				tickerThread.start();
+			} else {
+				syncObject.notify();
+			}
+			return identifierObject;
+		}
+	}
+
+	/**
+	 * Changes the execution time of the thread identified by the given object.
+	 *
+	 * @param identifierObject
+	 *            The object that identifies the ticker object to change
+	 * @param newExecutionTime
+	 *            The new execution time for the thread
+	 */
+	public void changeExecutionTime(Object identifierObject, long newExecutionTime) {
+		if (!(identifierObject instanceof EventIdentifier)) {
+			return;
+		}
+		EventIdentifier eventIdentifier = (EventIdentifier) identifierObject;
+		synchronized (syncObject) {
+			executionTimes.remove(eventIdentifier);
+			eventIdentifier.setExecutionTime(newExecutionTime);
+			executionTimes.add(eventIdentifier);
+			syncObject.notify();
+		}
+	}
+
+	/**
+	 * Removes the event identified by the given identifier. The
+	 * <code>eventIdentifier</code> is an object that was returned by a previous
+	 * call to {@link #registerEvent(long, Runnable)}.
+	 *
+	 * @param eventIdentifier
+	 *            The identifier of the event to remove
+	 */
+	public void deregisterEvent(Object eventIdentifier) {
+		if (!(eventIdentifier instanceof EventIdentifier)) {
+			return;
+		}
+		synchronized (syncObject) {
+			logger.log(Level.INFO, "Ticker removes event " + ((EventIdentifier) eventIdentifier).getEventName() + " at " + ((EventIdentifier) eventIdentifier).getExecutionTime() + ".");
+			runnables.remove(eventIdentifier);
+			removeEventIdentifier((EventIdentifier) eventIdentifier);
+			syncObject.notify();
+		}
+	}
+
+	/**
+	 * Removes the given event identifier from the {@link #executionTimes}
+	 * queue. This method had to be created because the {@link PriorityQueue}
+	 * reimplements {@link Collection#remove(Object)} to search for objects that
+	 * have the same natural order which might simply delete the wrong object if
+	 * two event identifiers have the same execution time.
+	 *
+	 * @param eventIdentifier
+	 *            The event identifier to remove
+	 * @return <code>true</code> if the event identifier was removed,
+	 *         <code>false</code> otherwise
+	 */
+	private boolean removeEventIdentifier(EventIdentifier eventIdentifier) {
+		Iterator<EventIdentifier> iterator = executionTimes.iterator();
+		while (iterator.hasNext()) {
+			if (iterator.next().equals(eventIdentifier)) {
+				iterator.remove();
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Stops the ticker. No further threads will be run.
+	 */
+	public void stop() {
+		synchronized (syncObject) {
+			running = false;
+			syncObject.notify();
+		}
+	}
+
+	/**
+	 * Main ticker thread.
+	 */
+	@Override
+	public void run() {
+		logger.log(Level.INFO, "Ticker started.");
+		synchronized (syncObject) {
+			while (running) {
+				if (executionTimes.isEmpty()) {
+					logger.log(Level.INFO, "Ticker is waiting for events.");
+					try {
+						syncObject.wait();
+					} catch (InterruptedException ie1) {
+						/*
+						 * ignore, ticker will land here again if there's
+						 * nothing to do.
+						 */
+					}
+				} else {
+					EventIdentifier eventIdentifier = executionTimes.peek();
+					if (eventIdentifier == null) {
+						continue;
+					}
+					long now = System.currentTimeMillis();
+					long executionTime = eventIdentifier.getExecutionTime();
+					if (executionTime > now) {
+						logger.log(Level.INFO, "Ticker is waiting up to " + (executionTime - now) + " for " + eventIdentifier.getEventName() + " to execute at " + executionTime + ".");
+						try {
+							syncObject.wait(executionTime - now);
+						} catch (InterruptedException ie1) {
+							/*
+							 * ignore, ticker will land here again if there's
+							 * nothing to do.
+							 */
+						}
+					} else {
+						removeEventIdentifier(eventIdentifier);
+						Runnable runnable = runnables.remove(eventIdentifier);
+						if (runnable != null) {
+							logger.log(Level.INFO, "Ticker executes " + eventIdentifier.getEventName() + ", " + (now - executionTime) + " ms late");
+							Thread eventThread = threadFactory.newThread(runnable);
+							eventThread.setName("Event Thread for " + eventIdentifier.getEventName() + " @ " + executionTime);
+							eventThread.start();
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Identifier objects for events to circumvent the
+	 * <em>consistent with equals</em> logic used by the Java Collections
+	 * architecture that gives problem when using normals Long objects as
+	 * identifiers.
+	 *
+	 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+	 */
+	private static class EventIdentifier implements Comparable<EventIdentifier> {
+
+		/** The execution time. */
+		private long executionTime;
+
+		/** The name of the event. */
+		private final String eventName;
+
+		/**
+		 * Constructs a new event identifier for an event at the given execution
+		 * time.
+		 *
+		 * @param executionTime
+		 *            The execution time of the event
+		 * @param eventName
+		 *            The name of the event
+		 */
+		public EventIdentifier(long executionTime, String eventName) {
+			this.executionTime = executionTime;
+			this.eventName = eventName;
+		}
+
+		/**
+		 * Sets the new execution time of this event identifier.
+		 *
+		 * @param newExecutionTime
+		 *            The new execution time
+		 */
+		public void setExecutionTime(long newExecutionTime) {
+			executionTime = newExecutionTime;
+		}
+
+		/**
+		 * Returns the execution time of the event.
+		 *
+		 * @return The execution time of the event
+		 */
+		public long getExecutionTime() {
+			return executionTime;
+		}
+
+		/**
+		 * Returns the name of the event.
+		 *
+		 * @return The name of the event.
+		 */
+		public String getEventName() {
+			return eventName;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 *
+		 * @see java.lang.Comparable#compareTo(java.lang.Object)
+		 */
+		@Override
+		public int compareTo(EventIdentifier o) {
+			return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, executionTime - o.executionTime));
+		}
+
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/time/Duration.java b/alien/src/net/pterodactylus/util/time/Duration.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/time/Duration.java
@@ -0,0 +1,173 @@
+/*
+ * utils - Duration.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.time;
+
+/**
+ * A duration is the length between two events in time.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Duration {
+
+	/** This duration in milliseconds. */
+	private final long duration;
+
+	/** The number of whole weeks in this duration. */
+	private final long weeks;
+
+	/** The number of days in the last week of this duration. */
+	private final int days;
+
+	/** The number of hours in the last day of this duration. */
+	private final int hours;
+
+	/** The number of minutes in the last hour of this duration. */
+	private final int minutes;
+
+	/** The number of seconds in the last minute of this duration. */
+	private final int seconds;
+
+	/** The number of milliseconds in the last second of this duration. */
+	private final int milliseconds;
+
+	/**
+	 * Creates a new duration with the specified length.
+	 *
+	 * @param duration
+	 *            The length of this duration in milliseconds
+	 */
+	public Duration(long duration) {
+		this.duration = duration;
+		milliseconds = (int) (duration % 1000);
+		seconds = (int) ((duration / 1000) % 60);
+		minutes = (int) ((duration / (1000 * 60)) % 60);
+		hours = (int) ((duration / (1000 * 60 * 60)) % 24);
+		days = (int) ((duration / (1000 * 60 * 60 * 24)) % 7);
+		weeks = duration / (1000 * 60 * 60 * 24 * 7);
+	}
+
+	/**
+	 * Returns the number of days in the last week of this duration
+	 *
+	 * @return The number of days
+	 */
+	public int getDays() {
+		return days;
+	}
+
+	/**
+	 * Returns the length of this duration.
+	 *
+	 * @return The length of this duration (in milliseconds)
+	 */
+	public long getDuration() {
+		return duration;
+	}
+
+	/**
+	 * Returns the number of hours in the last day of this duration.
+	 *
+	 * @return The number of hours
+	 */
+	public int getHours() {
+		return hours;
+	}
+
+	/**
+	 * Returns the number of milliseconds in the last second of this duration.
+	 *
+	 * @return The number of milliseconds
+	 */
+	public int getMilliseconds() {
+		return milliseconds;
+	}
+
+	/**
+	 * Returns the number of minutes in the last hour of this duration.
+	 *
+	 * @return The number of minutes
+	 */
+	public int getMinutes() {
+		return minutes;
+	}
+
+	/**
+	 * Returns the number of seconds in the last minute of this duration.
+	 *
+	 * @return The number of seconds
+	 */
+	public int getSeconds() {
+		return seconds;
+	}
+
+	/**
+	 * Returns the number of whole weeks in this duration.
+	 *
+	 * @return The number of weeks
+	 */
+	public long getWeeks() {
+		return weeks;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return toString(true);
+	}
+
+	/**
+	 * Returns a textual representation of this duration.
+	 *
+	 * @param showMilliseconds
+	 *            <code>true</code> if the milliseconds should be shown,
+	 *            <code>false</code> otherwise
+	 * @return The textual representation of this duration
+	 */
+	public String toString(boolean showMilliseconds) {
+		StringBuilder durationBuilder = new StringBuilder();
+		if ((milliseconds != 0) && showMilliseconds) {
+			int ms = milliseconds;
+			while ((ms % 10) == 0) {
+				ms /= 10;
+			}
+			durationBuilder.append(seconds).append('.').append(ms).append('s');
+		} else if (seconds != 0) {
+			durationBuilder.append(seconds).append('s');
+		} else if ((minutes == 0) && (hours == 0) && (days == 0) && (weeks == 0)) {
+			durationBuilder.append("0s");
+		}
+		if (minutes != 0) {
+			durationBuilder.insert(0, "m ").insert(0, minutes);
+		}
+		if (hours != 0) {
+			durationBuilder.insert(0, "h ").insert(0, hours);
+		}
+		if (days != 0) {
+			durationBuilder.insert(0, "d ").insert(0, days);
+		}
+		if (weeks != 0) {
+			durationBuilder.insert(0, "w ").insert(0, weeks);
+		}
+		return durationBuilder.toString();
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/validation/Validation.java b/alien/src/net/pterodactylus/util/validation/Validation.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/validation/Validation.java
@@ -0,0 +1,505 @@
+/*
+ * utils - Validation.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.pterodactylus.util.validation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * <p>
+ * Helps with parameter validation. Parameters can be checked using a construct
+ * like this:
+ * </p>
+ * <code><pre>
+ * public void copy(Object[] object, int leftValue, int ríghtValue) {
+ *     Validation.begin().isNotNull(object, "object").check()
+ *         .isPositive(leftValue, "leftValue").isLess(leftValue, object.length, "leftValue").check()
+ *         .isPositive(rightValue, "rightValue").isLess(rightValue, object.length, "rightValue").isGreater(rightValue, leftValue, "rightValue").check();
+ *     // do something with the values
+ * }
+ * </pre></code>
+ * <p>
+ * This example will perform several checks. Only the {@link #check()} method
+ * will throw an {@link IllegalArgumentException} if one of the previous checks
+ * failed, so you can gather several reasons for a validation failure before
+ * throwing an exception which will in turn decrease the time spent in
+ * debugging.
+ * </p>
+ * <p>
+ * In the example, <code>object</code> is first checked for a non-
+ * <code>null</code> value and an {@link IllegalArgumentException} is thrown if
+ * <code>object</code> is <code>null</code>. Afterwards <code>leftValue</code>
+ * is checked for being a positive value that is also smaller than the length of
+ * the array <code>object</code>. The {@link IllegalArgumentException} that is
+ * thrown if the checks failed will contain a message for each of the failed
+ * checks. At last <code>rightValue</code> is checked for being positive,
+ * smaller than the array’s length and larger than <code>leftValue</code>.
+ * </p>
+ * <p>
+ * Remember to call the {@link #check()} method after performing the checks,
+ * otherwise the {@link IllegalArgumentException} will never be thrown!
+ * </p>
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Validation {
+
+	/** The list of failed checks. */
+	private List<String> failedChecks;
+
+	/**
+	 * Private constructor to prevent construction from the outside.
+	 */
+	private Validation() {
+		/* do nothing. */
+	}
+
+	/**
+	 * Adds a check to the list of failed checks, instantiating a new list if
+	 * {@link #failedChecks} is still <code>null</code>.
+	 *
+	 * @param check
+	 *            The check to add
+	 */
+	private void addFailedCheck(String check) {
+		if (failedChecks == null) {
+			failedChecks = new ArrayList<String>();
+		}
+		failedChecks.add(check);
+	}
+
+	/**
+	 * Returns a new {@link Validation} object.
+	 *
+	 * @return A new validation
+	 */
+	public static Validation begin() {
+		return new Validation();
+	}
+
+	/**
+	 * Checks if one of the previous checks failed and throws an
+	 * {@link IllegalArgumentException} if a previous check did fail.
+	 *
+	 * @return This {@link Validation} object to allow method chaining
+	 * @throws IllegalArgumentException
+	 *             if a previous check failed
+	 */
+	public Validation check() throws IllegalArgumentException {
+		if (failedChecks == null) {
+			return this;
+		}
+		StringBuilder message = new StringBuilder();
+		message.append("Failed checks: ");
+		for (String failedCheck : failedChecks) {
+			message.append(failedCheck).append(", ");
+		}
+		message.setLength(message.length() - 2);
+		message.append('.');
+		throw new IllegalArgumentException(message.toString());
+	}
+
+	/**
+	 * Checks if the given object is not <code>null</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param object
+	 *            The object to check
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNotNull(String objectName, Object object) {
+		if (object == null) {
+			addFailedCheck(objectName + " should not be null");
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is less than <code>upperBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param upperBound
+	 *            The upper bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isLess(String objectName, double value, double upperBound) {
+		if (value >= upperBound) {
+			addFailedCheck(objectName + " should be < " + upperBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is less than <code>upperBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param upperBound
+	 *            The upper bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isLessOrEqual(String objectName, long value, long upperBound) {
+		if (value > upperBound) {
+			addFailedCheck(objectName + " should be <= " + upperBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is less than <code>upperBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param upperBound
+	 *            The upper bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isLessOrEqual(String objectName, double value, double upperBound) {
+		if (value > upperBound) {
+			addFailedCheck(objectName + " should be <= " + upperBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isEqual(String objectName, long value, long expected) {
+		if (value != expected) {
+			addFailedCheck(objectName + " should be == " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isEqual(String objectName, double value, double expected) {
+		if (value != expected) {
+			addFailedCheck(objectName + " should be == " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isEqual(String objectName, boolean value, boolean expected) {
+		if (value != expected) {
+			addFailedCheck(objectName + " should be == " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isEqual(String objectName, Object value, Object expected) {
+		if (!value.equals(expected)) {
+			addFailedCheck(objectName + " should equal " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is the same as the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isSame(String objectName, Object value, Object expected) {
+		if (value != expected) {
+			addFailedCheck(objectName + " should be == " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is not equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNotEqual(String objectName, long value, long expected) {
+		if (value == expected) {
+			addFailedCheck(objectName + " should be != " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is not equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNotEqual(String objectName, double value, double expected) {
+		if (value == expected) {
+			addFailedCheck(objectName + " should be != " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is not equal to the expected value.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param expected
+	 *            The expected value to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNotEqual(String objectName, boolean value, boolean expected) {
+		if (value == expected) {
+			addFailedCheck(objectName + " should be != " + expected + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is greater than <code>lowerBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param lowerBound
+	 *            The lower bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isGreater(String objectName, long value, long lowerBound) {
+		if (value <= lowerBound) {
+			addFailedCheck(objectName + " should be > " + lowerBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is greater than <code>lowerBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param lowerBound
+	 *            The lower bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isGreater(String objectName, double value, double lowerBound) {
+		if (value <= lowerBound) {
+			addFailedCheck(objectName + " should be > " + lowerBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is greater than <code>lowerBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param lowerBound
+	 *            The lower bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isGreaterOrEqual(String objectName, long value, long lowerBound) {
+		if (value < lowerBound) {
+			addFailedCheck(objectName + " should be >= " + lowerBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if <code>value</code> is greater than <code>lowerBound</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @param lowerBound
+	 *            The lower bound to check <code>value</code> against
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isGreaterOrEqual(String objectName, double value, double lowerBound) {
+		if (value < lowerBound) {
+			addFailedCheck(objectName + " should be >= " + lowerBound + " but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is greater to or equal to <code>0</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isPositive(String objectName, long value) {
+		if (value < 0) {
+			addFailedCheck(objectName + " should be >= 0 but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is greater to or equal to <code>0</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isPositive(String objectName, double value) {
+		if (value < 0) {
+			addFailedCheck(objectName + " should be >= 0 but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is less than <code>0</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNegative(String objectName, long value) {
+		if (value >= 0) {
+			addFailedCheck(objectName + " should be < 0 but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks if the given value is less than <code>0</code>.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The value to check
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isNegative(String objectName, double value) {
+		if (value >= 0) {
+			addFailedCheck(objectName + " should be < 0 but was " + value);
+		}
+		return this;
+	}
+
+	/**
+	 * Checks whether the given object is assignable to an object of the given
+	 * class.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param object
+	 *            The object to check
+	 * @param clazz
+	 *            The class the object should be representable as
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isInstanceOf(String objectName, Object object, Class<?> clazz) {
+		if (!object.getClass().isAssignableFrom(clazz)) {
+			addFailedCheck(objectName + " should be a kind of " + clazz.getName() + " but is " + object.getClass().getName());
+		}
+		return this;
+	}
+
+	/**
+	 * Checks whether the given value is one of the expected values.
+	 *
+	 * @param objectName
+	 *            The object’s name
+	 * @param value
+	 *            The object’s value
+	 * @param expectedValues
+	 *            The expected values
+	 * @return This {@link Validation} object to allow method chaining
+	 */
+	public Validation isOneOf(String objectName, Object value, Object... expectedValues) {
+		List<?> values;
+		if (!(values = Arrays.asList(expectedValues)).contains(value)) {
+			addFailedCheck(objectName + " should be one of " + values + " but is " + value);
+		}
+		return this;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/version/Version.java b/alien/src/net/pterodactylus/util/version/Version.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/version/Version.java
@@ -0,0 +1,182 @@
+/*
+ * utils - Version.java - Copyright © 2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.version;
+
+import java.util.Arrays;
+
+/**
+ * Version number container.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Version implements Comparable<Version> {
+
+	/** The version numbers. */
+	private final int[] numbers;
+
+	/** An optional appendix. */
+	private final String appendix;
+
+	/**
+	 * Creates a new version with the given numbers.
+	 *
+	 * @param numbers
+	 *            The numbers of the version
+	 */
+	public Version(int... numbers) {
+		this(null, numbers);
+	}
+
+	/**
+	 * Creates a new version with the given numbers.
+	 *
+	 * @param appendix
+	 *            The optional appendix
+	 * @param numbers
+	 *            The numbers of the version
+	 */
+	public Version(String appendix, int... numbers) {
+		this.numbers = new int[numbers.length];
+		System.arraycopy(numbers, 0, this.numbers, 0, numbers.length);
+		this.appendix = appendix;
+	}
+
+	/**
+	 * Returns the number at the given index.
+	 *
+	 * @param index
+	 *            The index of the number
+	 * @return The number, or <code>0</code> if there is no number at the given
+	 *         index
+	 */
+	public int getNumber(int index) {
+		if (index >= numbers.length) {
+			return 0;
+		}
+		return numbers[index];
+	}
+
+	/**
+	 * Returns the first number of the version.
+	 *
+	 * @return The first number of the version
+	 */
+	public int getMajor() {
+		return getNumber(0);
+	}
+
+	/**
+	 * Returns the second number of the version.
+	 *
+	 * @return The second number of the version
+	 */
+	public int getMinor() {
+		return getNumber(1);
+	}
+
+	/**
+	 * Returns the third number of the version.
+	 *
+	 * @return The third number of the version
+	 */
+	public int getRelease() {
+		return getNumber(2);
+	}
+
+	/**
+	 * Returns the fourth number of the version.
+	 *
+	 * @return The fourth number of the version
+	 */
+	public int getPatch() {
+		return getNumber(3);
+	}
+
+	/**
+	 * Returns the optional appendix.
+	 *
+	 * @return The appendix, or <code>null</code> if there is no appendix
+	 */
+	public String getAppendix() {
+		return appendix;
+	}
+
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		StringBuilder versionString = new StringBuilder();
+		for (int number : numbers) {
+			if (versionString.length() > 0) {
+				versionString.append('.');
+			}
+			versionString.append(number);
+		}
+		if (appendix != null) {
+			versionString.append('-').append(appendix);
+		}
+		return versionString.toString();
+	}
+
+	//
+	// INTERFACE Comparable<Version>
+	//
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.lang.Comparable#compareTo(java.lang.Object)
+	 */
+	@Override
+	public int compareTo(Version version) {
+		int lengthDiff = numbers.length - version.numbers.length;
+		for (int index = 0; index < Math.abs(lengthDiff); index++) {
+			int diff = numbers[index] - version.numbers[index];
+			if (diff != 0) {
+				return diff;
+			}
+		}
+		return lengthDiff;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.lang.Object#equals(java.lang.Object)
+	 */
+	@Override
+	public boolean equals(Object obj) {
+		if ((obj == null) || !(obj instanceof Version)) {
+			return false;
+		}
+		Version version = (Version) obj;
+		return Arrays.equals(numbers, version.numbers);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * @see java.lang.Object#hashCode()
+	 */
+	@Override
+	public int hashCode() {
+		return Arrays.hashCode(numbers);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/xml/DOMUtil.java b/alien/src/net/pterodactylus/util/xml/DOMUtil.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/xml/DOMUtil.java
@@ -0,0 +1,78 @@
+/*
+ * utils - DOMUtil.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.xml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Contains various helper methods that let you more easily work with
+ * {@code org.w3c.dom} objects.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DOMUtil {
+
+	/**
+	 * Returns the first child node of the given root node that has the given
+	 * name.
+	 *
+	 * @param rootNode
+	 *            The root node
+	 * @param childName
+	 *            The name of the child node
+	 * @return The child node, or {@code null} if the root node does not have a
+	 *         child node with the given name
+	 */
+	public static Node getChildNode(Node rootNode, String childName) {
+		NodeList nodeList = rootNode.getChildNodes();
+		for (int nodeIndex = 0, nodeSize = nodeList.getLength(); nodeIndex < nodeSize; nodeIndex++) {
+			Node childNode = nodeList.item(nodeIndex);
+			if (childNode.getNodeName().equals(childName)) {
+				return childNode;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns all child nodes of the given root node with the given name.
+	 *
+	 * @param rootNode
+	 *            The root node
+	 * @param childName
+	 *            The name of child nodes
+	 * @return All child nodes with the given name, or an empty array if there
+	 *         are no child nodes with the given name
+	 */
+	public static Node[] getChildNodes(Node rootNode, String childName) {
+		List<Node> nodes = new ArrayList<Node>();
+		NodeList nodeList = rootNode.getChildNodes();
+		for (int nodeIndex = 0, nodeSize = nodeList.getLength(); nodeIndex < nodeSize; nodeIndex++) {
+			Node childNode = nodeList.item(nodeIndex);
+			if (childNode.getNodeName().equals(childName)) {
+				nodes.add(childNode);
+			}
+		}
+		return nodes.toArray(new Node[nodes.size()]);
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/xml/SimpleXML.java b/alien/src/net/pterodactylus/util/xml/SimpleXML.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/xml/SimpleXML.java
@@ -0,0 +1,569 @@
+/*
+ * utils - SimpleXML.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.xml;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import net.pterodactylus.util.logging.Logging;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+
+/**
+ * SimpleXML is a helper class to construct XML trees in a fast and simple way.
+ * Construct a new XML tree by calling {@link #SimpleXML(String)} and append new
+ * nodes by calling {@link #append(String)}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SimpleXML {
+
+	/** Logger. */
+	private static final Logger logger = Logging.getLogger(SimpleXML.class.getName());
+
+	/**
+	 * A {@link List} containing all child nodes of this node.
+	 */
+	private List<SimpleXML> children = new ArrayList<SimpleXML>();
+
+	/**
+	 * The name of this node.
+	 */
+	private String name = null;
+
+	/**
+	 * The value of this node.
+	 */
+	private String value = null;
+
+	/** Attributes of the element. */
+	private Map<String, String> attributes = null;
+
+	/**
+	 * Constructs a new XML node without a name.
+	 */
+	public SimpleXML() {
+		super();
+	}
+
+	/**
+	 * Constructs a new XML node with the specified name.
+	 *
+	 * @param name
+	 *            The name of the new node
+	 */
+	public SimpleXML(String name) {
+		this(name, (String[]) null, (String[]) null);
+	}
+
+	/**
+	 * Constructs a new XML node with the specified name and a single attribute.
+	 *
+	 * @param name
+	 *            The name of the node
+	 * @param attributeName
+	 *            The name of the attribute
+	 * @param attributeValue
+	 *            The value of the attribute
+	 */
+	public SimpleXML(String name, String attributeName, String attributeValue) {
+		this(name, new String[] { attributeName }, new String[] { attributeValue });
+	}
+
+	/**
+	 * Constructs a new XML node with the specified name and attributes.
+	 *
+	 * @param name
+	 *            The name of the node
+	 * @param attributeNames
+	 *            The names of the attribute
+	 * @param attributeValues
+	 *            The values of the attribute
+	 */
+	public SimpleXML(String name, String[] attributeNames, String[] attributeValues) {
+		this.name = name;
+		attributes = new HashMap<String, String>();
+		if ((attributeNames != null) && (attributeValues != null) && (attributeNames.length == attributeValues.length)) {
+			for (int index = 0, size = attributeNames.length; index < size; index++) {
+				attributes.put(attributeNames[index], attributeValues[index]);
+			}
+		}
+	}
+
+	/**
+	 * Returns all attributes’ names. The array is not sorted.
+	 *
+	 * @return The names of all attributes
+	 */
+	public String[] getAttributeNames() {
+		return attributes.keySet().toArray(new String[attributes.size()]);
+	}
+
+	/**
+	 * Returns the value of the attribute with the given name.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute to look up
+	 * @return The value of the attribute
+	 */
+	public String getAttribute(String attributeName) {
+		return getAttribute(attributeName, null);
+	}
+
+	/**
+	 * Returns the value of the attribute with the given name.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute to look up
+	 * @param defaultValue
+	 *            The value to return if there is no attribute with the given
+	 *            name
+	 * @return The value of the attribute
+	 */
+	public String getAttribute(String attributeName, String defaultValue) {
+		if (!attributes.containsKey(attributeName)) {
+			return defaultValue;
+		}
+		return attributes.get(attributeName);
+	}
+
+	/**
+	 * Sets the value of an attribute.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute to set
+	 * @param attributeValue
+	 *            The value of the attribute
+	 */
+	public void setAttribute(String attributeName, String attributeValue) {
+		attributes.put(attributeName, attributeValue);
+	}
+
+	/**
+	 * Removes the attribute with the given name, returning its previous value.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute to remove
+	 * @return The value of the attribute before removing it
+	 */
+	public String removeAttribute(String attributeName) {
+		return attributes.remove(attributeName);
+	}
+
+	/**
+	 * Checks whether this node contains the attribute with the given name.
+	 *
+	 * @param attributeName
+	 *            The name of the attribute
+	 * @return <code>true</code> if this node has an attribute with the given
+	 *         name, <code>false</code> otherwise
+	 */
+	public boolean hasAttribute(String attributeName) {
+		return attributes.containsKey(attributeName);
+	}
+
+	/**
+	 * Returns whether this node has any child nodes.
+	 *
+	 * @return {@code true} if this node has child nodes, {@code false}
+	 *         otherwise
+	 */
+	public boolean hasNodes() {
+		return !children.isEmpty();
+	}
+
+	/**
+	 * Checks if this object has a child with the specified name.
+	 *
+	 * @param nodeName
+	 *            The name of the child node to check for
+	 * @return <code>true</code> if this node has at least one child with the
+	 *         specified name, <code>false</code> otherwise
+	 */
+	public boolean hasNode(String nodeName) {
+		return getNode(nodeName) != null;
+	}
+
+	/**
+	 * Returns the child node of this node with the specified name. If there are
+	 * several child nodes with the specified name only the first node is
+	 * returned.
+	 *
+	 * @param nodeName
+	 *            The name of the child node
+	 * @return The child node, or <code>null</code> if there is no child node
+	 *         with the specified name
+	 */
+	public SimpleXML getNode(String nodeName) {
+		for (int index = 0, count = children.size(); index < count; index++) {
+			if (children.get(index).name.equals(nodeName)) {
+				return children.get(index);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the child node that is specified by the names. The first element
+	 * of <code>nodeNames</code> is the name of the child node of this node, the
+	 * second element of <code>nodeNames</code> is the name of a child node's
+	 * child node, and so on. By using this method you can descend into an XML
+	 * tree pretty fast.
+	 *
+	 * <pre>
+	 * <code>
+	 * SimpleXML deepNode = topNode.getNodes(new String[] { "person", "address", "number" });
+	 * </code>
+	 * </pre>
+	 *
+	 * @param nodeNames
+	 *            The names of the nodes
+	 * @return A node that is a deep child of this node, or <code>null</code> if
+	 *         the specified node does not eixst
+	 */
+	public SimpleXML getNode(String[] nodeNames) {
+		SimpleXML node = this;
+		for (String nodeName : nodeNames) {
+			node = node.getNode(nodeName);
+		}
+		return node;
+	}
+
+	/**
+	 * Returns all child nodes of this node.
+	 *
+	 * @return All child nodes of this node
+	 */
+	public SimpleXML[] getNodes() {
+		return getNodes(null);
+	}
+
+	/**
+	 * Returns all child nodes of this node with the specified name. If there
+	 * are no child nodes with the specified name an empty array is returned.
+	 *
+	 * @param nodeName
+	 *            The name of the nodes to retrieve, or <code>null</code> to
+	 *            retrieve all nodes
+	 * @return All child nodes with the specified name
+	 */
+	public SimpleXML[] getNodes(String nodeName) {
+		List<SimpleXML> resultList = new ArrayList<SimpleXML>();
+		for (SimpleXML child : children) {
+			if ((nodeName == null) || child.name.equals(nodeName)) {
+				resultList.add(child);
+			}
+		}
+		return resultList.toArray(new SimpleXML[resultList.size()]);
+	}
+
+	/**
+	 * Appends a new XML node with the specified name and returns the new node.
+	 * With this method you can create deep structures very fast.
+	 *
+	 * <pre>
+	 * <code>
+	 * SimpleXML mouseNode = topNode.append("computer").append("bus").append("usb").append("mouse");
+	 * </code>
+	 * </pre>
+	 *
+	 * @param nodeName
+	 *            The name of the node to append as a child to this node
+	 * @return The new node
+	 */
+	public SimpleXML append(String nodeName) {
+		return append(new SimpleXML(nodeName));
+	}
+
+	/**
+	 * Appends a new XML node with the specified name and value and returns the
+	 * new node.
+	 *
+	 * @param nodeName
+	 *            The name of the node to append
+	 * @param nodeValue
+	 *            The value of the node to append
+	 * @return The newly appended node
+	 */
+	public SimpleXML append(String nodeName, String nodeValue) {
+		return append(nodeName).setValue(nodeValue);
+	}
+
+	/**
+	 * Appends the node with all its child nodes to this node and returns the
+	 * child node.
+	 *
+	 * @param newChild
+	 *            The node to append as a child
+	 * @return The child node that was appended
+	 */
+	public SimpleXML append(SimpleXML newChild) {
+		children.add(newChild);
+		return newChild;
+	}
+
+	/**
+	 * Removes the specified child from this node.
+	 *
+	 * @param child
+	 *            The child to remove
+	 */
+	public void remove(SimpleXML child) {
+		children.remove(child);
+	}
+
+	/**
+	 * Removes the child with the specified name from this node. If more than
+	 * one children have the same name only the first is removed.
+	 *
+	 * @param childName
+	 *            The name of the child node to remove
+	 */
+	public void remove(String childName) {
+		SimpleXML child = getNode(childName);
+		if (child != null) {
+			remove(child);
+		}
+	}
+
+	/**
+	 * Replace the child node with the specified name by a new node with the
+	 * specified content.
+	 *
+	 * @param childName
+	 *            The name of the child to replace
+	 * @param value
+	 *            The node child's value
+	 */
+	public void replace(String childName, String value) {
+		remove(childName);
+		append(childName, value);
+	}
+
+	/**
+	 * Replaces the child node that has the same name as the given node by the
+	 * given node.
+	 *
+	 * @param childNode
+	 *            The node to replace the previous child node with the same name
+	 */
+	public void replace(SimpleXML childNode) {
+		remove(childNode.getName());
+		append(childNode);
+	}
+
+	/**
+	 * Removes all children from this node.
+	 */
+	public void removeAll() {
+		children.clear();
+	}
+
+	/**
+	 * Sets the value of this node.
+	 *
+	 * @param nodeValue
+	 *            The new value of this node
+	 * @return This node
+	 */
+	public SimpleXML setValue(String nodeValue) {
+		value = nodeValue;
+		return this;
+	}
+
+	/**
+	 * Returns the name of this node.
+	 *
+	 * @return The name of this node
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Returns the value of this node.
+	 *
+	 * @return The value of this node
+	 */
+	public String getValue() {
+		return value;
+	}
+
+	/**
+	 * Returns the value of the first child node with the specified name.
+	 *
+	 * @param childName
+	 *            The name of the child node
+	 * @return The value of the child node
+	 * @throws NullPointerException
+	 *             if the child node does not exist
+	 */
+	public String getValue(String childName) {
+		return getNode(childName).getValue();
+	}
+
+	/**
+	 * Returns the value of the first child node with the specified name, or the
+	 * default value if there is no child node with the given name.
+	 *
+	 * @param childName
+	 *            The name of the child node
+	 * @param defaultValue
+	 *            The default value to return if there is no child node with the
+	 *            given name
+	 * @return The value of the child node
+	 * @throws NullPointerException
+	 *             if the child node does not exist
+	 */
+	public String getValue(String childName, String defaultValue) {
+		SimpleXML childNode = getNode(childName);
+		if (childNode == null) {
+			return defaultValue;
+		}
+		return childNode.getValue();
+	}
+
+	/**
+	 * Creates a {@link Document} from this node and all its child nodes.
+	 *
+	 * @return The {@link Document} created from this node
+	 */
+	public Document getDocument() {
+		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+		try {
+			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+			Document document = documentBuilder.newDocument();
+			Element rootElement = document.createElement(name);
+			for (Entry<String, String> attributeEntry : attributes.entrySet()) {
+				rootElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
+			}
+			document.appendChild(rootElement);
+			addChildren(rootElement);
+			return document;
+		} catch (ParserConfigurationException e) {
+			/* ignore. */
+		}
+		return null;
+	}
+
+	/**
+	 * Appends all children of this node to the specified {@link Element}. If a
+	 * node has a value that is not <code>null</code> the value is appended as a
+	 * text node.
+	 *
+	 * @param rootElement
+	 *            The element to attach this node's children to
+	 */
+	private void addChildren(Element rootElement) {
+		for (SimpleXML child : children) {
+			Element childElement = rootElement.getOwnerDocument().createElement(child.name);
+			for (Entry<String, String> attributeEntry : child.attributes.entrySet()) {
+				childElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
+			}
+			rootElement.appendChild(childElement);
+			if (child.value != null) {
+				Text childText = rootElement.getOwnerDocument().createTextNode(child.value);
+				childElement.appendChild(childText);
+			} else {
+				child.addChildren(childElement);
+			}
+		}
+	}
+
+	/**
+	 * Creates a SimpleXML node from the specified {@link Document}. The
+	 * SimpleXML node of the document's top-level node is returned.
+	 *
+	 * @param document
+	 *            The {@link Document} to create a SimpleXML node from
+	 * @return The SimpleXML node created from the document's top-level node
+	 */
+	public static SimpleXML fromDocument(Document document) {
+		SimpleXML xmlDocument = new SimpleXML(document.getFirstChild().getNodeName());
+		NamedNodeMap attributes = document.getFirstChild().getAttributes();
+		for (int attributeIndex = 0, attributeCount = attributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
+			Node attribute = attributes.item(attributeIndex);
+			logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
+			xmlDocument.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
+		}
+		document.normalizeDocument();
+		/* look for first non-comment node */
+		Node firstChild = null;
+		NodeList children = document.getChildNodes();
+		for (int index = 0, count = children.getLength(); index < count; index++) {
+			Node child = children.item(index);
+			if ((child.getNodeType() != Node.COMMENT_NODE) && (child.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE)) {
+				firstChild = child;
+				break;
+			}
+		}
+		return addDocumentChildren(xmlDocument, firstChild);
+	}
+
+	/**
+	 * Appends the child nodes of the specified {@link Document} to this node.
+	 * Text nodes are converted into a node's value.
+	 *
+	 * @param xmlDocument
+	 *            The SimpleXML node to append the child nodes to
+	 * @param document
+	 *            The document whose child nodes to append
+	 * @return The SimpleXML node the child nodes were appended to
+	 */
+	private static SimpleXML addDocumentChildren(SimpleXML xmlDocument, Node document) {
+		NodeList childNodes = document.getChildNodes();
+		for (int childIndex = 0, childCount = childNodes.getLength(); childIndex < childCount; childIndex++) {
+			Node childNode = childNodes.item(childIndex);
+			if ((childNode.getChildNodes().getLength() == 1) && (childNode.getFirstChild().getNodeName().equals("#text"))) {
+				SimpleXML newXML = xmlDocument.append(childNode.getNodeName(), childNode.getFirstChild().getNodeValue());
+				NamedNodeMap childNodeAttributes = childNode.getAttributes();
+				for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
+					Node attribute = childNodeAttributes.item(attributeIndex);
+					logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
+					newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
+				}
+			} else {
+				if ((childNode.getNodeType() == Node.ELEMENT_NODE) || (childNode.getChildNodes().getLength() != 0)) {
+					SimpleXML newXML = xmlDocument.append(childNode.getNodeName());
+					NamedNodeMap childNodeAttributes = childNode.getAttributes();
+					for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
+						Node attribute = childNodeAttributes.item(attributeIndex);
+						logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
+						newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
+					}
+					addDocumentChildren(newXML, childNode);
+				}
+			}
+		}
+		return xmlDocument;
+	}
+}
diff --git a/alien/src/net/pterodactylus/util/xml/XML.java b/alien/src/net/pterodactylus/util/xml/XML.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/xml/XML.java
@@ -0,0 +1,240 @@
+/*
+ * utils - XML.java - Copyright © 2006-2009 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.util.xml;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+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 net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * Contains method to transform DOM XML trees to byte arrays and vice versa.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class XML {
+
+	/** The logger. */
+	private static final Logger logger = Logging.getLogger(XML.class.getName());
+
+	/** Cached document builder factory. */
+	private static DocumentBuilderFactory documentBuilderFactory = null;
+
+	/** Cached document builder. */
+	private static DocumentBuilder documentBuilder = null;
+
+	/** Cached transformer factory. */
+	private static TransformerFactory transformerFactory = null;
+
+	/**
+	 * Returns a document builder factory. If possible the cached instance will
+	 * be returned.
+	 *
+	 * @return A document builder factory
+	 */
+	private static DocumentBuilderFactory getDocumentBuilderFactory() {
+		if (documentBuilderFactory != null) {
+			return documentBuilderFactory;
+		}
+		documentBuilderFactory = DocumentBuilderFactory.newInstance();
+		documentBuilderFactory.setXIncludeAware(true);
+		documentBuilderFactory.setNamespaceAware(true);
+		return documentBuilderFactory;
+	}
+
+	/**
+	 * Returns a document builder. If possible the cached instance will be
+	 * returned.
+	 *
+	 * @return A document builder
+	 */
+	private static DocumentBuilder getDocumentBuilder() {
+		if (documentBuilder != null) {
+			return documentBuilder;
+		}
+		try {
+			documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
+		} catch (ParserConfigurationException pce1) {
+			logger.log(Level.WARNING, "Could not create DocumentBuilder.", pce1);
+		}
+		return documentBuilder;
+	}
+
+	/**
+	 * Returns a transformer factory. If possible the cached instance will be
+	 * returned.
+	 *
+	 * @return A transformer factory
+	 */
+	private static TransformerFactory getTransformerFactory() {
+		if (transformerFactory != null) {
+			return transformerFactory;
+		}
+		transformerFactory = TransformerFactory.newInstance();
+		return transformerFactory;
+	}
+
+	/**
+	 * Creates a new XML document.
+	 *
+	 * @return A new XML document
+	 */
+	public static Document createDocument() {
+		return getDocumentBuilder().newDocument();
+	}
+
+	/**
+	 * Transforms the DOM XML document into a byte array.
+	 *
+	 * @param document
+	 *            The document to transform
+	 * @return The byte array containing the XML representation
+	 */
+	public static byte[] transformToByteArray(Document document) {
+		ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
+		OutputStreamWriter converter = new OutputStreamWriter(byteOutput, Charset.forName("UTF-8"));
+		writeToOutputStream(document, converter);
+		try {
+			converter.flush();
+			byteOutput.flush();
+			byte[] result = byteOutput.toByteArray();
+			return result;
+		} catch (IOException ioe1) {
+			return null;
+		} finally {
+			Closer.close(converter);
+			Closer.close(byteOutput);
+		}
+	}
+
+	/**
+	 * Writes the given document to the given writer.
+	 *
+	 * @param document
+	 *            The document to write
+	 * @param writer
+	 *            The writer to write the document to
+	 */
+	public static void writeToOutputStream(Document document, Writer writer) {
+		writeToOutputStream(document, writer, true);
+	}
+
+	/**
+	 * Writes the given document to the given writer.
+	 *
+	 * @param document
+	 *            The document to write
+	 * @param writer
+	 *            The writer to write the document to
+	 * @param preamble
+	 *            <code>true</code> to include the XML header,
+	 *            <code>false</code> to not include it
+	 */
+	public static void writeToOutputStream(Document document, Writer writer, boolean preamble) {
+		Result transformResult = new StreamResult(writer);
+		Source documentSource = new DOMSource(document);
+		try {
+			Transformer transformer = getTransformerFactory().newTransformer();
+			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, preamble ? "no" : "yes");
+			transformer.transform(documentSource, transformResult);
+		} catch (TransformerConfigurationException tce1) {
+			logger.log(Level.WARNING, "Could create Transformer.", tce1);
+		} catch (TransformerException te1) {
+			logger.log(Level.WARNING, "Could not transform Document.", te1);
+		}
+	}
+
+	/**
+	 * Transforms the byte array into a DOM XML document.
+	 *
+	 * @param data
+	 *            The byte array to parse
+	 * @return The DOM XML document
+	 */
+	public static Document transformToDocument(byte[] data) {
+		return transformToDocument(new ByteArrayInputStream(data));
+	}
+
+	/**
+	 * Transforms the input stream into a DOM XML document.
+	 *
+	 * @param inputStream
+	 *            The input stream to parse
+	 * @return The DOM XML document
+	 */
+	public static Document transformToDocument(InputStream inputStream) {
+		return transformToDocument(new InputSource(inputStream));
+	}
+
+	/**
+	 * Transforms the reader into a DOM XML document.
+	 *
+	 * @param inputReader
+	 *            The reader to read the XML from
+	 * @return The DOM XML document
+	 */
+	public static Document transformToDocument(Reader inputReader) {
+		return transformToDocument(new InputSource(inputReader));
+	}
+
+	/**
+	 * Transforms the inout source into a DOM XML document.
+	 *
+	 * @param inputSource
+	 *            The source to read the XML from
+	 * @return The DOM XML document
+	 */
+	public static Document transformToDocument(InputSource inputSource) {
+		try {
+			DocumentBuilder documentBuilder = getDocumentBuilder();
+			return documentBuilder.parse(inputSource);
+		} catch (SAXException saxe1) {
+			logger.log(Level.WARNING, "Could not parse InputSource.", saxe1);
+		} catch (IOException ioe1) {
+			logger.log(Level.WARNING, "Could not read InputSource.", ioe1);
+		}
+		return null;
+	}
+
+}
diff --git a/alien/src/net/pterodactylus/util/xml/package-info.java b/alien/src/net/pterodactylus/util/xml/package-info.java
new file mode 100644
--- /dev/null
+++ b/alien/src/net/pterodactylus/util/xml/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Package for XML-related utility classes.
+ *
+ */
+package net.pterodactylus.util.xml;
\ No newline at end of file
diff --git a/alien/src/wormarc/Archive.java b/alien/src/wormarc/Archive.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/Archive.java
@@ -0,0 +1,565 @@
+/* Freenet Write Once Read Multiple ARChive.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ * This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import wormarc.hgdeltacoder.HgDeltaCoder;
+
+public class Archive {
+    private final DeltaCoder mCoder = new HgDeltaCoder();
+    private final HistoryLinkMap mLinkMap = new HistoryLinkMap();
+    private final LinkDataFactory mLinkDataFactory = new RamLinkDataFactory();
+
+    ////////////////////////////////////////////////////////////
+    // Total in memory non-transient rep of an archive.
+    private List<Block> mBlocks;
+    private List<RootObject> mRootObjects;
+    ////////////////////////////////////////////////////////////
+
+    private Block mUpdates;
+
+    public final static class RootObject implements Comparable<RootObject> {
+        public final LinkDigest mDigest;
+        public final int mKind;
+        public RootObject(final LinkDigest digest, final int kind) {
+            mDigest = digest;
+            mKind = kind;
+        }
+
+        public boolean equals(final Object other) {
+            if (!(other instanceof RootObject)) {
+                return false;
+            }
+            return compareTo((RootObject)other) == 0;
+        }
+
+        // IMPORTANT: Must be able to sort stably so you get an identical binary rep for the same list.
+        public int compareTo(final RootObject obj) {
+            if (mKind - obj.mKind == 0) {
+                // Then by digest hex string.
+                return mDigest.toString().compareTo(obj.mDigest.toString());
+            }
+            // First by kind.
+            return mKind - obj.mKind;
+        }
+    }
+
+    // LATER: do better?
+    // I just need a way to return a struct from Archive.IO.read().
+    // Imperfect. Risky, Lists are mutable, Blocks are mutable.
+    public final static class ArchiveData {
+        public final List<Block> mBlocks;
+        public final List<RootObject> mRootObjects;
+
+        public ArchiveData(final List<Block> blocks, final List<RootObject> rootObjects) {
+            mBlocks = Collections.unmodifiableList(blocks);
+            mRootObjects = Collections.unmodifiableList(rootObjects);
+        }
+
+        // DCI: define hash too?
+        public boolean equals(final Object other) {
+            if (!(other instanceof ArchiveData)) {
+                return false;
+            }
+            final ArchiveData otherData = (ArchiveData)other;
+            return mBlocks.equals(otherData.mBlocks) && mRootObjects.equals(otherData.mRootObjects);
+        }
+
+        public String pretty() {
+            final StringBuilder buffer = new StringBuilder();
+            buffer.append("--- ArchiveData ---\n");
+            buffer.append("\nmRootObjects:\n");
+            for (Archive.RootObject obj : mRootObjects) {
+                buffer.append(String.format("   %s:%d\n", obj.mDigest, obj.mKind));
+            }
+            buffer.append("mBlocks:\n");
+            int count = 0;
+            for (Block block : mBlocks) {
+                buffer.append(String.format("   [%d]\n", count));
+                for (LinkDigest digest : block.getDigests()) {
+                    buffer.append("      ");
+                    buffer.append(digest.toString());
+                    buffer.append("\n");
+                }
+                count++;
+            }
+            buffer.append("---\n");
+            return buffer.toString();
+        }
+    }
+
+    public interface IO {
+        // DCI: back to this.
+        // It workd be cleaner to have this take and ArchiveData, but the block list isn't immutable.
+        void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) throws IOException;
+        // ArchiveData == blocks, rootObjects
+        Archive.ArchiveData read(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException;
+    }
+
+    public final static int REPARTITION_MULTIPLE = 2;
+    public final static int MAX_CHAIN_LENGTH = 16;
+    public final static int MAX_BLOCKS = 4;
+
+    public Archive() {
+        reset();
+    }
+
+    public Archive deepCopy() {
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't copy while updating.");
+        }
+        Archive ret = new Archive();
+        ret.mBlocks = new ArrayList<Block>(mBlocks);
+        ret.mRootObjects = new ArrayList<RootObject>(mRootObjects);
+        ret.mUpdates = null;
+        ret.mLinkMap.putAll(mLinkMap.getUnmodifiableMap());
+        return ret;
+    }
+
+    public final void reset() {
+        mBlocks = new ArrayList<Block>();
+        mRootObjects = new ArrayList<RootObject>();
+        mUpdates = null;
+    }
+
+    public ArchiveData getData() {
+        return new ArchiveData(mBlocks, mRootObjects);
+    }
+
+    public void setFromData(final ArchiveData data) {
+        mBlocks = new ArrayList<Block>(data.mBlocks);
+        mRootObjects = new ArrayList<RootObject>(data.mRootObjects);
+        mUpdates = null;
+    }
+
+    public InputStream getFile(final LinkDigest chainHead) throws IOException {
+        if (chainHead == null) {
+            throw new IllegalArgumentException("chainHead is null");
+        }
+
+        try {
+            return mCoder.applyDeltas(mLinkMap.getChain(chainHead, true));
+        } catch (HistoryLinkMap.LinkNotFoundException lookupFailed) {
+            lookupFailed.rethrowAsIOException();
+            return null; // Unreachable.
+        }
+    }
+
+    // Calling this with stopAtEnd == false allows you to read change history past the
+    // last time the chain was truncated.
+    public List<LinkDigest> getChain(final LinkDigest chainHead, final boolean stopAtEnd) throws IOException {
+        if (chainHead == null) {
+            throw new IllegalArgumentException("chainHead is null");
+        }
+        try {
+            final List<LinkDigest> digests = new ArrayList<LinkDigest>();
+            for (HistoryLink link : mLinkMap.getChain(chainHead, stopAtEnd)) {
+                digests.add(link.mHash);
+            }
+            return digests;
+        } catch (HistoryLinkMap.LinkNotFoundException lookupFailed) {
+            lookupFailed.rethrowAsIOException();
+            return null; // Unreachable.
+        }
+    }
+
+    public LinkDigest putFile(final InputStream rawBytes, final LinkDigest prevChainHead) throws IOException {
+        if (mUpdates == null) {
+            IOUtil.silentlyClose(rawBytes);
+            throw new IllegalStateException("Not updating. Did you forget to call startUpdate?");
+        }
+
+        if (rawBytes == null) {
+            throw new IllegalArgumentException("rawBytes is null");
+        }
+
+        if (prevChainHead == null) {
+            throw new IllegalArgumentException("prevChainHead is null");
+        }
+
+        InputStream prevStream = null; // Leaving this null causes a full reinsert.
+        try {
+            if (!prevChainHead.isNullDigest() &&
+                mLinkMap.getChain(prevChainHead, true).size() < MAX_CHAIN_LENGTH) {
+                // Add to chain the existing chain.
+                prevStream = getFile(prevChainHead);
+            }
+
+            final HistoryLink link = mCoder.makeDelta(mLinkDataFactory,
+                                                      prevChainHead,
+                                                      prevStream,
+                                                      rawBytes,
+                                                      false);
+
+            mUpdates.append(link.mHash);
+            mLinkMap.addLink(link);
+
+            return link.mHash;
+
+        } finally {
+            IOUtil.silentlyClose(rawBytes);
+            IOUtil.silentlyClose(prevStream);
+        }
+    }
+
+    public void startUpdate() {
+        if (mUpdates != null) {
+            throw new IllegalStateException("Already updating.");
+        }
+        mUpdates = new Block();
+    }
+
+    // LATER: revisit.
+    // Doesn't guarantee that the Archive's state is rolled back to where it was
+    // when startUpdate was called.
+    public void abandonUpdate() {
+        if (mUpdates == null) {
+            return;
+        }
+        mUpdates = null;
+    }
+
+    public boolean commitUpdate() {
+        if (mUpdates == null) {
+            throw new IllegalStateException("Not updating. Did you forget to call startUpdate?");
+        }
+
+        if (mUpdates.getDigests().size() == 0) {
+            mUpdates = null;
+            return false;
+        }
+
+        mBlocks.add(0, mUpdates);
+        mUpdates = null;
+        return true;
+    }
+
+    public Set<LinkDigest> addAllLinks(final Set<LinkDigest> all) {
+        for (Block block : mBlocks) {
+            all.addAll(block.getDigests());
+        }
+
+        if (all.contains(LinkDigest.NULL_DIGEST)) {
+            throw new RuntimeException("Assertion Failure: NULL_DIGEST in blocks");
+        }
+
+        for (RootObject obj : mRootObjects) {
+            if (obj.mDigest.isNullDigest()) {
+                continue;
+            }
+            if (!all.contains(obj.mDigest)) {
+                throw new RuntimeException("Assertion Failure: root object not in blocks: " + obj.mDigest);
+            }
+        }
+
+        return all;
+    }
+
+    public Set<LinkDigest> allLinks() {
+        return addAllLinks(new HashSet<LinkDigest>());
+    }
+
+    protected Set<LinkDigest> referencedLinks() throws IOException {
+        // DCI: DOG SLOW.
+        // DCI: cache value? notion of archive being "dirty"
+        // Set<LinkDigest> getReferencedLinks(Achive archive, Archive.RootObject obj);
+        final Set<LinkDigest> links = new HashSet<LinkDigest>();
+        for (RootObject obj : mRootObjects) {
+            if (!obj.mDigest.isNullDigest()) {
+                links.add(obj.mDigest);
+            }
+            links.addAll(RootObjectKind.getContainer(this, obj).getReferencedLinks(mLinkMap));
+        }
+        return links;
+    }
+
+    protected static List<Block> mergeBlocks(final List<Block> blocks, final List<PartitioningMath.Partition> partitions) {
+        final List<Block> merged = new ArrayList<Block>();
+        for (PartitioningMath.Partition partition : partitions) {
+            if (partition.getStart() == partition.getEnd()) {
+                merged.add(blocks.get(partition.getStart()));
+                continue;
+            }
+            final ArrayList<LinkDigest> digests = new ArrayList<LinkDigest>();
+            for (int index = partition.getStart(); index <= partition.getEnd(); index++) {
+                digests.addAll(blocks.get(index).getDigests());
+            }
+            merged.add(new Block(digests));
+        }
+        // DCI: prune duplicates? keep a set and don't add them in the first place?
+
+        return merged;
+    }
+
+    // Can break ordering constraint.
+    // Prepend the manifest update to the start of the first block.
+    public void compressAndUpdateArchiveManifest() throws IOException {
+        compressAndUpdateArchiveManifest(MAX_BLOCKS);
+    }
+
+    public void compressAndUpdateArchiveManifest(final int maxBlocks) throws IOException {
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't update the archive manifest while updating");
+        }
+
+        if (mBlocks.size() > maxBlocks) {
+            compress(maxBlocks); // DCI: what if you fail below?
+        }
+
+        // IMPORTANT: Clear the ARCHIVE_MANIFEST value.
+        // There are tricks in ManifestArchive.fromBytes to fixup the top link
+        // that depend on this value being NULL_DIGEST.
+        LinkDigest currentVersion = getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
+        setRootObject(LinkDigest.NULL_DIGEST, RootObjectKind.ARCHIVE_MANIFEST);
+
+        mUpdates = new Block();
+
+        final ArchiveManifest newManifest = new ArchiveManifest(mRootObjects, mBlocks);
+        try {
+            // DCI: retest
+            currentVersion = putFile(newManifest.toBytes(), currentVersion);
+            mBlocks.get(0).prepend(currentVersion);
+        } finally {
+            // IMPORTANT: Backs out change on failure.
+            setRootObject(currentVersion, RootObjectKind.ARCHIVE_MANIFEST);
+            mUpdates = null;
+        }
+
+        assertArchiveManifestIsValid("compressAndupdateArchiveManifest " +
+                                     "produced an invalid ARCHIVE_MANIFEST!");
+    }
+
+    public boolean compress() throws IOException {
+        return compress(MAX_BLOCKS);
+    }
+
+    public boolean compress(final int maxBlocks) throws IOException {
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't compress while updating");
+        }
+
+        final Set<LinkDigest> referenced = referencedLinks();
+        final ArrayList<PartitioningMath.Partition> uncompressed = new ArrayList<PartitioningMath.Partition>();
+        int index = 0;
+        for (Block block : mBlocks) {
+            final ArrayList<LinkDigest> survivors = new ArrayList<LinkDigest>();
+            for (LinkDigest digest : block.getDigests()) {
+                if (referenced.contains(digest)) {
+                    survivors.add(digest);
+                }
+            }
+            // Length after dropping unreferenced links.
+            uncompressed.add(new PartitioningMath.Partition(index, index, mLinkMap.getLength(survivors)));
+            index++;
+        }
+
+        final List<PartitioningMath.Partition> compressed = PartitioningMath.compress(uncompressed,
+                                                                                maxBlocks,
+                                                                                REPARTITION_MULTIPLE);
+        if (uncompressed.size() == compressed.size()) {
+            return false;
+        }
+
+        mBlocks = mergeBlocks(mBlocks, compressed);
+        return true;
+    }
+
+    ////////////////////////////////////////////////////////////
+    public void read(final IO source) throws IOException {
+        if (source == null) {
+            throw new IllegalArgumentException("source is null.");
+        }
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't read while updating.");
+        }
+        setFromData(source.read(mLinkMap, mLinkDataFactory));
+    }
+
+    public void write(final IO sink) throws IOException {
+        if (sink == null) {
+            throw new IllegalArgumentException("sink is null.");
+        }
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't write while updating.");
+        }
+
+        assertArchiveManifestIsValid("Trying to write archive with invalid ARCHIVE_MANIFEST!");
+
+        sink.write(mLinkMap, mBlocks, mRootObjects);
+    }
+
+    public static Archive load(final IO source, boolean skipValidation) throws IOException {
+        final Archive loaded = new Archive();
+        loaded.read(source);
+        if (!loaded.getRootObject(RootObjectKind.ARCHIVE_MANIFEST).isNullDigest()) {
+             if (!loaded.hasValidArchiveManifest()) {
+                 if (!skipValidation) {
+                     // This is a runtime error, not an assertion failure
+                     // because we didn't create the manifest.
+                     throw new IOException("Invalid ARCHIVE_MANIFEST!"); // DCI: predicate failure
+                 }
+             }
+        }
+        return loaded;
+    }
+
+    public static Archive load(final IO source) throws IOException {
+        return load(source, false);
+    }
+
+    ////////////////////////////////////////////////////////////
+    public void setRootObject(final LinkDigest value, final int kind, final boolean replace) {
+        final RootObject obj = new RootObject(value, kind);
+        if (replace) {
+            for (int index = 0; index < mRootObjects.size(); index++) {
+                if (mRootObjects.get(index).mKind == kind) {
+                    mRootObjects.set(index, obj);
+                    return;
+                }
+            }
+        }
+        mRootObjects.add(obj);
+
+        // Kind of gross, but this list is tiny.
+        Collections.sort(mRootObjects);
+    }
+
+    public void unsetRootObject(final int kind) {
+        for (RootObject obj : mRootObjects) {
+            if (obj.mKind == kind) {
+                mRootObjects.remove(obj);
+                // Can't continue iterating after modifying. Do better?
+                unsetRootObject(kind);
+                break;
+            }
+        }
+    }
+
+    public void setRootObject(final LinkDigest value, final int kind) {
+        setRootObject(value, kind, true);
+    }
+
+    // Returns NULL_DIGEST if not found.
+    public LinkDigest getRootObject(final int kind) {
+        for (RootObject obj : mRootObjects) {
+            if (obj.mKind == kind) {
+                return obj.mDigest;
+            }
+        }
+        return LinkDigest.NULL_DIGEST;
+    }
+
+    // Updates the first root object of kind kind.
+    public LinkDigest updateRootObject(final InputStream data, final int kind) throws IOException {
+        final LinkDigest digest = putFile(data, getRootObject(kind));
+        setRootObject(digest, kind);
+        return digest;
+    }
+
+    public boolean hasValidArchiveManifest() throws IOException {
+        if (mUpdates != null) {
+            throw new IllegalStateException("Can't validate the archive manifest while updating.");
+        }
+
+        final LinkDigest digest = getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
+        if (digest.isNullDigest()) {
+            return false;
+        }
+
+        // This doesn't guarantee that the binary reps are identical. hmmmm...
+        // DCI: Have seen this fail!
+        return ArchiveManifest.fromBytes(getFile(digest), digest).makeArchiveData().equals(getData());
+    }
+
+    public void assertArchiveManifestIsValid(String failureMsg) throws IOException {
+        if (!getRootObject(RootObjectKind.ARCHIVE_MANIFEST).isNullDigest()) {
+            if (!hasValidArchiveManifest()) {
+                throw new RuntimeException(String.format("Assertion Failure: %s", failureMsg));
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    public int getChainLength(final LinkDigest chainHead) {
+        if (chainHead == null) {
+            throw new IllegalArgumentException("chainHead is null");
+        }
+
+        try {
+            return mLinkMap.getChain(chainHead, true).size();
+        } catch (HistoryLinkMap.LinkNotFoundException lookupFailed) {
+            return -1;
+        }
+    }
+
+    public String pretty() {
+        final StringBuilder buffer = new StringBuilder();
+        buffer.append("--- Archive ---\n");
+        buffer.append("\nmRootObjects:\n");
+        for (Archive.RootObject obj : mRootObjects) {
+            buffer.append(String.format("   %s:%d\n", obj.mDigest, obj.mKind));
+        }
+        buffer.append("mBlocks:\n");
+        int count = 0;
+        for (Block block : mBlocks) {
+            long blockLength = -1;
+            try {
+                blockLength = mLinkMap.getLength(block);
+            } catch (IOException ioe) {
+            }
+            buffer.append(String.format("   [%d] : %d\n", count, blockLength));
+            for (LinkDigest digest : block.getDigests()) {
+                buffer.append("      ");
+                buffer.append(prettyLink(digest.toString()));
+                buffer.append("\n");
+            }
+            count++;
+        }
+        buffer.append("---\n");
+        return buffer.toString();
+    }
+
+    public String prettyLink(final String linkHash) {
+        try {
+            final HistoryLink link = mLinkMap.getLink(new LinkDigest(linkHash));
+            final long length = BinaryLinkRep.getRepLength(link);
+            return String.format("%s:%s [%d]%s", linkHash, link.mParent.toString(),
+                                 length, link.mIsEnd ? ":END" : "");
+
+        } catch (HistoryLinkMap.LinkNotFoundException lookupFailed) {
+            return linkHash + ":???";
+        }
+    }
+
+    public boolean isUpdating() { return mUpdates != null; }
+}
diff --git a/alien/src/wormarc/ArchiveManifest.java b/alien/src/wormarc/ArchiveManifest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/ArchiveManifest.java
@@ -0,0 +1,283 @@
+/* A top level manifest of the entire contents of an archive.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+// INTENT: Should contain all the information needed to persist an archive.
+//         This makes re-insertion possible.
+public class ArchiveManifest {
+    long mVersion; // DCI: really need a long?
+    List<Archive.RootObject> mRootObjects;
+    List<Block> mBlocks;
+
+    public final static long SUPPORTED_VERSION = 1; // Binary rep version.
+
+    // These lists are already unmodifiable.
+    public List<Archive.RootObject> getRootObjects() { return mRootObjects; }
+    public List<Block> getBlocks() { return mBlocks; }
+
+    public ArchiveManifest(List<Archive.RootObject> rootObjects, List<Block> blocks) {
+        mVersion = SUPPORTED_VERSION;
+        mRootObjects = Collections.unmodifiableList(rootObjects);
+        mBlocks = Collections.unmodifiableList(blocks);
+    }
+
+    public Archive.ArchiveData makeArchiveData() {
+        return new Archive.ArchiveData(mBlocks, mRootObjects);
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Handle read from / writing to stream.
+
+    /*
+      Writing into the Archive is tricky because we can't know the final
+      chain head HistoryLink for the file itself.
+
+      REQUIREMENTS:
+      0. A well formed ArchiveManifest should a value for the ARCHIVE_MANIFEST
+         root object which allows it to be read from the Archive, even if
+         that value is unknown at the time it is written into the archive.
+      1. Successive to write calls on an ArchiveManifest should always return
+         bit for bit identical data.
+
+      SOLUTION:
+      REQUIRE that there is only one ARCHIVE_MANIFEST root object.
+      REQUIRE that the latest link of the archive manifest is always the
+      first link in the first block.
+
+      On write:
+      ALWAYS store NULL_DIGEST in the ARCHIVE_MANIFEST root object and in place
+      of the chain head digest for the head link of the file itself in the
+      block lists.  It should be the first link in the first block.
+
+      On read:
+      Fixup the NULL_DIGEST references with the chain head hash that the
+      the file was read from in both places.
+
+      Use case:
+      Write:
+      Archive updates manifest.
+      Archive writes manifest into itself.
+      At this point the archive has the chain head hash for the update
+      manifest, even though it isn't stored in the manifest itself.
+      Archive stashes that hash somewhere (e.g. the ARCHIVE_MANIFEST field
+      in a FreenetTopKey).
+
+      Write subcases:
+      0. We don't know the chain head value (updating)
+      ARCHIVE_MANIFEST root object MUST be NULL_DIGEST.
+      We need to insert a NULL_DIGEST place holder hash at the start of
+      the first block.
+
+      1. We do know the chain head value (re-inserting)
+      Save the hash. Set ARCHIVE_MANIFEST root object to NULL_HASH.
+      CHECK that the first link in the first block is the saved hash.
+      Write NULL_HASH instead.
+
+      NEVER HIT THIS CASE FOR FNIKI!
+
+      Read:
+      Archive gets a non NULL_DIGEST ARCHIVE_MANIFEST by some means
+      (e.g. it reads it out of the the ARCHIVE_MANIFEST field in
+      a FreenetTopKey).
+      Archive uses it to read the ArchiveManifest instance out of
+      Freenet.
+
+      LATER: Do better.
+      Why write the NULL_DIGEST into the block / root objects at all?
+      Could save ~= 40 bytes per insert.
+      Didn't do this because I didn't want to break format compatibility again.
+
+    */
+
+    public InputStream toBytes() throws IOException {
+        // Writes everything into RAM! hmmmm...
+
+        if (mBlocks.size() == 0) {
+            throw new IOException("ArchiveManifest must have at least one block!");
+        }
+
+        LinkDigest digest = getArchiveManifestDigest(mRootObjects);
+
+        boolean insertSentinel = digest.isNullDigest();
+
+        if ((!insertSentinel) &&
+            ((mBlocks.get(0).getDigests().size() == 0) ||
+             (!mBlocks.get(0).getDigests().get(0).equals(digest)))) {
+            throw new IOException("ARCHIVE_MANIFEST chain head not found at start of first block!");
+        }
+
+        ByteArrayOutputStream  buffer = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(buffer);
+        if (mVersion != SUPPORTED_VERSION) {
+            throw new IOException("Version mismatch.");
+        }
+        outputStream.writeLong(mVersion);
+        outputStream.writeByte(mRootObjects.size()); // DCI: test, sign?
+        for (Archive.RootObject obj : mRootObjects) {
+            if (obj.mKind == RootObjectKind.ARCHIVE_MANIFEST) {
+                outputStream.write(LinkDigest.NULL_DIGEST.getBytes());
+            } else {
+                outputStream.write(obj.mDigest.getBytes());
+            }
+            outputStream.writeInt(obj.mKind);
+        }
+        outputStream.writeByte(mBlocks.size());
+
+        boolean shouldAddOne = insertSentinel;
+        for (Block block : mBlocks) {
+            int linkCount = block.getDigests().size();
+            if (shouldAddOne) {
+                // Handle insert of optional sentinel link at start of first block.
+                linkCount++;
+                shouldAddOne = false;
+            }
+            outputStream.writeInt(linkCount);
+        }
+
+        // Placeholder for archive manifest topkey.
+        outputStream.write(LinkDigest.NULL_DIGEST.getBytes());
+
+        boolean shouldSkipOne = (!insertSentinel);
+        for (Block block : mBlocks) {
+            for (LinkDigest value : block.getDigests()) {
+                if (shouldSkipOne) {
+                    shouldSkipOne = false;
+                    continue;
+                }
+                // DCI: add a \n so the delta coder works better?
+                outputStream.write(value.getBytes());
+            }
+        }
+        outputStream.close();
+        return new ByteArrayInputStream(buffer.toByteArray());
+    }
+
+    // Always closes stream.
+    public static ArchiveManifest fromBytes(InputStream rawBytes, LinkDigest chainHeadFixup) throws IOException {
+        try {
+            if (chainHeadFixup == null || chainHeadFixup.isNullDigest()) {
+                throw new IOException("chainHeadFixup is null or NULL_DIGEST!");
+            }
+
+            DataInputStream inputStream = new DataInputStream(rawBytes);
+            long version = inputStream.readLong();
+            if (version != SUPPORTED_VERSION) {
+                throw new IOException("ArchiveManifest format version mis-match");
+            }
+            int rootObjectCount = inputStream.readByte();
+            List<Archive.RootObject> rootObjects = new ArrayList<Archive.RootObject>();
+            while (rootObjectCount > 0) {
+                LinkDigest digest = BinaryLinkRep.readLinkDigest(inputStream);
+                int kind = inputStream.readInt();
+                if (kind == RootObjectKind.ARCHIVE_MANIFEST && digest.isNullDigest()) {
+                    // Fixup the reference to the latest link for this file.
+                    digest = chainHeadFixup;
+                }
+                rootObjects.add(new Archive.RootObject(digest, kind));
+                rootObjectCount--;
+            }
+
+            int blockCount = inputStream.readByte();
+            List<Integer> blockSizes = new ArrayList<Integer>();
+            while (blockCount > 0) {
+                blockSizes.add(inputStream.readInt());
+                blockCount--;
+            }
+
+            List<Block> blocks = new ArrayList<Block>();
+            for (int digestCount : blockSizes) {
+                List<LinkDigest> digests = new ArrayList<LinkDigest>();
+                while (digestCount > 0) {
+                    digests.add(BinaryLinkRep.readLinkDigest(inputStream));
+                    digestCount--;
+                }
+                if (blocks.size() == 0 && digests.size() >= 1) {
+                    if (digests.get(0).isNullDigest()) {
+                        digests.set(0, chainHeadFixup); // Fixup the reference to the latest link for this file.
+                    } else {
+                        throw new IOException("ArchiveManifest missing NULL_DIGEST sentinel block?");
+                    }
+                }
+                blocks.add(new Block(digests));
+            }
+            inputStream.close();
+            rawBytes = null;
+            return new ArchiveManifest(rootObjects, blocks);
+        } finally {
+            if (rawBytes != null) {
+                rawBytes.close();
+            }
+        }
+    }
+
+    // Hmmmm... Move?
+    public static LinkDigest getArchiveManifestDigest(List<Archive.RootObject> rootObjects)
+        throws IOException {
+        LinkDigest digest = null;
+        for (Archive.RootObject obj : rootObjects) {
+            if (obj.mKind == RootObjectKind.ARCHIVE_MANIFEST) {
+                if (digest != null) {
+                    throw new IOException("ArchiveManifest has multiple ARCHIVE_MANIFEST root objects!");
+                }
+                digest = obj.mDigest;
+            }
+        }
+        if (digest == null) {
+            throw new IOException("ArchiveManifest has no ARCHIVE_MANIFEST root object!");
+        }
+        return digest;
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Every referenced link.
+    public Set<LinkDigest> getReferencedLinks(HistoryLinkMap linkMap) {
+        Set<LinkDigest> links = new HashSet<LinkDigest>();
+        for (Block block : mBlocks) {
+            links.addAll(block.getDigests());
+        }
+
+        // Doesn't recurse! If the manifest is well formed it should already
+        // contain all it's links.
+        for (Archive.RootObject obj : mRootObjects) {
+            if (obj.mDigest != LinkDigest.NULL_DIGEST) {
+                links.add(obj.mDigest);
+            }
+        }
+        return links;
+    }
+}
diff --git a/alien/src/wormarc/ArchiveResolver.java b/alien/src/wormarc/ArchiveResolver.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/ArchiveResolver.java
@@ -0,0 +1,35 @@
+/* An interface for classes than can create an Archive from and ExternalReference.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.IOException;
+
+public interface ArchiveResolver {
+    // Can raise FileNotFoundException.
+    Archive resolve(ExternalRefs.Reference fromReference) throws IOException;
+    String getNym(ExternalRefs.Reference fromReference) throws IOException;
+    //String isCached(ExternalRefs.Reference fromReference) throws IOException;
+    //boolean cache(ExternalRefs.Reference fromReference) throws IOException;
+}
diff --git a/alien/src/wormarc/AuditArchive.java b/alien/src/wormarc/AuditArchive.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/AuditArchive.java
@@ -0,0 +1,275 @@
+/* A collection of functions used to check the integrity of archives.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+
+// DCI: fix name. Does intra archive stuff.
+public class AuditArchive {
+    public static final class Changes {
+        public final Set<LinkDigest> mAdded;
+        public final Set<LinkDigest> mRemoved;
+        public final Set<LinkDigest> mCommon;
+
+        Changes(Set<LinkDigest> added,
+                Set<LinkDigest> removed,
+                Set<LinkDigest> common) {
+            mAdded = Collections.unmodifiableSet(added);
+            mRemoved = Collections.unmodifiableSet(removed);
+            mCommon = Collections.unmodifiableSet(common);
+        }
+    }
+
+    public static Changes changes(Archive newer, ExternalRefs.Reference older, ArchiveResolver resolver) throws IOException {
+        if (!newer.hasValidArchiveManifest()) {
+            throw new IOException("The newer archive doesn't have a valid manifest.");
+        }
+
+        Archive other = resolver.resolve(older);
+        if (!other.hasValidArchiveManifest()) {
+            throw new IOException("The older archive doesn't have a valid manifest.");
+        }
+
+        Set<LinkDigest> allA = other.allLinks();
+        Set<LinkDigest> allB = newer.allLinks();
+
+        // Add to the new archive.
+        Set<LinkDigest> added = new HashSet<LinkDigest>(allB);
+        added.removeAll(allA);
+
+        // Removed from the old archive.
+        Set<LinkDigest> removed = new HashSet<LinkDigest>(allA);
+        added.removeAll(allB);
+
+        // In both
+        Set<LinkDigest> common = new HashSet<LinkDigest>(allB);
+        added.removeAll(added);
+
+        return new Changes(added, removed, common);
+    }
+
+    public static Set<LinkDigest> added(Archive archive, ArchiveResolver resolver) throws IOException {
+        LinkDigest refsDigest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
+        if (refsDigest.isNullDigest()) {
+            throw new IOException("Archive doesn't have PARENT_REFERENCES.");
+        }
+        ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
+        Set<LinkDigest> allParentLinks = new HashSet<LinkDigest>();
+        for (ExternalRefs.Reference ref : refs.mRefs) {
+            Archive parent = resolver.resolve(ref);
+            if (!parent.hasValidArchiveManifest()) {
+                // DCI: PredicateFailureException?
+                throw new IOException("The parent archive doesn't have a valid manifest:"
+                                      + ref.mExternalKey);
+            }
+            parent.addAllLinks(allParentLinks);
+        }
+
+        Set<LinkDigest> currentLinks = archive.allLinks();
+        currentLinks.removeAll(allParentLinks);
+        return currentLinks;
+    }
+
+    ////////////////////////////////////////////////////////////
+    private static final class SuspectInfo {
+        final ExternalRefs.Reference mRef;
+        final Set<LinkDigest> mAllLinks;
+        final ExternalRefs mRefs;
+        SuspectInfo(ExternalRefs.Reference ref,
+                    Set<LinkDigest> allLinks,
+                    ExternalRefs refs) {
+            mRef = ref;
+            mAllLinks = Collections.unmodifiableSet(allLinks);
+            mRefs = refs;
+        }
+    }
+
+    private static final SuspectInfo getSuspectInfo(Archive archive,
+                                                    ExternalRefs.Reference archiveRef,
+                                                    ArchiveResolver resolver,
+                                                    Map<String, SuspectInfo> suspects)
+        throws IOException {
+        SuspectInfo suspect = suspects.get(archiveRef.mExternalKey);
+        if (suspect== null) {
+            if (archive == null) {
+                System.err.println("getSuspectInfo -- resolving: " + archiveRef.mExternalKey);
+                archive = resolver.resolve(archiveRef);
+            }
+            System.err.println("getSuspectInfo -- " + archiveRef.mExternalKey);
+            System.err.println("getSuspectInfo -- 1:" + archive.getRootObject(RootObjectKind.ARCHIVE_MANIFEST));
+            if (!archive.hasValidArchiveManifest()) {
+
+                // DCI: fails.  This is a real bug.  Investigate further?
+                // DCI: PredicateFailureException?
+                throw new IOException("The parent archive doesn't have a valid manifest: "
+                                      + archiveRef.mExternalKey);
+            }
+            LinkDigest refsDigest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
+            ExternalRefs refs = null;
+            if (refsDigest.isNullDigest()) {
+                refs = ExternalRefs.NONE;
+            } else {
+                refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
+            }
+
+            suspect = new SuspectInfo(archiveRef,
+                                      archive.allLinks(),
+                                      refs);
+            // Cache
+            suspects.put(archiveRef.mExternalKey, suspect);
+        }
+        return suspect;
+    }
+
+    private static boolean isPerpetrator(ExternalRefs.Reference archiveRef,
+                                         LinkDigest link,
+                                         ArchiveResolver resolver,
+                                         Map<String, SuspectInfo> infoCache) throws IOException {
+
+        SuspectInfo current = getSuspectInfo(null, archiveRef, resolver, infoCache);
+        Set<LinkDigest> allParentLinks = new HashSet<LinkDigest>();
+        for (ExternalRefs.Reference ref : current.mRefs.mRefs) {
+            SuspectInfo parent = getSuspectInfo(null, ref, resolver, infoCache);
+            allParentLinks.addAll(parent.mAllLinks);
+        }
+
+        Set<LinkDigest> currentLinks = new HashSet<LinkDigest>(current.mAllLinks);
+        currentLinks.removeAll(allParentLinks);
+
+        return currentLinks.contains(link);
+    }
+
+    // Breadth first!
+    private static ExternalRefs.Reference findPerpetrator(ExternalRefs.Reference archiveRef,
+                                                          LinkDigest link,
+                                                          ArchiveResolver resolver,
+                                                          Map<String, SuspectInfo> infoCache)
+        throws IOException {
+
+        List<ExternalRefs.Reference> suspects = new ArrayList<ExternalRefs.Reference>();
+        suspects.add(archiveRef); // Current archive version. MUST be cached.
+
+        while (!suspects.isEmpty()) {
+            ExternalRefs.Reference suspectRef = suspects.remove(0);
+            SuspectInfo suspect = getSuspectInfo(null, suspectRef, resolver, infoCache);
+            if (isPerpetrator(suspectRef, link, resolver, infoCache)) {
+                return suspect.mRef;
+            }
+            suspects.addAll(suspect.mRefs.mRefs);
+        }
+
+        // DCI: predicate failure?
+        throw new IOException("Couldn't find link???");
+    }
+
+    public static List<ExternalRefs.Reference> history(Archive archive,
+                                                       ExternalRefs.Reference archiveRef,
+                                                       List<LinkDigest> chain,
+                                                       ArchiveResolver resolver) throws IOException {
+        chain = new ArrayList<LinkDigest>(chain); // Deep copy so we can consume.
+        if (archiveRef == null) {
+            archiveRef = ExternalRefs.CURRENT_ARCHIVE;
+        }
+        Map<String, SuspectInfo> infoCache = new HashMap<String, SuspectInfo>();
+        infoCache.put(archiveRef.mExternalKey, getSuspectInfo(archive, archiveRef, resolver, infoCache));
+
+        List<ExternalRefs.Reference> historyList = new ArrayList<ExternalRefs.Reference>();
+        while (!chain.isEmpty()) {
+            LinkDigest link = chain.remove(0);
+            ExternalRefs.Reference perpetrator = findPerpetrator(archiveRef, link, resolver, infoCache);
+            historyList.add(perpetrator);
+            // DCI: test. Think. This stuff makes my head hurt.
+            // Don't need to search above the last link we found in the chain.
+            archiveRef = perpetrator;
+        }
+
+        return historyList;
+    }
+
+    public interface ChangeLogCallback {
+        boolean onChangeEntry(ExternalRefs.Reference oldVer,
+                              ExternalRefs.Reference newVer,
+                              FileManifest.Changes fromNewToOld);
+    }
+
+    // Generate a change log like the one in the current wikibot based
+    // fniki wiki.
+    // LATER: Deal with non-linear change history.
+    public static void getManifestChangeLog(ExternalRefs.Reference latestRef,
+                                            Archive archive,
+                                            ArchiveResolver resolver,
+                                            ChangeLogCallback callback) throws IOException {
+
+        if (archive.getRootObject(RootObjectKind.FILE_MANIFEST).isNullDigest()) {
+            throw new IOException("No FILE_MANIFEST in root objects: " +
+                                  latestRef.toString());
+        }
+
+        Map<String, LinkDigest> currentMap = FileManifest.fromArchiveRootObject(archive).getMap();
+        ExternalRefs.Reference currentRef = latestRef;
+        while (currentRef != ExternalRefs.NULL_ARCHIVE) {
+            ExternalRefs.Reference nextRef = ExternalRefs.NULL_ARCHIVE;
+            Map<String, LinkDigest> nextMap = new HashMap<String, LinkDigest>();
+
+            LinkDigest digest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
+            if (!digest.isNullDigest()) {
+                ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(digest));
+
+                if (refs.mRefs.size() > 1) {
+                    throw new IOException("Code too dumb to deal with multiple parents. Sorry :-(");
+                }
+
+                if (refs.mRefs.size() > 0) {
+                    nextRef = refs.mRefs.get(0);
+                    archive = resolver.resolve(nextRef);
+
+                    if (archive.getRootObject(RootObjectKind.FILE_MANIFEST).isNullDigest()) {
+                        throw new IOException("No FILE_MANIFEST in root objects: " +
+                                              nextRef.toString());
+                    }
+
+                    nextMap = FileManifest.fromArchiveRootObject(archive).getMap();
+                }
+            }
+
+            if (!callback.onChangeEntry(currentRef, nextRef, FileManifest.diff(nextMap, currentMap))) {
+                break; // Client code told us to give up.
+            }
+
+            currentRef = nextRef;
+            currentMap = nextMap;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/BinaryLinkRep.java b/alien/src/wormarc/BinaryLinkRep.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/BinaryLinkRep.java
@@ -0,0 +1,197 @@
+/* Methods to read and write the binary representation of HistoryLinks.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.EOFException;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+// All values are nw byte order, mHash is derived, not stored
+// <data length:4 byte unsigned int><1 byte flags><parent: 20 bytes><data>
+public class BinaryLinkRep {
+    private final static int LINK_HEADER_LEN = 4 + 1 + 20;
+    private final static int FLAG_IS_END = 1;
+    private final static int STREAM_BUFFERING = 4096;
+    private final static long INT_MASK = 0xffffffffL;
+    // Big-endian.
+    private static byte[] unsignedIntToBytes(long value) {
+        if (value < 0 || value > 4294967295L) {
+            throw new IllegalArgumentException("Not an unsigned int!");
+        }
+
+        byte[] bytes = new byte[4];
+        for (int index = 0; index < 4; index++) {
+            bytes[3 - index] = (byte)(value & 0xff);
+            value = value >> 8;
+        }
+
+        return bytes;
+    }
+
+    static MessageDigest createDigestForLink(long dataLength, boolean isEnd, LinkDigest parent)
+        throws IOException {
+        MessageDigest sha1 = null;
+        try {
+            sha1 = MessageDigest.getInstance("SHA");
+        }
+        catch (NoSuchAlgorithmException nsae) {
+            throw new IOException("Couldn't load SHA1 algorithm.");
+        }
+        // big endian 4 byte unsigned integer.
+        sha1.update(unsignedIntToBytes(dataLength));
+        // one byte flags field.
+        sha1.update((byte)(isEnd ? 1 :0));
+        // 20 byte SHA1 digest.
+        sha1.update(parent.getBytes());
+        return sha1;
+    }
+
+    public final static LinkDigest readLinkDigest(DataInputStream source) throws IOException {
+        return new LinkDigest(IOUtil.readBytes(source, 20));
+    }
+
+    public final static long getRepLength(HistoryLink link) {
+        return link.mDataLength + LINK_HEADER_LEN;
+    }
+
+    // ASSUMPTION: PACKED!
+    public final static long getRepLength(Iterable<HistoryLink> links) {
+        long total = 0;
+        for (HistoryLink link : links) {
+            total += link.mDataLength + LINK_HEADER_LEN;
+        }
+        return total;
+    }
+
+    private final static int getFlags(HistoryLink link) {
+        return link.mIsEnd ? FLAG_IS_END : 0;
+    }
+    private final static boolean isEnd(int flags) {
+        return (flags & FLAG_IS_END) != 0;
+    }
+
+    public final static InputStream toBytes(HistoryLink link) throws IOException {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        DataOutputStream output = new DataOutputStream(buffer);
+        output.writeInt((int)(link.mDataLength + LINK_HEADER_LEN));
+        output.writeByte(getFlags(link));
+        output.write(link.mParent.getBytes());
+        output.close();
+
+        return new SequenceInputStream(new ByteArrayInputStream(buffer.
+                                                                toByteArray()),
+                                       link.inputStream());
+    }
+
+    public final static void write(OutputStream toStream, HistoryLink link) throws IOException {
+        DataOutputStream output = new DataOutputStream(toStream);
+        output.writeInt((int)(link.mDataLength + LINK_HEADER_LEN));
+        output.writeByte(getFlags(link));
+        output.write(link.mParent.getBytes());
+        output.flush();
+        link.copyTo(toStream);
+    }
+
+    // Returns null on EOF
+    public final static HistoryLink fromBytes(InputStream inputBytes,
+                                              LinkDataFactory factory)
+        throws IOException {
+        try {
+            DataInputStream input = new DataInputStream(inputBytes);
+            long length = input.readInt() & INT_MASK;
+            int flags = input.readByte();
+            LinkDigest parent = readLinkDigest(input);
+            return HistoryLink.makeLink(length - LINK_HEADER_LEN,
+                                        isEnd(flags),
+                                        parent,
+                                        input,
+                                        factory);
+        } catch (EOFException eofe) {
+            return null;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Hmmmm... a bunch of convenient helper stuff that belongs somewhere else?
+
+    // Helper enumeration implementation used to concatinate streams.
+    final static class LinkStreamEnumeration implements Enumeration<InputStream> {
+        final Iterator<HistoryLink> mLinkIter;
+
+        public LinkStreamEnumeration(Iterator<HistoryLink> links) {
+            mLinkIter = links;
+        }
+
+        public boolean hasMoreElements() {
+            return mLinkIter.hasNext();
+        }
+
+        public InputStream nextElement() {
+            HistoryLink link = null;
+            try {
+                link = mLinkIter.next();
+                return toBytes(link);
+            } catch (IOException ioe) {
+                throw new NoSuchElementException("Couldn't open InputStream for link: " + link);
+            }
+        }
+    }
+
+    public final static InputStream toBytes(Iterable<HistoryLink> links)
+        throws IOException {
+        return new BufferedInputStream(new SequenceInputStream(new LinkStreamEnumeration(links.iterator())),
+                                       STREAM_BUFFERING);
+    }
+
+    public final static List<HistoryLink> readAll(InputStream in, LinkDataFactory factory)
+        throws IOException {
+        ArrayList<HistoryLink> links = new ArrayList<HistoryLink>();
+        while (true) {
+            HistoryLink link = fromBytes(in, factory);
+            if (link == null) {
+                break;
+            }
+            links.add(link);
+        }
+        return Collections.unmodifiableList(links);
+    }
+}
diff --git a/alien/src/wormarc/Block.java b/alien/src/wormarc/Block.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/Block.java
@@ -0,0 +1,72 @@
+/* An ordered sequence of LinkDigest hashes.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+// DCI: Need a notion of being "dirty", and a cached name...
+public class Block {
+    // No guarantee that links don't appear multiple times???
+    private ArrayList<LinkDigest> mDigests;
+
+    public Block(List<LinkDigest> digests) { mDigests = new ArrayList<LinkDigest> (digests);}
+    public Block() { this(new ArrayList<LinkDigest>()); }
+
+    public List<LinkDigest> getDigests() { return Collections.unmodifiableList(mDigests); }
+
+    public void prepend(LinkDigest linkDigest) {
+        if (linkDigest == null) {
+            throw new IllegalArgumentException("linkDigest is null");
+        }
+        mDigests.add(0, linkDigest);
+    }
+
+    public void append(LinkDigest linkDigest) {
+        if (linkDigest == null) {
+            throw new IllegalArgumentException("linkDigest is null");
+        }
+        mDigests.add(linkDigest);
+    }
+
+    public void append(List<LinkDigest> linkDigests) {
+        // TRADE OFF: debuggability vs speed.  Can remove later if this is too much of speed hit.
+        for (LinkDigest digest: linkDigests) {
+            if (digest == null) {
+                throw new IllegalArgumentException("LinkDigests contains a null element");
+            }
+        }
+        mDigests.addAll(linkDigests);
+    }
+
+    public boolean equals(Object other) {
+        if (!(other instanceof Block)) {
+            return false;
+        }
+        Block otherBlock = (Block)other;
+        return mDigests.equals(otherBlock.mDigests);
+    }
+}
+
diff --git a/alien/src/wormarc/DeltaCoder.java b/alien/src/wormarc/DeltaCoder.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/DeltaCoder.java
@@ -0,0 +1,50 @@
+/* An abstract interface for a delta encoder / decoder.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+public interface DeltaCoder {
+    // Make a new HistoryLink to store the incremental changes to a file.
+    // parent is the LinkDigest for the latest link in the HistoryLink
+    // chain representing the previous version. oldData is the previous file data.
+
+    // oldData can be null. This forces a full reinsert.
+    // The parent value is set in the returned link even when oldData is null.
+    // This allows you to follow the history of shortened chains.
+    //
+    // disableCompress is currently unimplemented. IT MUST BE false.
+    HistoryLink makeDelta(LinkDataFactory linkDataFactory,
+                          LinkDigest parent,
+                          InputStream oldData,
+                          InputStream newData,
+                          boolean disableCompression
+                          ) throws IOException;
+
+
+    // Reconstruct a file from a list of HistoryLinks.
+    InputStream applyDeltas(Iterable<HistoryLink> history) throws IOException;
+}
diff --git a/alien/src/wormarc/ExternalRefs.java b/alien/src/wormarc/ExternalRefs.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/ExternalRefs.java
@@ -0,0 +1,145 @@
+/* A reference to an archive resolvable by an ArchiveResolver. e.g. a Freenet URI.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class ExternalRefs {
+    public final static class Reference implements Comparable<Reference> {
+        public final int mKind;
+        public final String mExternalKey;
+
+        public Reference(int kind, String key) {
+            mKind = kind;
+            mExternalKey = key;
+
+            if (mExternalKey == null) {
+                throw new IllegalArgumentException("Key is null");
+            }
+
+            if (mKind < 0 || mKind > 127) {
+                throw new IllegalArgumentException("kind out of range [0, 127]");
+            }
+            if (mExternalKey.length() > 32767) {
+                throw new IllegalArgumentException("Key too big.");
+            }
+        }
+        // IMPORTANT: Must be able to sort stably so you get an identical binary rep for the same list.
+        public int compareTo(Reference other) {
+            if (mKind - other.mKind == 0) {
+                // Then by key string.
+                return mExternalKey.compareTo(other.mExternalKey);
+            }
+            // First by kind.
+            return mKind - other.mKind;
+        }
+    }
+    public final List<Reference> mRefs;
+
+    public final static int KIND_LOCAL = 1;
+    public final static int KIND_FREENET = 2;
+
+    public final static Reference CURRENT_ARCHIVE = new Reference(KIND_LOCAL, "current_archive");
+    // Like hg rev -1. i.e. the rev before the first.
+    public final static Reference NULL_ARCHIVE = new Reference(KIND_LOCAL, "null_archive");
+    public final static ExternalRefs NONE = new ExternalRefs(new ArrayList<Reference>());
+
+    ExternalRefs(List<Reference> refs) {
+        Collections.sort(refs);  // To get the same serialed rep.
+        mRefs = Collections.unmodifiableList(refs);
+        if (mRefs.size() > 127) {
+            throw new IllegalArgumentException("Too many refs.");
+        }
+    }
+
+    public InputStream toBytes() throws IOException {
+        ByteArrayOutputStream  buffer = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(buffer);
+
+        outputStream.writeByte(mRefs.size());
+        for (Reference ref : mRefs) {
+            outputStream.writeByte(ref.mKind);
+            byte[] raw = ref.mExternalKey.getBytes(IOUtil.UTF8);
+            outputStream.writeShort(raw.length);
+            outputStream.write(raw);
+        }
+        return new ByteArrayInputStream(buffer.toByteArray());
+    }
+
+    public static ExternalRefs fromBytes(InputStream rawBytes) throws IOException {
+        DataInputStream inputStream = new DataInputStream(rawBytes);
+        int count = inputStream.readByte();
+        if (count > 127) {
+            throw new IOException("Parse Error: count out of range [0, 127]");
+        }
+
+        List<Reference> refs = new ArrayList<Reference>();
+        while (count > 0) {
+            int kind = inputStream.readByte();
+            if (count < 0 || count > 127) {
+                throw new IOException("Parse Error: kind out of range [0, 127]");
+            }
+
+            int rawLength = inputStream.readShort();
+            if (rawLength < 0 || rawLength > 32767) {
+                throw new IOException("Parse Error: keyLength out of range [0, 32767]");
+            }
+            byte[] raw = new byte[rawLength];
+            inputStream.readFully(raw);
+            String key = new String(raw, IOUtil.UTF8);
+
+            refs.add(new Reference(kind, key));
+            count--;
+        }
+        return new ExternalRefs(refs);
+    }
+
+    public String pretty() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append("--- ExternalRefs ---\n");
+        for (Reference ref: mRefs) {
+            buffer.append(String.format("   [%d]:%s\n", ref.mKind, ref.mExternalKey));
+        }
+        buffer.append("---");
+        return buffer.toString();
+    }
+
+    public static ExternalRefs create(List<String> keys, int kind) {
+        List<Reference> refs = new ArrayList<Reference>();
+        for (String key : keys) {
+            refs.add(new Reference(kind, key));
+        }
+        return new ExternalRefs(refs);
+    }
+}
diff --git a/alien/src/wormarc/FileManifest.java b/alien/src/wormarc/FileManifest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/FileManifest.java
@@ -0,0 +1,588 @@
+/* A set of files with human readable names, stored in an Archive.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+// DCI: catch null pointers in all public interfaces?
+// DCI: helperfunc nullCheck(refValue, "refValue"); throws on null.
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class FileManifest implements LinkContainer {
+    // Map the digest of a file to the head of the file chain it is stored in.
+    Map<LinkDigest, LinkDigest> mFileDigestToChainHeadDigest = new HashMap<LinkDigest, LinkDigest>();
+    // Map a human readable name to the digest of a file
+    Map<String, LinkDigest> mNameToFileDigest = new HashMap<String, LinkDigest>();
+
+    // There is no cleanup contract.
+    // This may leave a puss oozing sore behind on failure.
+
+    // Names are just strings.
+    // Mapping them to directories is implementation dependent.
+    // Empty directory insertion? Handled below the line of this interface.
+    public interface IO {
+        // Can OPTIONALLY send hash.
+        // ShaDigest.NULL_DIGEST is allowed to indicate DUNNO hash yet.
+        Map<String, LinkDigest> getFiles() throws IOException;
+        InputStream getFile(String name) throws IOException;
+
+        void putFile(String name, InputStream rawBytes) throws IOException;
+        void deleteFile(String name) throws IOException;
+
+        // DCI: think. start and end sync are hacks so I can implment empty dir
+        // handling below the line.
+        void startSync(Set<String> allFiles) throws IOException;
+        void endSync(Set<String> allFiles) throws IOException;
+    }
+
+    // "!H20s20s" + name bytes, H == unsigned short, is full length, including header and name
+    private final static int HEADER_LENGTH = 2 + 2 * 20;
+
+    public InputStream getFile(Archive archive, String name) throws IOException {
+        if (archive == null) {
+            throw new IllegalArgumentException("archive is null");
+        }
+        if (name == null) {
+            throw new IllegalArgumentException("name is null");
+        }
+
+        return archive.getFile(getChainHeadDigest(name));
+    }
+
+    public InputStream getFile(Archive archive, LinkDigest fileDigest) throws IOException {
+        if (archive == null) {
+            throw new IllegalArgumentException("archive is null");
+        }
+        if (fileDigest == null) {
+            throw new IllegalArgumentException("fileDigest is null");
+        }
+
+        LinkDigest chainHeadDigest = mFileDigestToChainHeadDigest.get(fileDigest);
+        if (chainHeadDigest == null) {
+            throw new FileNotFoundException("fileDigest not in the FileManifest");
+        }
+        return archive.getFile(chainHeadDigest);
+    }
+
+    // should call purge() when done putting files
+    public LinkDigest putFile(Archive archive, String name, InputStream rawBytes) throws IOException {
+        if (archive == null) {
+            throw new IllegalArgumentException("archive is null");
+        }
+        if (name == null) {
+            throw new IllegalArgumentException("name is null");
+        }
+        if (rawBytes == null) {
+            throw new IllegalArgumentException("rawBytes is null");
+        }
+
+        return putFile(archive, name, null, rawBytes);
+    }
+
+    // should call purge() when done putting files
+    // prevFileDigest == null means lookup from name. INTENT: Allow cheap renaming/forking
+    public LinkDigest putFile(Archive archive, String name, LinkDigest prevFileDigest, InputStream rawBytes)
+        throws IOException {
+        if (archive == null) {
+            throw new IllegalArgumentException("archive is null");
+        }
+        if (name == null) {
+            throw new IllegalArgumentException("name is null");
+        }
+        if (rawBytes == null) {
+            throw new IllegalArgumentException("rawBytes is null");
+        }
+
+        LinkDigest prevChain = LinkDigest.NULL_DIGEST;
+        if (prevFileDigest == null) {
+            prevFileDigest = mNameToFileDigest.get(name);
+            if (prevFileDigest != null) {
+                prevChain = mFileDigestToChainHeadDigest.get(prevFileDigest);
+            }
+        }
+
+        MessageDigest insertedFileDigest = null;
+        try {
+            insertedFileDigest = MessageDigest.getInstance("SHA");
+        } catch (NoSuchAlgorithmException nsae) {
+            rawBytes.close();
+            throw new IOException("Couldn't load SHA1 algorithm");
+        }
+
+        DigestInputStream wrappedInput = new DigestInputStream(rawBytes, insertedFileDigest);
+        LinkDigest newChainHead = archive.putFile(wrappedInput, prevChain);  // closes on failure.
+        LinkDigest newFileDigest = new LinkDigest(insertedFileDigest.digest());
+        // DCI: need purge() to delete the old mappings?
+
+        // Reuse copies of the same file inserted under different names.
+        if (!mFileDigestToChainHeadDigest.containsKey(newFileDigest)) {
+            mFileDigestToChainHeadDigest.put(newFileDigest, newChainHead);
+        }
+        mNameToFileDigest.put(name, newFileDigest);
+        return newFileDigest;
+    }
+
+    public void removeFiles(List<String> names) {
+        if (names == null) {
+            throw new IllegalArgumentException("names is null");
+        }
+        boolean changed = false;
+        for (String name : names) {
+            if (name == null) {
+                throw new IllegalArgumentException("names contains a null entry.");
+            }
+            if (mNameToFileDigest.containsKey(name)) {
+                mNameToFileDigest.remove(name);
+                changed = true;
+            }
+        }
+        if (changed) {
+            purge();
+        }
+    }
+
+    // DCI: think. Making the initial copy is cheap. making a change that
+    //      causes the chain to shorten may be really expensive.. hmmmm....
+    // DCI: Not legal when not updating
+    // Cheap.
+    // Overwrites.
+    public void copy(String fromName, String toName) throws IOException {
+        if (fromName == null) {
+            throw new IllegalArgumentException("fromName is null");
+        }
+        if (toName == null) {
+            throw new IllegalArgumentException("toName is null");
+        }
+
+        mNameToFileDigest.put(toName, getFileDigest(fromName));
+    }
+
+    // DCI: not legal when not updating
+    // Cheap.
+    public void rename(String fromName, String toName) throws IOException {
+        if (fromName == null) {
+            throw new IllegalArgumentException("fromName is null");
+        }
+        if (toName == null) {
+            throw new IllegalArgumentException("toName is null");
+        }
+
+        mNameToFileDigest.put(toName, getFileDigest(fromName));
+        mNameToFileDigest.remove(fromName);
+    }
+
+    // DCI: Better name?, document
+    // Remove ophaned file digest entries.
+    public void purge() {
+        Set<LinkDigest> referencedFileDigests = new HashSet<LinkDigest>(mNameToFileDigest.values());
+        Set<LinkDigest> knownFileDigests = new HashSet<LinkDigest>(mFileDigestToChainHeadDigest.keySet());
+        if (!knownFileDigests.containsAll(referencedFileDigests)) {
+            throw new RuntimeException("Assertion Failure: Unresolved fileDigest links");
+        }
+        knownFileDigests.removeAll(referencedFileDigests);
+        if (knownFileDigests.size() == 0) {
+            return;
+        }
+        for (LinkDigest key : knownFileDigests) {
+            mFileDigestToChainHeadDigest.remove(key);
+        }
+    }
+
+    public boolean contains(String name) {
+        return mNameToFileDigest.containsKey(name);
+    }
+
+    public boolean contains(LinkDigest fileDigest) {
+        return mFileDigestToChainHeadDigest.containsKey(fileDigest);
+    }
+
+    public LinkDigest getChainHeadDigest(String name) throws IOException {
+        LinkDigest chainHeadDigest = mFileDigestToChainHeadDigest.get(getFileDigest(name));
+        if (chainHeadDigest == null) {
+            // i.e. The name is in the name map, but the file digest it points to can't be found.
+            throw new IOException("Badly formed or corrupt file manifest");
+        }
+        return chainHeadDigest;
+    }
+
+    public LinkDigest getFileDigest(String name) throws IOException {
+        LinkDigest fileDigest = mNameToFileDigest.get(name);
+        if (fileDigest == null) {
+            throw new FileNotFoundException(String.format("File doesn't exist in the archive: %s", name));
+        }
+        return fileDigest;
+    }
+
+    public Map<String, LinkDigest> getMap() {
+        return Collections.unmodifiableMap(mNameToFileDigest);
+    }
+
+    // DCI: rationalize names. e.g. getFiles()?
+    public Set<String> allFiles() {
+        return new HashSet<String>(mNameToFileDigest.keySet());
+    }
+
+    // May contain spurious values if you don't call purge.
+    public Set<LinkDigest> referencedFileDigests() {
+        return new HashSet<LinkDigest>(mFileDigestToChainHeadDigest.keySet());
+    }
+
+    // May contain spurious values if you don't call purge.
+    public Set<LinkDigest> referencedChainHeads() {
+        return new HashSet<LinkDigest>(mFileDigestToChainHeadDigest.values());
+    }
+
+    public InputStream toBytes() throws IOException {
+        // DCI: entire list is built in RAM
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(buffer);
+        List<String> keys = new ArrayList<String>(mNameToFileDigest.keySet());
+        Collections.sort(keys); // Sort so we alwasy get the same bytes for the same list.
+        for (String key : keys) {
+            byte[] name = key.getBytes(IOUtil.UTF8);
+            int length = HEADER_LENGTH + name.length;
+            if (length < 0 || length > 32767) {
+                throw new RuntimeException("Length doesn't fit in a signed short");
+            }
+            outputStream.writeShort(length);
+            LinkDigest fileDigest = mNameToFileDigest.get(key);
+            outputStream.write(fileDigest.getBytes());
+            outputStream.write(mFileDigestToChainHeadDigest.get(fileDigest).getBytes());
+            outputStream.write(name);
+        }
+        outputStream.close();
+        return new ByteArrayInputStream(buffer.toByteArray());
+    }
+
+    // DCI: C&P, move into BinaryLinkRep?
+    private static byte[] readBytes(DataInputStream source, int numberOfBytes) throws IOException {
+        byte[] data = new byte[numberOfBytes];
+        int count = 0;
+        while (count < data.length) {
+            int bytesRead = source.read(data, count, data.length - count);
+            if (bytesRead == -1) {
+                throw new IOException("Unexpected EOF reading bytes");
+            }
+            count += bytesRead;
+        }
+        return data;
+    }
+
+    // Returns a new empty manifest if the FILE_MANIFEST value is NULL_DIGEST.
+    public static FileManifest fromArchiveRootObject(Archive archive) throws IOException {
+        LinkDigest manifestDigest = archive.getRootObject(RootObjectKind.FILE_MANIFEST);
+        if (manifestDigest.isNullDigest()) {
+            return new FileManifest();
+        } else {
+            return fromBytes(archive.getFile(manifestDigest));
+        }
+    }
+
+    // Hmmmm... not possible to stack multiple manifests on the same stream. DCI: fix
+    // DCI: test against python generated binary rep.
+    // Closes rawBytes.
+    public static FileManifest fromBytes(InputStream rawBytes) throws IOException {
+        FileManifest manifest = new FileManifest();
+        DataInputStream inputStream = new DataInputStream(rawBytes);
+        boolean eofAllowed = true;
+        try {
+            while (true) {
+                int length = inputStream.readShort();
+                eofAllowed = false;
+                LinkDigest fileDigest = BinaryLinkRep.readLinkDigest(inputStream);
+                LinkDigest chainHeadDigest = BinaryLinkRep.readLinkDigest(inputStream);
+                String name = new String(readBytes(inputStream, length - HEADER_LENGTH), IOUtil.UTF8);
+                manifest.mFileDigestToChainHeadDigest.put(fileDigest, chainHeadDigest);
+                manifest.mNameToFileDigest.put(name, fileDigest);
+                eofAllowed = true;
+            }
+        } catch (EOFException eof) {
+            if (!eofAllowed) {
+                throw new EOFException("Unexpected EOF reading manifest. Corrupt?");
+            }
+        } finally {
+            rawBytes.close();
+        }
+        return manifest;
+    }
+
+    public static final class Changes {
+        public final Set<String> mDeleted;
+        public final Set<String> mAdded;
+        public final Set<String> mModified;
+        public final Set<String> mUnmodified;
+
+        // PUNT for now. takes a little work.
+        // The LinkDigest -> name inverse map isn't guaranteed to be one to one.
+        // public final Set<StringPair> mRenamed; // Better mForked.
+        public Changes(Set<String> deleted, Set<String> added,
+                       Set<String> modified, Set<String> unmodified) {
+            mDeleted = deleted;
+            mAdded = added;
+            mModified = modified;
+            mUnmodified = unmodified;
+        }
+
+        public boolean isUnmodified() {
+            return mDeleted.isEmpty() && mAdded.isEmpty() && mModified.isEmpty();
+        }
+
+        public String toString() {
+            return String.format("{mDeleted=%s, mAdded=%s, mModified=%s, mUnmodified=%s}",
+                                 mDeleted,
+                                 mAdded,
+                                 mModified,
+                                 mUnmodified);
+
+        }
+
+    }
+
+    public static void debugDump(Map<String, LinkDigest> fileMap, String msg) {
+        if (msg == null) {
+            msg = "";
+        }
+        System.err.println(String.format("--- Dumping file map: %s ---", msg));
+        List<String> names = new ArrayList<String>(fileMap.keySet());
+        Collections.sort(names);
+        for (String name : names) {
+            System.err.println(String.format("%s -> %s", name, fileMap.get(name)));
+        }
+        System.err.println("---");
+    }
+
+    // DCI: really not built into the java libraries somewhere?
+    // Determines the changes that you must apply to the oldMap
+    // to transform it into the newMap.
+    public static Changes diff(Map<String, LinkDigest> oldMap,
+                               Map<String, LinkDigest> newMap) {
+        Set<String> ourKeys = getKeys(oldMap);
+        Set<String> otherKeys = getKeys(newMap);
+
+        Set<String> unchangedNames = new HashSet<String>(ourKeys);
+        unchangedNames.retainAll(otherKeys); // Intersection.
+
+        // Deleted
+        Set<String> deletedNames = new HashSet<String>(ourKeys);
+        deletedNames.removeAll(otherKeys); // Subtraction.
+
+        // Added
+        Set<String> addedNames = new HashSet<String>(otherKeys);
+        addedNames.removeAll(ourKeys); // Subtraction.
+
+        // Modified / Unmodified.
+        Set<String> modifiedNames = new HashSet<String>();
+        Set<String> unmodifiedNames = new HashSet<String>();
+        for (String name : unchangedNames) {
+            if (oldMap.get(name).equals(newMap.get(name))) {
+                unmodifiedNames.add(name);
+            } else {
+                modifiedNames.add(name);
+            }
+        }
+        return new Changes(deletedNames, addedNames, modifiedNames, unmodifiedNames);
+    }
+
+    ////////////////////////////////////////////////////////////
+    protected void doPreCommitCleanup() throws IOException {
+        // DCI: cleanup filedigest -> headDigest so that files with multiple names
+        //      point to the shortest chain.
+        // DCI: Check for possible reinsert win.  i.e. full re-insert  smaller than delta?
+        //      Doesn't deltacoder do that?
+        // DCI: Optional chain shortening?
+    }
+
+    // DCI: cleanup c&p
+    public Changes diffTo(Archive archive, IO newer) throws IOException  {
+        Map<String, LinkDigest> otherMap = new HashMap<String, LinkDigest>(newer.getFiles());
+        for (String name : getKeys(otherMap)) {
+            LinkDigest fileDigest = otherMap.get(name);
+            if (fileDigest == null) {
+                throw new IOException("FileManifest.IO returned a null fileDigest in the getFiles() map.");
+            }
+            if (fileDigest.isNullDigest()) {
+                // DCI: Think.  Way to prevent double read?
+                // Fixup missing hashes.
+                otherMap.put(name, IOUtil.getFileDigest(newer.getFile(name)));
+            }
+        }
+
+        return diff(mNameToFileDigest, otherMap);
+    }
+
+    // DCI: startSync, endSync
+    public Changes syncFilesTo(Archive archive, IO sink) throws IOException {
+
+        sink.startSync(getKeys(mNameToFileDigest));
+
+        HashMap<String, LinkDigest> oldMap = new HashMap<String, LinkDigest>(sink.getFiles());
+
+        // Hack so we don't sha1 hash files which are going to be deleted.
+        Set<String> deletedNames = getKeys(oldMap); // MUST copy keySet() is a reference not a copy!
+        deletedNames.removeAll(mNameToFileDigest.keySet());
+        for (String name : getKeys(oldMap)) { // DCI: use itr that allows deletion instead?
+            if (deletedNames.contains(name)) {
+                oldMap.remove(name);
+                continue;
+            }
+
+            LinkDigest fileDigest = oldMap.get(name);
+            if (fileDigest == null) {
+                throw new IOException("FileManifest.IO returned a null fileDigest in the getFiles() map.");
+            }
+            if (fileDigest.isNullDigest()) {
+                // DCI: test this code path.
+                // DCI: Think.  Way to prevent double read?
+                // Fixup missing hashes.
+                oldMap.put(name, IOUtil.getFileDigest(sink.getFile(name)));
+            }
+        }
+
+        Changes changes = diff(oldMap, mNameToFileDigest);
+
+        changes = new Changes(deletedNames, changes.mAdded, changes.mModified, changes.mUnmodified);
+
+        if (changes.isUnmodified()) {
+            return new Changes(deletedNames, changes.mAdded, changes.mModified, changes.mUnmodified);
+        }
+
+        for (String name : changes.mDeleted) { // NOT changes.
+            sink.deleteFile(name);
+        }
+
+        Set<String> updatedNames = new HashSet<String>();
+        updatedNames.addAll(changes.mAdded);
+        updatedNames.addAll(changes.mModified);
+        for (String name : updatedNames) {
+            sink.putFile(name, getFile(archive, name));
+        }
+
+        sink.endSync(getKeys(mNameToFileDigest));
+
+        return changes;
+    }
+
+    public Changes updateFrom(Archive archive, IO source) throws IOException {
+        if (!archive.isUpdating()) {
+            throw new IllegalStateException("!archive.isUpdating()");
+        }
+
+        Map<String, LinkDigest> otherMap = new HashMap<String, LinkDigest>(source.getFiles());
+        for (String name : getKeys(otherMap)) {
+            LinkDigest fileDigest = otherMap.get(name);
+            if (fileDigest == null) {
+                throw new IOException("FileManifest.IO returned a null fileDigest in the getFiles() map.");
+            }
+            if (fileDigest.isNullDigest()) {
+                // DCI: Think.  Way to prevent double read?
+                // Fixup missing hashes.
+                otherMap.put(name, IOUtil.getFileDigest(source.getFile(name)));
+            }
+        }
+
+        Changes changes = diff(mNameToFileDigest, otherMap);
+
+        if (changes.isUnmodified()) {
+            return changes;
+        }
+
+        removeFiles(new ArrayList<String>(changes.mDeleted));
+        for (String added : changes.mAdded) {
+            putFile(archive, added, source.getFile(added));
+        }
+        for (String modified : changes.mModified) {
+            putFile(archive, modified, source.getFile(modified));
+        }
+
+        doPreCommitCleanup();
+        return changes;
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Every link referenced by every file.
+    public Set<LinkDigest> getReferencedLinks(HistoryLinkMap linkMap) {
+        purge();
+        Set<LinkDigest> links = new HashSet<LinkDigest>();
+        for (LinkDigest head : mFileDigestToChainHeadDigest.values()) {
+            if (head == null) {
+                throw new RuntimeException("head is null.");
+            }
+            // DCI: better way to filter?
+            for (HistoryLink link : linkMap.getChain(head, true)) {
+                links.add(link.mHash);
+            }
+        }
+        return links;
+    }
+
+    // Deep copy.
+    static Set<String> getKeys(Map<String, LinkDigest> source) {
+        return new HashSet<String>(source.keySet());
+    }
+
+    public String pretty(Archive archive) {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append("--- FileManifest ---\n");
+        try {
+            List<String> names = new ArrayList<String>(mNameToFileDigest.keySet());
+            Collections.sort(names);
+            for (String name : names) {
+                buffer.append("   ");
+                buffer.append(getFileDigest(name));
+                buffer.append(" : [");
+                // DCI: add full file length
+                buffer.append(name);
+                buffer.append("]\n");
+                for (LinkDigest digest : archive.getChain(getChainHeadDigest(name), true)) {
+                    buffer.append("      ");
+                    buffer.append(archive.prettyLink(digest.toString()));
+                    buffer.append("\n");
+                }
+            }
+        } catch (IOException ioe) {
+            buffer.append("FAILED WITH IO ERROR!");
+        }
+
+        buffer.append("---\n");
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/HistoryLink.java b/alien/src/wormarc/HistoryLink.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/HistoryLink.java
@@ -0,0 +1,106 @@
+/* A single delta coded change to a file.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+// DCI:  rationalize argument order. parent, sha, datalength, data
+// Hmmm... this doesn't belong in the public interface... don't want to add a virtual to HistoryLink
+// to hide it.
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+import java.security.MessageDigest;
+
+public final class HistoryLink {
+    public final long mDataLength; // DCI: DOCUMENT
+    public final boolean mIsEnd;
+    // DCI: design flaw: what you really want is an immutable array
+    // DCI: rename to mDigest?
+    public final LinkDigest mHash; // DCI: no way to DECLARE fixed length in java?
+    public final LinkDigest mParent;
+    public final LinkData mData;
+    // DCI: make protected?
+    //DCI: fix equality / hash
+    public HistoryLink(final long dataLength,
+                       final boolean isEnd,
+                       final LinkDigest linkDigest,
+                       final LinkDigest parent,
+                       final LinkData data) {
+        mDataLength = dataLength;
+        mIsEnd = isEnd;
+        mHash = linkDigest;
+        mParent = parent;
+        mData = data;
+        if (mParent.isNullDigest() && !mIsEnd) {
+            throw new IllegalArgumentException("parent is NULL_DIGEST but not isEnd???: " + linkDigest);
+        }
+    }
+
+    public String toString() {
+        final StringBuffer buf = new StringBuffer(64);
+        buf.append("{mHash=");
+        buf.append(mHash);
+        buf.append(", mParent=");
+        buf.append(mParent);
+        buf.append(", mDataLength=");
+        buf.append(Long.toString(mDataLength));
+        buf.append(", mIsEnd=");
+        buf.append(Boolean.toString(mIsEnd));
+        buf.append('}');
+        return buf.toString();
+    }
+
+    // Caller must close.
+    public InputStream inputStream() throws IOException {
+        return mData.openInputStream();
+    }
+
+    public void copyTo(final OutputStream destination) throws IOException {
+        mData.copyTo(destination);
+    }
+
+    public byte[] copyTo(final byte[] destination) throws IOException {
+        return mData.copyTo(destination);
+    }
+
+    public static HistoryLink makeLink(final long dataLength,
+                                       final boolean isEnd,
+                                       final LinkDigest parent,
+                                       final InputStream dataStream,
+                                       final LinkDataFactory linkDataFactory) throws IOException {
+
+        final MessageDigest sha1 = BinaryLinkRep.createDigestForLink(dataLength, isEnd, parent);
+        final LinkData linkData = linkDataFactory.makeLinkData(dataStream,
+                                                               dataLength,
+                                                               sha1);
+        return new HistoryLink(dataLength,
+                               isEnd,
+                               new LinkDigest(sha1.digest()),
+                               parent, linkData);
+    }
+
+    // RepInvariant -- checks mHash
+}
+
diff --git a/alien/src/wormarc/HistoryLinkMap.java b/alien/src/wormarc/HistoryLinkMap.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/HistoryLinkMap.java
@@ -0,0 +1,224 @@
+/* A class to manage mapping LinkDigest references to HistoryLink instances.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.List;
+
+public class HistoryLinkMap {
+    // DCI: fix serialVersionUID
+    // DCI: Hmmmm...memory use can grow without bound...
+    private Map<LinkDigest,HistoryLink> mMap = new HashMap<LinkDigest,HistoryLink>();
+
+    ////////////////////////////////////////////////////////////
+    public static class LinkNotFoundException extends RuntimeException {
+        LinkNotFoundException(LinkDigest digest) {
+            super(String.format("HistoryLink: %s not found", digest.toString()));
+        }
+        public void rethrowAsIOException() throws IOException {
+            throw new IOException(getMessage());
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Nested helper classes for iteration.
+    final class LinkLookupIterator implements Iterator<HistoryLink> {
+        final HistoryLinkMap mLinkMap;
+        final Iterator<LinkDigest> mDigestIterator;
+
+        LinkLookupIterator(HistoryLinkMap linkMap, Iterator<LinkDigest> digestIterator) {
+            mLinkMap = linkMap;
+            mDigestIterator = digestIterator;
+        }
+
+        public final boolean hasNext() { return mDigestIterator.hasNext(); }
+        public final HistoryLink next() {
+            // Let LinkNotFoundException propagate.
+            return mLinkMap.getLink(mDigestIterator.next());
+        }
+        public final void remove() { throw new RuntimeException("Not implemented"); }
+    }
+
+    final class LinkLookupIterable implements Iterable<HistoryLink> {
+        final HistoryLinkMap mLinkMap;
+        final Iterable<LinkDigest> mDigestIterable;
+
+        LinkLookupIterable(HistoryLinkMap linkMap, Iterable<LinkDigest> digestIterable) {
+            mLinkMap = linkMap;
+            mDigestIterable = digestIterable;
+        }
+
+        public Iterator<HistoryLink> iterator() {
+            return new LinkLookupIterator(mLinkMap, mDigestIterable.iterator());
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    private final static void throwIfNullOrNullDigest(LinkDigest linkDigest) {
+        if (linkDigest != null) {
+            if (linkDigest.isNullDigest()) {
+                throw new IllegalArgumentException("linkDigest was NULL_DIGEST");
+            }
+            return;
+        }
+        throw new IllegalArgumentException("linkDigest was null");
+    }
+
+    private final static void throwIfNull(LinkDigest linkDigest) {
+        if (linkDigest != null) {
+            return;
+        }
+        throw new IllegalArgumentException("linkDigest was null");
+    }
+
+    private final static void throwIfNull(Iterable<LinkDigest> linkDigest) {
+        if (linkDigest == null) {
+            throw new IllegalArgumentException("linkDigests is null");
+        }
+    }
+
+    public boolean contains(LinkDigest linkDigest) {
+        throwIfNull(linkDigest);
+        return mMap.containsKey(linkDigest);
+    }
+
+    public HistoryLink getLink(LinkDigest linkDigest) {
+        throwIfNull(linkDigest);
+        HistoryLink link = mMap.get(linkDigest);
+        if (link == null) {
+            throw new LinkNotFoundException(linkDigest);
+        }
+        return link;
+    }
+
+    public Iterable<HistoryLink> getLinks(List<LinkDigest> linkDigests) {
+        throwIfNull(linkDigests);
+        return new LinkLookupIterable(this, linkDigests);
+    }
+
+    public List<HistoryLink> getChain(LinkDigest linkDigest, boolean stopAtEnd) {
+        throwIfNullOrNullDigest(linkDigest);
+        
+        //System.out.println("Starting: " + linkDigest);
+        ArrayList<HistoryLink> links = new ArrayList<HistoryLink>();
+        HistoryLink link = new HistoryLink(0, false, null, linkDigest, null);
+        int traversalCount = 33; // Hmmm
+        while (!link.mParent.isNullDigest() &&
+               (!(stopAtEnd && link.mIsEnd))) {
+            //System.out.println("Looking up parent: " + link);
+            link = getLink(link.mParent);
+            links.add(link);
+            traversalCount--;
+            if (traversalCount == 0 && !stopAtEnd) {
+                throw new RuntimeException("getChain() gave up.  Possible loop: " + linkDigest);
+            }
+        }
+        //System.out.println("Exiting: " + linkDigest);
+        return links;
+    }
+
+    public void addLink(HistoryLink link) {
+        if (link == null || link.mHash == null) {
+            throw new IllegalArgumentException("Bad HistoryLink");
+        }
+        mMap.put(link.mHash, link);
+    }
+
+    public void addLinks(Iterable<HistoryLink> links) {
+        if (links == null) {
+            throw new IllegalArgumentException("links is null");
+        }
+        for (HistoryLink link : links) {
+            if (link == null || link.mHash == null) {
+                throw new IllegalArgumentException("Bad HistoryLink");
+            }
+            mMap.put(link.mHash, link);
+        }
+    }
+
+    public void removeLink(LinkDigest linkDigest) {
+        throwIfNull(linkDigest);
+        mMap.remove(linkDigest);
+    }
+
+    public void removeLinks(Iterable<LinkDigest> linkDigests) {
+        throwIfNull(linkDigests);
+        for (LinkDigest linkDigest : linkDigests) {
+            mMap.remove(linkDigests);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Access to underlying map.
+    public Map<LinkDigest,HistoryLink> getUnmodifiableMap() {
+        return Collections.unmodifiableMap(mMap);
+    }
+
+    public void putAll(Map<LinkDigest,HistoryLink> otherMap) {
+        mMap.putAll(otherMap);
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Block helper functions.
+
+    // Get the length of the binary rep of the block.
+    public long getLength(List<LinkDigest> digests) throws IOException {
+        return BinaryLinkRep.getRepLength(getLinks(digests));
+    }
+
+    public Iterable<HistoryLink> getLinks(Block block) throws IOException {
+        return getLinks(block.getDigests());
+    }
+
+    // Get the length of the binary rep of the block.
+    public long getLength(Block block) throws IOException {
+        return BinaryLinkRep.getRepLength(getLinks(block.getDigests()));
+    }
+
+    // Read the binary rep of the block.
+    public InputStream getBinaryRep(Block block) throws IOException {
+        return BinaryLinkRep.toBytes(getLinks(block.getDigests()));
+    }
+
+    public Block readFrom(InputStream rawByteStream,
+                          LinkDataFactory linkDataFactory) throws IOException {
+
+        Block block = new Block();
+        // Force iteration over the entire list before adding to the map.
+        ArrayList<HistoryLink> links = new ArrayList<HistoryLink>(BinaryLinkRep.readAll(rawByteStream, linkDataFactory));
+        for (HistoryLink link : links) {
+            block.append(link.mHash);
+            addLink(link);
+        }
+
+        return block;
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/IOUtil.java b/alien/src/wormarc/IOUtil.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/IOUtil.java
@@ -0,0 +1,235 @@
+/* A collection of IO utility functions.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import java.util.Random;
+
+public class IOUtil {
+    private final static Random sRandom = new Random();
+    private final static int BUF_LEN = 1024 * 32;
+
+    // Don't change without reviewing code.
+    public final static String UTF8 = "utf8";
+
+    public static void silentlyClose(InputStream stream) {
+        if (stream == null) {
+            return;
+        }
+        try {
+            stream.close();
+        } catch (IOException ioe) {
+            // NOP
+        }
+    }
+
+    public final static void copyAndClose(InputStream fromStream, OutputStream toStream) throws IOException {
+        if (fromStream == null || toStream == null) {
+            throw new IllegalArgumentException();
+        }
+
+        try {
+            byte[] buffer = new byte[BUF_LEN];
+            while (true) {
+                int bytesRead = fromStream.read(buffer);
+                if (bytesRead == -1) {
+                    return;
+                }
+                toStream.write(buffer, 0, bytesRead);
+            }
+        }
+        finally {
+            try {
+                fromStream.close();
+            }
+            finally {
+                toStream.close();
+            }
+        }
+    }
+
+    // Closes stream.
+    public final static LinkDigest getFileDigest(InputStream fromStream) throws IOException {
+        MessageDigest sha1 = null;
+        try {
+            try {
+                sha1 = MessageDigest.getInstance("SHA");
+            }
+            catch (NoSuchAlgorithmException nsae) {
+                throw new IOException("Couldn't load SHA1 algorithm.");
+            }
+
+            // DCI: Better to use a wrapper stream filter from the java crypto lib?
+            byte[] buffer = new byte[BUF_LEN];
+            while (true) {
+                int bytesRead = fromStream.read(buffer);
+                if (bytesRead == -1) {
+                    break;
+                }
+                sha1.update(buffer, 0, bytesRead);
+            }
+            return new LinkDigest(sha1.digest());
+        }
+        finally {
+            fromStream.close();
+        }
+    }
+
+    public final static void copyAndClose(InputStream fromStream, String toFileName) throws IOException {
+        copyAndClose(fromStream, new FileOutputStream(toFileName));
+    }
+
+    public final static byte[] readAndClose(InputStream fromStream) throws IOException {
+        if (fromStream == null) {
+            throw new IllegalArgumentException();
+        }
+
+        ByteArrayOutputStream toStream = new ByteArrayOutputStream();
+        copyAndClose(fromStream, toStream);
+
+        byte[] bytes = toStream.toByteArray();
+        return bytes;
+    }
+
+    public final static byte[] readFully(String inputFile) throws IOException {
+        return readAndClose(new FileInputStream(inputFile));
+    }
+
+    public final static void writeFully(byte[] data, String outputFile) throws IOException {
+        copyAndClose(new ByteArrayInputStream(data), new FileOutputStream(outputFile));
+    }
+
+    public final static String readUtf8StringAndClose(InputStream fromStream) throws IOException {
+        ByteArrayOutputStream toStream = new ByteArrayOutputStream();
+        copyAndClose(fromStream, toStream);
+        return new String(toStream.toByteArray(), UTF8);
+    }
+
+    public final static InputStream toStreamAsUtf8(String value) throws IOException {
+        return new ByteArrayInputStream(value.getBytes(UTF8));
+    }
+
+    // ATTRIBUTION: mmyers, SO
+    // http://stackoverflow.com/questions/140131/convert-a-string-representation-of-a-hex-dump-to-a-byte-array-using-java
+    public final static byte[] fromHexString(final String encoded) {
+        if ((encoded.length() % 2) != 0)
+            throw new IllegalArgumentException("Input string must contain an even number of characters");
+
+        final byte result[] = new byte[encoded.length()/2];
+        final char enc[] = encoded.toCharArray();
+        for (int i = 0; i < enc.length; i += 2) {
+            StringBuilder curr = new StringBuilder(2);
+            curr.append(enc[i]).append(enc[i + 1]);
+            result[i/2] = (byte) Integer.parseInt(curr.toString(), 16);
+        }
+        return result;
+    }
+
+    // ATTRIBUTION: Peter Lawrey, SO
+    // http://stackoverflow.com/questions/332079/in-java-how-do-i-convert-a-byte-array-to-a-string-of-hex-digits-while-keeping-le
+    public final static String toHexString(byte[] bytes, int maxBytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%1$02x", b));
+            maxBytes -= 1;
+            if (maxBytes == 0) {
+                break;
+            }
+        }
+        return sb.toString();
+    }
+
+    public final static String randomHexString(int length) {
+        if (length < 1) {
+            throw new IllegalArgumentException();
+        }
+        byte[] bytes = new byte[length];
+        sRandom.nextBytes(bytes);
+        return toHexString(bytes, bytes.length);
+    }
+
+    // ATTRIBUTION: erikson, SO
+    // http://stackoverflow.com/questions/779519/delete-files-recursively-in-java
+    public final static void delete(File f) throws IOException {
+        if (f.isDirectory()) {
+            for (File c : f.listFiles()) {
+                delete(c);
+            }
+        }
+        if (!f.delete()) {
+            throw new FileNotFoundException("Failed to delete file: " + f);
+        }
+    }
+    public final static void delete(String fileOrDirectory) throws IOException { delete( new File(fileOrDirectory) ); }
+
+    ////////////////////////////////////////////////////////////
+    // Binary IO
+    ////////////////////////////////////////////////////////////
+
+    public static byte[] readBytes(DataInputStream source, int numberOfBytes) throws IOException {
+        byte[] data = new byte[numberOfBytes];
+        int count = 0;
+        while (count < data.length) {
+            int bytesRead = source.read(data, count, data.length - count);
+            if (bytesRead == -1) {
+                throw new IOException("Unexpected EOF reading bytes");
+            }
+            count += bytesRead;
+        }
+        return data;
+    }
+
+    // DCI: remove? is there other code that should be using this?
+    // public static void writeString(DataOutputStream outputStream, String value) throws IOException {
+    //     byte[] bytes = value.getBytes(UTF8);
+    //     if (bytes.length > 32767) { // DCI: is this required?
+    //         throw new IOException("Length doesn't fit in a signed short");
+    //     }
+
+    //     outputStream.writeShort(bytes.length);
+    //     outputStream.write(bytes);
+    // }
+
+    public static String readString(DataInputStream source) throws IOException {
+        int length = source.readShort();
+        if (length > 32767) { // DCI: is this required?
+            throw new IOException("Length doesn't fit in a signed short");
+        }
+        return new String(readBytes(source, length), UTF8);
+    }
+}
diff --git a/alien/src/wormarc/LinkContainer.java b/alien/src/wormarc/LinkContainer.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/LinkContainer.java
@@ -0,0 +1,32 @@
+/* An interface for classes which can contain references to HistoryLinks.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.util.Set;
+
+// DCI: What else besides FileManifest?
+interface LinkContainer {
+    Set<LinkDigest> getReferencedLinks(HistoryLinkMap linkMap);
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/LinkData.java b/alien/src/wormarc/LinkData.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/LinkData.java
@@ -0,0 +1,38 @@
+/* An abstract interface to the backing store used to store the data for a HistoryLink.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+// DCI: grrrr... overdesigned
+interface LinkData {
+    // Caller must close!
+    InputStream openInputStream() throws IOException; // DCI: needed?
+    void copyTo(OutputStream destination) throws IOException;
+    byte[] copyTo(byte[] destination) throws IOException;
+}
+
diff --git a/alien/src/wormarc/LinkDataFactory.java b/alien/src/wormarc/LinkDataFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/LinkDataFactory.java
@@ -0,0 +1,36 @@
+/* An abstract interface used to create LinkData subclass instances using a particular backing store.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+
+// INTENT: decouple rep of link data from presentation implementation (BinaryRep).
+//        e.g. RAM storage, single file storage, multiple file storage.
+public interface LinkDataFactory {
+    LinkData makeLinkData(InputStream source, long length, MessageDigest messageDigest) throws IOException;
+}
+
diff --git a/alien/src/wormarc/LinkDigest.java b/alien/src/wormarc/LinkDigest.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/LinkDigest.java
@@ -0,0 +1,81 @@
+/* A reference to a HistoryLink.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+*/
+
+package wormarc;
+
+import java.util.Arrays;
+
+// !^*&^$#&^*&^%  no immutable byte[] values in Java!
+public final class LinkDigest implements Comparable<LinkDigest> {
+    private final byte[] mBytes = new byte[20];
+    private final int mHashCode;
+
+    public final static LinkDigest NULL_DIGEST = new LinkDigest(new byte[20]);
+    public final static LinkDigest EMPTY_DIGEST =
+        new LinkDigest("da39a3ee5e6b4b0d3255bfef95601890afd80709"); // hash of "";
+
+    public LinkDigest(String linkHash) {
+        this(IOUtil.fromHexString(linkHash));
+    }
+
+    public LinkDigest(byte[] bytes) {
+        if (bytes.length != 20) {
+            throw new IllegalArgumentException("Raw SHA1 must be 20 bytes");
+        }
+        System.arraycopy(bytes, 0, mBytes, 0, 20);
+        mHashCode = Arrays.hashCode(mBytes);
+    }
+
+    // Deep copy!
+    public byte[] getBytes() {
+        byte[] bytes = new byte[20];
+        System.arraycopy(mBytes, 0, bytes, 0, 20);
+        return bytes;
+    }
+
+    public String hexDigest(int bytes) {
+        return IOUtil.toHexString(mBytes, bytes);
+    }
+
+    public int hashCode() { return mHashCode; }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof LinkDigest))) {
+            return false;
+        }
+        return Arrays.equals(((LinkDigest)obj).mBytes, mBytes);
+    }
+
+    public String toString() {
+        return hexDigest(20);
+    }
+
+    public int compareTo(LinkDigest other) {
+        // DCI: TOO SLOW? use Arrays.equals
+        return hexDigest(20).compareTo(other.hexDigest(20));
+    }
+
+    public boolean isNullDigest() { return equals(NULL_DIGEST); }
+}
+
diff --git a/alien/src/wormarc/PartitioningMath.java b/alien/src/wormarc/PartitioningMath.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/PartitioningMath.java
@@ -0,0 +1,207 @@
+/* Helper functions used to partition an Archive's HistoryLinks into Blocks.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+// DCI: unit test this!
+
+import java.util.ArrayList;
+import java.util.List;
+
+// DCI: javadoc
+public class PartitioningMath {
+    public static final class Partition {
+        final int mStart;
+        final int mEnd;
+        final long mLength;
+
+        public Partition(int start, int end, long length) {
+            mStart = start;
+            mEnd = end;
+            mLength = length;
+        }
+
+        int getStart() { return mStart; }
+        int getEnd() { return mEnd; }
+        long getLength() { return mLength; }
+    }
+
+    public static boolean isOrdered(List<Partition> partitions) {
+        List<Long> lengths = new ArrayList<Long>();
+        for (Partition partition : partitions) {
+            lengths.add(partition.getLength());
+        }
+
+        // Ignore trailing 0 length blocks.
+        while (lengths.size() > 0 && lengths.get(lengths.size() -1) == 0) {
+            lengths.remove(lengths.size() -1);
+        }
+
+        for (int index = 0; index < lengths.size() - 1; index++) {
+            if (lengths.get(index) > lengths.get(index + 1)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Underlying block list is newest to oldest.
+    // Partitions list is newest to oldest.
+    public static boolean isContiguous(List<Partition> partitions) {
+        if (partitions.size() == 0) {
+            return true;
+        }
+
+        // DCI: copied from python. Why again?
+        Partition lastPartition = partitions.get(partitions.size() - 1);
+        if (lastPartition.getStart() > lastPartition.getEnd()) {
+            return false;
+        }
+
+        for (int index = 0; index < partitions.size() - 1; index++) {
+            Partition current = partitions.get(index);
+            if (current.getStart() > current.getEnd()) {
+                return false;
+            }
+            Partition next = partitions.get(index + 1);
+            int span = next.getStart() - current.getEnd();
+            if (span < 0  || span > 1) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static void assertTrue(boolean condition) {
+        if (condition) { return; }
+        throw new RuntimeException("Assertion failure in BlockPartitionMath");
+    }
+
+    // Current is younger than next.
+    public static List<Partition> repartition(List<Partition> partitions, int multiple) {
+        for (int index = 0; index < partitions.size() - 1; index++) {
+            Partition current = partitions.get(index);
+            Partition next = partitions.get(index + 1);
+            if (current.getLength() * multiple >= next.getLength()) {
+                List<Partition> good = new ArrayList<Partition>(partitions.subList(0, index));
+                List<Partition> rest = new ArrayList<Partition>(partitions.subList(index, partitions.size()));
+                Partition restHead = rest.get(0);
+                Partition restNext = rest.get(1);
+                // inherited from python code.
+                //assertTrue((restNext.getStart() - restHead.getEnd() >= 0) && // In order, makes sense.
+                //           (restNext.getStart() - restHead.getEnd() < 2));   // I can't remember why. ???
+                assertTrue(restNext.getStart() - restHead.getEnd() >= 0);
+                rest.set(1,  new Partition(restHead.getStart(), restNext.getEnd(),
+                                           restHead.getLength() + restNext.getLength()));
+                rest.remove(0);
+                good.addAll(repartition(rest, multiple)); // DCI: bug in python here
+                assertTrue(isOrdered(good));
+                // assertTrue(is_contiguous(godd)); // Removed this constraint. Can drop empty partitions.
+                return good;
+            }
+        }
+
+        List<Partition> ret = new ArrayList<Partition> (partitions);
+        assertTrue(isOrdered(ret));
+        return ret;
+    }
+
+    public static List<Partition> compress(List<Partition> partitions, int maxLen, int multiple) {
+        List<Partition> nonZeroLength = new ArrayList<Partition> ();
+        for (Partition partition : partitions) {
+            if (partition.getLength() > 0) {
+                nonZeroLength.add(partition);
+            }
+        }
+         // Deep copy is important.
+        partitions = nonZeroLength;
+
+        if (partitions.size() <= maxLen) {
+            // Doesn't repartition if it didn't compress.
+            return partitions;
+        }
+
+        assertTrue(maxLen > 1);
+
+        while (partitions.size() > maxLen) {
+            Partition head = partitions.get(0);
+            Partition next = partitions.get(1);
+            Partition combined = new Partition(head.getStart(), next.getEnd(),
+                                               head.getLength() + next.getLength());
+            partitions.set(1, combined);
+            partitions = new ArrayList<Partition> (partitions.subList(1, partitions.size()));
+            // Enforce the ordering constraint.
+            partitions = repartition(partitions, multiple);
+        }
+        assertTrue(isOrdered(partitions));
+
+        return partitions;
+    }
+
+    ////////////////////////////////////////////////////////////
+    // Debug helper methods.
+    public static String toString(List<Partition> partitions) {
+        if (partitions.isEmpty()) {
+            return "()";
+        }
+        StringBuilder buf = new StringBuilder();
+        buf.append("(");
+
+        boolean first  = true;
+        for (Partition partition : partitions) {
+            if (!first) {
+                buf.append(", ");
+            }
+            first = false;
+            buf.append("(");
+            buf.append(partition.mStart);
+            buf.append(", ");
+            buf.append(partition.mEnd);
+            buf.append(", ");
+            buf.append(partition.mLength);
+            buf.append(")");
+        }
+
+        buf.append(")");
+
+        return buf.toString();
+    }
+
+    public static boolean equal(List<Partition> listA, List<Partition> listB) {
+        if (listA.size() != listB.size()) {
+            return false;
+        }
+        for (int index = 0; index < listA.size(); index++) {
+            Partition a = listA.get(index);
+            Partition b = listB.get(index);
+            if ((a.mStart != b.mStart) ||
+                (a.mEnd != b.mEnd) ||
+                (a.mLength != b.mLength))  {
+                return false;
+            }
+
+        }
+        return true;
+    }
+}
diff --git a/alien/src/wormarc/RamLinkDataFactory.java b/alien/src/wormarc/RamLinkDataFactory.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/RamLinkDataFactory.java
@@ -0,0 +1,82 @@
+/* A LinkDataFactory which makes LinkData instances that are stored in RAM.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.EOFException;
+import java.security.MessageDigest;
+
+class RamLinkData implements LinkData {
+    private final byte[] mData;
+
+    RamLinkData(byte[] data) {
+        mData = data;
+    }
+
+    public InputStream openInputStream() throws IOException {
+        return new ByteArrayInputStream(mData);
+    }
+
+    public void copyTo(OutputStream destination) throws IOException {
+        destination.write(mData);
+    }
+
+    public byte[] copyTo(byte[] destination) throws IOException {
+        System.arraycopy(mData, 0, destination, 0, destination.length);
+        return destination;
+    }
+}
+
+// INTENT: decouple rep of link data from presentation implementation (BinaryRep).
+//        e.g. RAM storage, single file storage, multiple file storage.
+public class RamLinkDataFactory implements  LinkDataFactory {
+    private static LinkDataFactory mInstance;
+    public LinkData makeLinkData(InputStream source, long length, MessageDigest messageDigest) throws IOException {
+        if (length > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Can't allocate a buffer that big. Sorry :-(");
+        }
+        byte[] data = new byte[(int)length];
+        int count = 0;
+        while (count < data.length) {
+            int bytesRead = source.read(data, count, data.length - count);
+            if (bytesRead == -1) {
+                throw new EOFException("Unexpected EOF reading RamLinkData.");
+            }
+            messageDigest.update(data, count, bytesRead);
+            count += bytesRead;
+        }
+        return new RamLinkData(data);
+    }
+
+    public final static LinkDataFactory instance() {
+        if (mInstance == null) {
+            mInstance = new RamLinkDataFactory();
+        }
+        return mInstance;
+    }
+}
diff --git a/alien/src/wormarc/RootObjectKind.java b/alien/src/wormarc/RootObjectKind.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/RootObjectKind.java
@@ -0,0 +1,65 @@
+/* A class to marshal instances of root object classes from Archive root object LinkDigest references.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.HashSet;
+
+// DCI: better name. RootObject, and pull that out of Archive?
+public class RootObjectKind {
+    public final static int ARCHIVE_MANIFEST = 1;
+    public final static int FILE_MANIFEST = 2;
+    // Use a manifest if there are more than few files.
+    // Otherwise the root objects don't fit in a topkey.
+    public final static int SINGLE_FILE = 3;
+    public final static int PARENT_REFERENCES = 4;
+
+    public static LinkContainer getContainer(Archive archive,
+                                             Archive.RootObject obj) throws IOException {
+        // Hmmmm... consider using polymorphism instead of switch if there are
+        // many more cases.
+        switch (obj.mKind) {
+        case FILE_MANIFEST:
+            return FileManifest.fromBytes(archive.getFile(obj.mDigest));
+
+        case ARCHIVE_MANIFEST: // Drop through on purpose.
+        case SINGLE_FILE:
+        case PARENT_REFERENCES:
+            final Set<LinkDigest> linkSet = new HashSet<LinkDigest>(archive.getChain(obj.mDigest, true));
+            return new LinkContainer() {
+                public Set<LinkDigest> getReferencedLinks(HistoryLinkMap linkMap) {
+                    return linkSet;
+                }
+            };
+        default:
+            return new LinkContainer() {
+                public Set<LinkDigest> getReferencedLinks(HistoryLinkMap linkMap) {
+                    return new HashSet<LinkDigest>();
+                }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/cli/CLI.java b/alien/src/wormarc/cli/CLI.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/cli/CLI.java
@@ -0,0 +1,627 @@
+/* An experimental command line client for manipulating WORMArc archives.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.cli;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+
+
+import fmsutil.FMSUtil;
+import wormarc.Archive;
+import wormarc.AuditArchive;
+import wormarc.FileManifest;
+import wormarc.IOUtil;
+import wormarc.LinkDigest;
+import wormarc.ExternalRefs;
+import wormarc.RootObjectKind;
+
+import wormarc.io.FreenetIO;
+import wormarc.io.FreenetTopKey;
+
+public class CLI {
+    private static final PrintStream sOut = System.out;
+    private final static String FCP_HOST = "127.0.0.1";
+    private final static int FCP_PORT = 19481;
+
+    private static CLICache getCache(boolean createCache) throws IOException {
+        String cwd = (new File(".")).getCanonicalPath();
+        File cacheDir = new File(cwd, ".wormarc");
+
+        if (!(cacheDir.exists() && cacheDir.isDirectory())) {
+            if (!createCache) {
+                throw new IOException("No .wormarc directory here!");
+            }
+            sOut.println(String.format("Creating new archive: %s", cacheDir));
+            cacheDir.mkdir();
+        } else if (createCache) {
+            sOut.println(String.format("The archive already exists!"));
+        }
+        CLICache cache =  new CLICache(cacheDir.getCanonicalPath());
+        String head = null;
+        try {
+            head = cache.readHead();
+        } catch (IOException ioe) {
+            if (!createCache) {
+                sOut.println("Head not set?");
+            }
+            head = "default";
+        }
+        cache.setName(head);
+        cache.saveHead(cache.getName());
+        return cache;
+    }
+
+    private static Archive loadHead(CLICache cache) throws IOException {
+        return Archive.load(cache);
+    }
+
+    private final static void dumpList(Set<String> values, String prefix) {
+        if (values.isEmpty()) {
+            return;
+        }
+        List<String> ordered = new ArrayList<String>(values);
+        Collections.sort(ordered);
+        for (String name : ordered) {
+            sOut.println(String.format("%s %s", prefix, name));
+        }
+    }
+
+    private final static void showChanges(FileManifest.Changes changes, boolean verbose) {
+        dumpList(changes.mDeleted, verbose ? "deleted" : "!");
+        dumpList(changes.mAdded, verbose ? "added" : "A");
+        dumpList(changes.mModified, verbose ? "modified" : "M");
+    }
+
+    private final static LinkDigest getChainHead(Archive archive, String chainId, boolean silent) throws IOException {
+        LinkDigest chainHead = LinkDigest.NULL_DIGEST;
+        try {
+            chainHead = new LinkDigest(chainId);
+        } catch (IllegalArgumentException notAHash) {
+        }
+
+        if (chainHead.isNullDigest()) {
+            try {
+                FileManifest current = FileManifest.fromArchiveRootObject(archive);
+                chainHead = current.getChainHeadDigest(chainId);
+                if (!silent) {
+                    sOut.println("Looked up chain digest from FileManifest: " + chainHead);
+                }
+            } catch (IOException notAFileName) {
+            }
+        }
+
+        if (chainHead.isNullDigest()) {
+            try {
+                chainHead = archive.getRootObject(Integer.parseInt(chainId));
+                if (!silent) {
+                    sOut.println("Looked up as a RootObject ordinal: " + chainHead);
+                }
+            } catch (NumberFormatException notANumber) {
+            }
+        }
+
+        if (chainHead.isNullDigest()) {
+            sOut.println("Try a 20 digit hex string a file name from the FileManifest or a RootObject kind number.");
+        }
+        return chainHead;
+    }
+
+    ////////////////////////////////////////////////////////////
+    static class Command {
+        final String mName;
+        final boolean mNeedsCache;
+        final boolean mCreateCache;
+        final String mBrief;
+        final String mArgs;
+        final String mLonger;
+        Command(String name, boolean needsCache, boolean createCache,
+                String args, String brief, String longer) {
+            mName = name;
+            mNeedsCache = needsCache;
+            mCreateCache = createCache;
+            mArgs = args;
+            mBrief = brief;
+            mLonger = longer;
+        }
+
+        Command(String name, boolean needsCache, boolean createCache, String args, String brief) {
+            this(name, needsCache, createCache, args, brief, null);
+        }
+
+        boolean canParse(String[] args) { return true; }
+        void invoke(String[] args, CLICache cache) throws Exception {};
+    }
+
+    private final static String HELP_TEXT =
+        "wormarc: Write Once Read Multiple ARChive command line client.\n" +
+        "written as part of the fniki Freenet Wiki project\n" +
+        "Copyright (C) 2010, 2011 Darrell Karbott, GPL2 (or later)\n\n" +
+        "SUMMARY:\n" +
+        "This is a command line client to test the wormarc library.\n" +
+        "It is experimental code. Use it at your own peril.\n\n" +
+        "COMMANDS:\n";
+
+    final static Command COMMANDS[] = new Command[] {
+        // MUST be first.
+        new Command("help", false, false, "", "display this message") {
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                System.out.print(HELP_TEXT);
+                Map<String, Command> table = new HashMap<String, Command>();
+                for (Command command : COMMANDS) {
+                    table.put(command.mName, command);
+                }
+                List<String> names = new ArrayList<String>(table.keySet());
+                Collections.sort(names);
+                for (String name : names) {
+                    Command value = table.get(name);
+                    sOut.println(String.format("   %s%s -- %s", value.mName, value.mArgs, value.mBrief));
+                    if (value.mLonger != null) {
+                        sOut.println(value.mLonger);
+                        sOut.println("");
+                    }
+                }
+            }
+        },
+        new Command("cat", true, false, " <chainId>", "print file to stdout") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                String chainId = args[1];
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No head found!");
+                    return;
+                }
+
+                LinkDigest chainHead = getChainHead(archive, chainId, true);
+                if (chainHead.isNullDigest()) {
+                    return;
+                }
+
+                // Hmmmm... potenially closing stdout.
+                IOUtil.copyAndClose(archive.getFile(chainHead), sOut);
+            }
+        },
+        new Command("create", true, true, "", "create the local .wormarc cache directory") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+        },
+        new Command("head", true, false, " <name>", "set the head") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                cache.saveHead(args[1]);
+            }
+        },
+        new Command("branch", true, false, " <name>", "copy the current head to a new name") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                try {
+                    String name = cache.cloneHead(args[1]);
+                    cache.setName(name);
+                    cache.saveHead(name);
+                    sOut.println(String.format("Created branch: %s", name));
+                } catch (FileNotFoundException fne) {
+                    sOut.println("Branch failed. Maybe head doesn't exist?");
+                } catch (IOException ioe) {
+                    sOut.println(String.format("Branch failed. Maybe branch[%s] already exists?", args[1]));
+                }
+            }
+        },
+        new Command("update", true, false, "", "update the local directory to match the head",
+                    "      BE CAREFUL. This command can DELETE ALL FILES in the working directory\n" +
+                    "      when you update from an empty archive!") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No previous head found.");
+                    return;
+                }
+
+                FileManifest current = FileManifest.fromArchiveRootObject(archive);
+                sOut.println(String.format("Synching files from %s to local directory.", cache.getName()));
+                FileManifest.Changes changes = current.syncFilesTo(archive, cache.getManifestIO());
+                showChanges(changes, true);
+            }
+        },
+        new Command("commit", true, false, "", "commit changes to the local directory to the head") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                Archive archive = null;
+                boolean incremental = true;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No previous head found. Doing non-incremental write.");
+                    archive = new Archive();
+                    incremental = false;
+                }
+
+                archive.unsetRootObject(RootObjectKind.PARENT_REFERENCES);
+
+                FileManifest current = FileManifest.fromArchiveRootObject(archive);
+                if (incremental) {
+                    sOut.println(String.format("Previous version: %s", cache.getName()));
+                }
+                archive.startUpdate();
+                FileManifest.Changes changes = current.updateFrom(archive, cache.getManifestIO());
+                showChanges(changes, true);
+                if (changes.isUnmodified()) {
+                    sOut.println("No changes.");
+                    archive.abandonUpdate();
+                    return;
+                }
+
+                try {
+                    List<String> keys = Arrays.asList(cache.readValue("remote"));
+                    LinkDigest refs =
+                        archive.updateRootObject(ExternalRefs.create(keys, ExternalRefs.KIND_FREENET)
+                                                 .toBytes(),
+                                                 RootObjectKind.PARENT_REFERENCES);
+                    sOut.println("set PARENT_REFERENCES with one parent uri:");
+                    sOut.println(keys.get(0));
+                } catch (IOException ioe) {
+                    sOut.println("Couldn't set parent PARENT_REFERENCES");
+                }
+
+                archive.commitUpdate();
+                archive.compressAndUpdateArchiveManifest();
+                cache.bumpOrdinal();
+                archive.write(cache);
+
+                String readName = cache.getName();
+                cache.saveHead(readName);
+                sOut.println(String.format("Wrote version: %s", readName));
+            }
+        },
+
+        new Command("status", true, false, "", "show the status of the local directory") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                String key = "SSK private key not set.";
+                try {
+                    key = cache.readValue("key");
+                } catch (IOException ioe) {
+                    // NOP
+                }
+
+                String remote = "Parent URI not set.";
+                try {
+                    remote = cache.readValue("remote");
+                } catch (IOException ioe) {
+                    // NOP
+                }
+
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("Couldn't read head. Maybe you haven't committed to it yet?");
+                    return;
+                }
+
+                FileManifest current = FileManifest.fromArchiveRootObject(archive);
+                sOut.println(String.format("Directory   : %s", cache.toString()));
+                sOut.println(String.format("Private Key : %s", key));
+                sOut.println(String.format("Remote      : %s", remote));
+                sOut.println(String.format("Head version: %s", cache.getName()));
+                sOut.println("");
+
+                FileManifest.Changes changes = current.diffTo(archive, cache.getManifestIO());
+                showChanges(changes, false);
+                if (changes.isUnmodified()) {
+                    sOut.println("No changes.");
+                }
+            }
+        },
+        new Command("filehistory", true, false, " <chainId>", "show the change history for a single chain") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                String chainId = args[1];
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No head found!");
+                    return;
+                }
+
+                LinkDigest chainHead = getChainHead(archive, chainId, false);
+                if (chainHead.isNullDigest()) {
+                    return;
+                }
+
+                List<LinkDigest> chain = archive.getChain(chainHead, true); // DCI: fails if not cached.
+
+                sOut.println(String.format("Searching for %d links... ", chain.size()));
+
+                FreenetIO freenetResolver = new FreenetIO(FCP_HOST, FCP_PORT, cache);
+
+                List<ExternalRefs.Reference> refs =
+                    AuditArchive.history(archive,
+                                         cache.getHeadRef(),
+                                         chain,
+                                         freenetResolver);
+
+                for (int index = 0; index < chain.size(); index++) {
+                    sOut.println(String.format("  [%s]:%s", freenetResolver.getNym(refs.get(index)),
+                                               chain.get(index)));
+                }
+            }
+        },
+        new Command("manifesthistory", true, false, "", "shows the change history of the manifest") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                ExternalRefs.Reference remote = null;
+                try {
+                    // LATER: parse a command line URI?
+                    remote = new ExternalRefs.Reference(ExternalRefs.KIND_FREENET,
+                                                        cache.readValue("remote"));
+                } catch (IOException ioe) {
+                    sOut.println("Couldn't read remote. Don't know what version to start from.");
+                }
+
+                FreenetIO freenetResolver = new FreenetIO(FCP_HOST, FCP_PORT, cache);
+                Archive archive = freenetResolver.resolve(remote);
+                AuditArchive.ChangeLogCallback callback = new AuditArchive.ChangeLogCallback () {
+                        public boolean onChangeEntry(ExternalRefs.Reference oldVer,
+                                                     ExternalRefs.Reference newVer,
+                                                     FileManifest.Changes  fromNewToOld) {
+
+                            sOut.println("---");
+                            sOut.println("[" + oldVer.mExternalKey + "]");
+                            showChanges(fromNewToOld, true);
+                            sOut.println("---");
+                            return true;
+                        }
+                    };
+
+                AuditArchive.getManifestChangeLog(remote, archive, freenetResolver, callback);
+            }
+        },
+        new Command("key", true, false, " <ssk_private_key>", "Set the default SSK insert key.",
+                    "      This is stored UNENCRYPTED in the .wormarc directory." ) {
+            public boolean canParse(String[] args) { return args.length <= 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                if (args.length == 2) {
+                    checkPrivateKey(args[1]);
+                    cache.saveValue("key", args[1]);
+                } else {
+                    try {
+                        String key = cache.readValue("key");
+                        checkPrivateKey(key);
+                        sOut.println(String.format("key: %s", key));
+                    } catch (IOException ioe) {
+                        sOut.println("Key not set.");
+                    }
+                }
+            }
+        },
+        new Command("remote", true, false, " <ssk_requesturi>", "Sets the parent request uri for the next push.",
+                    "      Use the value 'clear' to unset this." ) {
+            public boolean canParse(String[] args) { return args.length <= 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                if (args.length == 2) {
+                    if (args[1].equals("clear")) {
+                        cache.deleteValue("remote");
+                    } else {
+                        cache.saveValue("remote", args[1]);
+                    }
+                } else {
+                    try {
+                        String remote = cache.readValue("remote");
+                        sOut.println(String.format("remote: %s", remote));
+                    } catch (IOException ioe) {
+                        sOut.println("Remote not set.");
+                    }
+                }
+            }
+        },
+        new Command("dump", true, false, "", "dump debug information about the current head") {
+            public boolean canParse(String[] args) { return args.length == 1; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No head found!");
+                    return;
+                }
+
+                FileManifest current = FileManifest.fromArchiveRootObject(archive);
+                sOut.println(cache);
+                sOut.println(String.format("Head version: %s", cache.getName()));
+                sOut.println(archive.pretty());
+                sOut.println(current.pretty(archive));
+                LinkDigest digest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
+                if (!digest.isNullDigest()) {
+                    ExternalRefs parents = ExternalRefs.fromBytes(archive.getFile(digest));
+                    sOut.println(parents.pretty());
+                } else {
+                    sOut.println("No PARENT_REFERENCES in the root objects!");
+                }
+            }
+        },
+        new Command("push", true, false, " <insert_uri>", "insert the current head into Freenet") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                Archive archive = null;
+                try {
+                    archive = loadHead(cache);
+                } catch (IOException ioe) {
+                    sOut.println("No head found!");
+                    return;
+                }
+
+                String insertUri = args[1];
+                if (!(insertUri.startsWith("SSK@") || insertUri.startsWith("CHK@"))) {
+                    try {
+                        insertUri =  cache.readValue("key") + insertUri;
+                        sOut.println("Used stored private key. Inserting to: ");
+                        sOut.println(insertUri);
+                    } catch (IOException ioe) {
+                        sOut.println("Couldn't prepend private key. Maybe it wasn't set?");
+                        return;
+                    }
+                }
+
+                FreenetIO io = new FreenetIO(FCP_HOST, FCP_PORT, cache);
+                io.setInsertUri(insertUri);
+                sOut.println(String.format("Pushing version: %s to Freenet Insert URI:", cache.getName()));
+                sOut.println(insertUri);
+
+                io.maybeLoadPreviousTopKey(archive);
+
+                archive.write(io);
+
+                sOut.println(String.format("Pushed to: %s", io.getRequestUri()));
+            }
+        },
+        new Command("pull", true, false, " <request_uri>", "read the archive out of freenet. ???") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                String requestUri = args[1];
+                FreenetIO io = new FreenetIO(FCP_HOST, FCP_PORT, cache);
+                io.setRequestUri(requestUri);
+                sOut.println(String.format("Reading: %s", requestUri));
+                Archive archive = Archive.load(io);
+                cache.setName("lastpull.0");
+                archive.write(cache);
+                cache.saveHead(cache.getName());
+                sOut.println(String.format("Read and switched head to: %s", cache.getName()));
+                cache.saveValue("remote", requestUri);
+                sOut.println(String.format("Set remote to: %s", requestUri));
+            }
+        },
+        new Command("topkey", true, false, " <request_uri>", "Dump the contents of a top key.") {
+            public boolean canParse(String[] args) { return args.length == 2; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                String requestUri = args[1];
+                FreenetIO io = new FreenetIO(FCP_HOST, FCP_PORT, cache);
+                sOut.println(String.format("Reading Top Key: %s", requestUri));
+                FreenetTopKey topKey = io.readTopKey(requestUri);
+                sOut.println(String.format("Version: %s", topKey.mVersion));
+                sOut.println("Root Objects:");
+                for (Archive.RootObject obj : topKey.mRootObjects) {
+                    sOut.println(String.format("   [%d] -> %s", obj.mKind, obj.mDigest.toString()));
+                }
+                int count = 0;
+                sOut.println("Blocks:");
+                for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions) {
+                    sOut.println(String.format("   Block[%d]: %d", count++, desc.mLength));
+                    for (int index = 0; index < desc.mCHKs.size(); index++) {
+                        sOut.println(String.format("      %s", desc.getCHK(index)));
+                    }
+                }
+            }
+        },
+        new Command("resolvename", false, false, " <fmsuser> <fmsgroup> <name_to_resolve>",
+                    "Read BISS name resolution records.") {
+            public boolean canParse(String[] args) { return args.length == 4; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                List<FMSUtil.BISSRecord> records =
+                    FMSUtil.getBISSRecords("127.0.0.1", 11119, args[1], args[2], args[3], 20);
+
+                for (FMSUtil.BISSRecord record : records) {
+                    sOut.println(record);
+                }
+            }
+        },
+        new Command("setname", false, false, " <fmsuser> <fmsgroup> <name_to_set> <value>",
+                    "Send a BISS name update msg.") {
+            public boolean canParse(String[] args) { return args.length == 5; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                FMSUtil.sendBISSMsg("127.0.0.1", 11119, args[1], args[2], args[3], args[4]);
+                sOut.println("Sent message.");
+            }
+        },
+
+        // Debugging hack. Remove.
+        new Command("showargs", false, false, " <arg list>", "print the args passed to it.") {
+            public boolean canParse(String[] args) { return true; }
+            public void invoke(String[] args, CLICache cache) throws Exception {
+                int index = 0;
+                for (String arg : args) {
+                    sOut.println(String.format("[%d]:[%s]", index, arg));
+                    index++;
+                }
+            }
+        },
+    };
+
+    private static Command HELP = COMMANDS[0];
+
+    private static void checkPrivateKey(String ssk) throws IOException {
+        if (!ssk.startsWith("SSK@") || ssk.indexOf("AQECAAE/") == -1) {
+            throw new IOException("Expected a SSK private key with a trailing '/' " +
+                                  "but didn't find one.");
+        }
+    }
+
+    private static Command lookupCommand(String[] args) {
+        if (args == null || args.length == 0) {
+            return HELP;
+        }
+        String abbrev = args[0];
+        Command hit = null;
+        for (Command candidate : COMMANDS) {
+            if (candidate.mName.startsWith(abbrev)) {
+                if (hit != null) {
+                    return HELP;
+                }
+                hit = candidate;
+            }
+        }
+
+        if (hit != null && hit.canParse(args)) {
+            return hit;
+        }
+        return HELP;
+    }
+    public final static void main(String[] args) {
+        try {
+            Command command = lookupCommand(args);
+            CLICache cache = null;
+            if (command.mNeedsCache) {
+                cache = getCache(command.mCreateCache);
+            }
+            command.invoke(args, cache);
+        } catch (Exception ioe) {
+            sOut.println("FAILED with Exception");
+            ioe.printStackTrace(sOut);
+        }
+    }
+}
diff --git a/alien/src/wormarc/cli/CLICache.java b/alien/src/wormarc/cli/CLICache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/cli/CLICache.java
@@ -0,0 +1,195 @@
+/* An implementation helper class for the CLI client.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.cli;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import wormarc.Archive;
+import wormarc.ArchiveResolver;
+import wormarc.IOUtil;
+import wormarc.FileManifest;
+import wormarc.ExternalRefs;
+import wormarc.io.ArchiveCache;
+import wormarc.io.DirectoryIO;
+
+class CLICache extends ArchiveCache implements ArchiveResolver {
+    public CLICache(String directory) throws IOException {
+        super(directory);
+    }
+
+    protected static String namePart(String name) {
+        int pos = name.lastIndexOf(".");
+        if (pos == -1) {
+            return name;
+        }
+
+        if (pos > 0 && pos < name.length() - 1) {
+            return name.substring(0, pos);
+        }
+        throw new IllegalArgumentException("Couldn't parse <name><dot><ordinal> version name.");
+    }
+
+    protected static int ordinalPart(String name) {
+        int pos = name.lastIndexOf(".");
+        if (pos > 0 && pos < name.length() - 1) {
+            try {
+                return Integer.parseInt(name.substring(pos + 1));
+            } catch (NumberFormatException nfe) {
+            }
+        }
+        throw new IllegalArgumentException("Couldn't parse <name><dot><ordinal> version name.");
+    }
+
+    // Override
+    public void setName(String name) {
+        if (name.equals(namePart(name))) {
+            name = name + ".0";
+        }
+
+        // Catch illegal values.
+        namePart(name);
+        ordinalPart(name);
+
+        super.setName(name);
+    }
+
+    public void bumpOrdinal() {
+        if (getName() == null) {
+            throw new IllegalStateException("Name not set.");
+        }
+        String name = namePart(getName());
+        int ordinal = ordinalPart(getName());
+        while ((new File(mDirectory, String.format("%s.%d", name, ordinal))).exists()) {
+            ordinal++;
+        }
+        setName(String.format("%s.%d", name, ordinal));
+    }
+
+    public File headFile() throws IOException {
+        return new File(mDirectory, readHead());
+    }
+
+    public boolean headFileExists() {
+        try {
+            return headFile().exists();
+        } catch (IOException ioe) {
+            return false;
+        }
+    }
+
+    public String cloneHead(String name) throws IOException {
+        if (namePart(name).equals(name)) {
+            name = name + ".0";
+        }
+
+        File copy = new File(mDirectory, name);
+        if (copy.exists()) {
+            throw new IOException("Branch already exists.");
+        }
+
+        IOUtil.copyAndClose(new FileInputStream(headFile()),
+                            new FileOutputStream(copy));
+
+        return name;
+    }
+
+
+    public void saveValue(String key, String value) throws IOException {
+        IOUtil.copyAndClose(new ByteArrayInputStream(value.getBytes(IOUtil.UTF8)),
+                            new FileOutputStream(new File(mDirectory, key)));
+    }
+
+    public String readValue(String key) throws IOException {
+        return IOUtil.readUtf8StringAndClose(new FileInputStream(new File(mDirectory, key)));
+    }
+
+    public void deleteValue(String key) throws IOException {
+        (new File(mDirectory, key)).delete();
+    }
+
+    public void saveHead(String name) throws IOException { saveValue("head", name); }
+    public String readHead() throws IOException { return readValue("head"); }
+
+
+    public FileManifest.IO getManifestIO() throws IOException {
+        DirectoryIO dirIO = new DirectoryIO(mDirectory.getParent());
+        dirIO.ignore(".wormarc"); // DCI: really hard code?
+        dirIO.ignore(".hg");
+        dirIO.ignore(".git");
+        dirIO.ignore(".svn");
+        return dirIO;
+    }
+
+    public String toString() {
+        return mDirectory.toString();
+    }
+
+    // DCI: errr. what if someone sets the name?
+    // Hack to test auditing.
+    public ExternalRefs.Reference getHeadRef() {
+        if (!headFileExists()) {
+            return ExternalRefs.CURRENT_ARCHIVE;
+        }
+        return new ExternalRefs.Reference(ExternalRefs.KIND_LOCAL, getName());
+    }
+
+
+    // DCI: get rid of this local resolver impl now that we have Freenet IO?
+    public Archive resolve(ExternalRefs.Reference fromReference) throws IOException {
+        if (fromReference.mKind != ExternalRefs.KIND_LOCAL) {
+            throw new FileNotFoundException("Only KIND_LOCAL references are supported.");
+        }
+        String previousName = getName();
+        try {
+            setName(fromReference.mExternalKey);
+            return Archive.load(this);
+        } finally {
+            if (previousName != null) {
+                setName(previousName);
+            }
+        }
+    }
+
+    // somename_archivename.0 -> return somename
+    public String getNym(ExternalRefs.Reference fromReference) throws IOException {
+        if (fromReference.mExternalKey.equals(ExternalRefs.CURRENT_ARCHIVE.mExternalKey)) {
+            return "CURRENT_ARCHIVE"; // HACK on top of a hack.
+        }
+
+        return fromReference.mExternalKey;
+
+        // int pos = fromReference.mExternalKey.indexOf("_");
+        // if (pos == -1 || pos == fromReference.mExternalKey.length() - 1) {
+        //     return "UNKNOWN";
+        // }
+
+        // return fromReference.mExternalKey.substring(0, pos);
+    }
+}
diff --git a/alien/src/wormarc/hgdeltacoder/HgDeltaCoder.java b/alien/src/wormarc/hgdeltacoder/HgDeltaCoder.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/HgDeltaCoder.java
@@ -0,0 +1,252 @@
+/* A DeltaCoder implementation implementing the encoding/decoding algorithms used by the hg revlog.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+// ATTRIBUTION: Mirko Friedenhagen <mfriedenhagen@users.berlios.de>
+// This file contains some pieces of compression and decompression code
+// taken from hgkit, specifically from:
+// src/main/java/org/freehg/hgkit/core/Util.java
+//
+// This file is indirectly an derived work based on the
+// Mercurial Python codebase, written by Matt Mackall (and others).
+package wormarc.hgdeltacoder;
+
+import java.io.InputStream;
+import java.io.DataInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.SequenceInputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.DeflaterOutputStream;
+
+import wormarc.DeltaCoder;
+import wormarc.HistoryLink;
+import wormarc.LinkDataFactory;
+import wormarc.IOUtil;
+import wormarc.LinkDigest;
+
+import wormarc.hgdeltacoder.ported.BDiff;
+import wormarc.hgdeltacoder.ported.MDiff;
+
+// ALL OPERATION DONE IN RAM!
+public class HgDeltaCoder implements DeltaCoder {
+    private final static class ZLibResult {
+        public byte[] mData;
+        public boolean mUncompressed;
+
+        ZLibResult(byte[] data, boolean uncompressed) {
+            mData = data;
+            mUncompressed = uncompressed;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+
+
+    private static final int BUFFER_SIZE = 16 * 1024;
+    private static final int ASSUMED_COMPRESSION_RATIO = 3;
+    private static final char ZLIB_COMPRESSION = 'x';
+    private static final char UNCOMPRESSED = 'u';
+    static final int EOF = -1;
+
+    private static final byte[] doDecompress(byte[] data) throws IOException {
+        ByteArrayOutputStream uncompressedOut = new ByteArrayOutputStream(data.length * ASSUMED_COMPRESSION_RATIO);
+        // decompress the bytearray using what should be python zlib
+        final byte[] buffer = new byte[BUFFER_SIZE];
+        final InflaterInputStream inflaterInputStream = new InflaterInputStream(new ByteArrayInputStream(data));
+        int len = 0;
+        while ((len = inflaterInputStream.read(buffer)) != EOF) {
+            uncompressedOut.write(buffer, 0, len);
+        }
+        return uncompressedOut.toByteArray();
+    }
+
+    private static final byte[] decompress(byte[] data) throws IOException {
+        if (data.length < 1) {
+            return new byte[0];
+        }
+        byte dataHeader = data[0];
+        switch (dataHeader) {
+        case UNCOMPRESSED:
+            final byte[] copy = new byte[data.length - 1];
+            System.arraycopy(data, 1, copy, 0, data.length - 1);
+            return copy;
+        case ZLIB_COMPRESSION:
+            return doDecompress(data);
+        case 0:
+            return data;
+        default:
+            throw new IOException("Unknown compression type : " + (char) (dataHeader));
+        }
+    }
+    ////////////////////////////////////////////////////////////
+    private static final byte[] doCompress(byte[] data) throws IOException {
+        ByteArrayOutputStream compressedOut = new ByteArrayOutputStream(data.length / ASSUMED_COMPRESSION_RATIO);
+        // Compress the byte array using what should be python zlib
+        final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(compressedOut);
+        deflaterOutputStream.write(data);
+        deflaterOutputStream.close();
+        return compressedOut.toByteArray();
+    }
+
+    private static final ZLibResult compress(byte[] text) throws IOException {
+        if (text.length == 0) {
+            return new ZLibResult(text, false);
+        }
+
+        int length = text.length;
+        byte[] bin = null;
+
+        if (length < 44) {
+            /* NOP */
+        }
+        // DCI: required for Java?
+        // else if (length > 1000000) {
+        //     // # zlib makes an internal copy, thus doubling memory usage for
+        //     // # large files, so lets do this in pieces
+        //     // z = zlib.compressobj()
+        //     // p = []
+        //     // pos = 0
+        //     // while pos < l:
+        //     //     pos2 = pos + 2**20
+        //     //     p.append(z.compress(text[pos:pos2]))
+        //     //     pos = pos2
+        //     // p.append(z.flush())
+        //     // if sum(map(len, p)) < l:
+        //     //     bin = "".join(p)
+        // }
+        else {
+            bin = doCompress(text);
+            //System.err.println("Compressed: " + text.length + " to: " + bin.length);
+        }
+
+        if (bin == null || bin.length > length) {
+            if (text[0] == 0) {
+                return new ZLibResult(text, false);
+            }
+            return new ZLibResult(text, true);
+        }
+        return new ZLibResult(bin, false);
+    }
+
+    ////////////////////////////////////////////////////////////
+
+    private final static byte[] UNCOMPRESSED_HEADER = {'u', };
+
+    private final static HistoryLink buildLink(byte[] data, boolean uncompressed,
+                                               boolean isEnd,
+                                               LinkDigest parent,
+                                               LinkDataFactory linkDataFactory) throws IOException {
+        int dataLength = data.length;
+        DataInputStream source = null;
+
+        if (uncompressed) {
+            dataLength += 1;
+            source = new DataInputStream(new SequenceInputStream(new ByteArrayInputStream(UNCOMPRESSED_HEADER),
+                                                                 new ByteArrayInputStream(data)));
+
+        } else {
+            source = new DataInputStream(new ByteArrayInputStream(data));
+        }
+
+        return HistoryLink.makeLink(dataLength,
+                                    isEnd,
+                                    parent,
+                                    source,
+                                    linkDataFactory);
+    }
+
+    public HistoryLink makeDelta(LinkDataFactory linkDataFactory,
+                                 LinkDigest parent,
+                                 InputStream oldData,
+                                 InputStream newData,
+                                 boolean disableCompression) throws IOException {
+
+
+        ZLibResult result = null;
+        if (oldData == null) {
+            if (disableCompression) {
+                result = new ZLibResult(IOUtil.readAndClose(newData), true);
+                // BUG: shouldn't be 'u' for /0 data or  less than  44
+                throw new RuntimeException("You just hit a buggy unimplemented code path. Sorry :-("); // DCI:
+            } else {
+                result = compress(IOUtil.readAndClose(newData));
+            }
+            //System.err.println("makeDelta: full insert: " + result.mData.length);
+        } else {
+            if (disableCompression) {
+                throw new IllegalArgumentException("disableCompression only allowed for first link.");
+            }
+            // result = compress(hgTextDiff(IOUtil.readAndClose(oldData),
+            //                              IOUtil.readAndClose(newData)));
+
+
+            byte[] delta = BDiff.bdiff(IOUtil.readAndClose(oldData),
+                                       IOUtil.readAndClose(newData));
+
+            //System.err.println("makeDelta -- made delta: " + delta.length);
+
+            result = compress(delta);
+
+            // DCI: check if this result is larger than compressing the total file?
+
+            // djk: Use a DataOutputStream?
+
+        }
+
+        //System.err.println("makeDelta result, len: " + result.mData.length + " uncompressed: " + result.mUncompressed);
+        return buildLink(result.mData, result.mUncompressed,
+                         oldData == null, parent, linkDataFactory);
+
+    }
+
+    public InputStream applyDeltas(Iterable<HistoryLink> history) throws IOException {
+        byte[] text = null;
+        ArrayList<byte[]> deltas = new ArrayList<byte[]>();
+        for (HistoryLink link: history) {
+            // DCI: memory usage. multiple copies!
+            byte[] rawData = new byte[(int)link.mDataLength];
+            rawData = decompress(link.copyTo(rawData));
+            if (link.mIsEnd) {
+                text = rawData;
+                break;
+            }
+            deltas.add(0, rawData);
+        }
+
+        if (text == null) {
+            throw new IOException("No base file in the history chain.");
+        }
+
+        if (deltas.size() == 0) {
+            return new ByteArrayInputStream(text);
+        }
+
+        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        MDiff.patches(text, deltas, bytesOut);
+        return new ByteArrayInputStream(bytesOut.toByteArray());
+    }
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cli/CLIWrapper.java b/alien/src/wormarc/hgdeltacoder/cli/CLIWrapper.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cli/CLIWrapper.java
@@ -0,0 +1,81 @@
+/* An ancient file used during the initial testing of the java bdiff and mpatch code.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.hgdeltacoder.cli;
+
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.ArrayList;
+
+import wormarc.IOUtil;
+import wormarc.hgdeltacoder.ported.BDiff;
+import wormarc.hgdeltacoder.ported.MDiff;
+
+// Not really meant for public consumption.
+// I wrote this so that I could run my python unit tests against
+// the Java implementation.
+public class CLIWrapper {
+    // Skips the first and last file.
+    final static List<byte[]> readInputFiles(String[] nameList) throws IOException{
+        ArrayList<byte[]> byteBlocks = new ArrayList<byte[]>();
+        for (int index = 0; index < nameList.length - 2; index++) {
+            byteBlocks.add(IOUtil.readFully(nameList[index + 1]));
+        }
+        return byteBlocks;
+    }
+
+    final static String[] shiftArgsLeft(String[] args) {
+        String[] shifted = new String[args.length - 1];
+        System.arraycopy(args, 1, shifted, 0, args.length -1);
+        return shifted;
+    }
+
+    public final static void main(String[] args) {
+        try {
+            // diff old_file new_file out_file
+            if (args[0].equals("diff")) {
+                args = shiftArgsLeft(args);
+                IOUtil.writeFully(BDiff.bdiff(IOUtil.readFully(args[0]),
+                                              IOUtil.readFully(args[1])),
+                                  args[2]);
+            // patch orig_file patch0 patch1 ... patchn out_file
+            } else if (args[0].equals("patch")) {
+                args = shiftArgsLeft(args);
+                ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+                MDiff.patches(IOUtil.readFully(args[0]), readInputFiles(args), bytesOut);
+                IOUtil.writeFully(bytesOut.toByteArray(),
+                                  args[args.length -1]);
+            } else {
+                throw new IllegalArgumentException("Only 'diff' and 'patch' are supported.");
+            }
+            System.exit(0);
+            //System.out.println("SUCCEEDED");
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(-1);
+            //System.out.println("FAILED");
+        }
+    }
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cptr/CBytePtr.java b/alien/src/wormarc/hgdeltacoder/cptr/CBytePtr.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cptr/CBytePtr.java
@@ -0,0 +1,133 @@
+
+/*
+    CBytePtr.java, generated by gen_pointer_wrapper.py.
+
+    Copyright (C) 2010 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 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
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+    This file was developed as component of
+    "fniki" (a wiki implementation running over Freenet).
+
+    DON'T MODIFY THIS FILE BY HAND.
+*/
+
+package wormarc.hgdeltacoder.cptr;
+import wormarc.hgdeltacoder.ported.*;
+
+// Generated by gen_pointer_wrapper.py
+public class CBytePtr {
+    public CBytePtr(byte[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public CBytePtr(byte[] rep) {
+        mRep = rep;
+        mPos = 0;
+    }
+
+    public final CBytePtr copyPtr() {
+        return new CBytePtr(mRep, mPos);
+    }
+
+    public final CBytePtr copyPtrWithOffset(int offset) {
+        return new CBytePtr(mRep, mPos + offset);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof CBytePtr))) {
+            return false;
+        }
+
+        CBytePtr other = (CBytePtr)obj;
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+
+        return (mPos  == other.mPos);
+
+    }
+
+    public final int minus(CBytePtr other) {
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+        return mPos - other.mPos;
+    }
+
+    public final byte deref() {
+        return mRep[mPos];
+    }
+
+    public final void plusPlus() {
+        mPos ++;
+    }
+
+    public final byte bracket(int index) {
+        return mRep[index + mPos];
+    }
+
+    public final byte setValueAt(int offset, byte value) {
+        mRep[offset + mPos] = value;
+        return mRep[offset + mPos];
+    }
+
+    public final static CBytePtr alloc(int size) {
+        byte[] values = new byte[size];
+        return new CBytePtr(values);
+    }
+
+    public final void realloc(int size) {
+        byte[] values = new byte[size];
+        mRep = values;
+        mPos = 0;
+    }
+
+    public final void free() {
+        mRep = null;
+        mPos = -1;
+    }
+
+    public final void set(byte[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public final int pos() {
+        return mPos;
+    }
+
+    public final byte[] unsafeRep() {
+        return mRep;
+    }
+
+    public String toString() {
+        int allocated = 0;
+        if (mRep != null) {
+           allocated = mRep.length;
+        }
+        return "{allocated=" + allocated + ", pos=" + mPos +"}";
+    }
+
+    private int mPos;
+    private byte[] mRep;
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cptr/CHunkPtr.java b/alien/src/wormarc/hgdeltacoder/cptr/CHunkPtr.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cptr/CHunkPtr.java
@@ -0,0 +1,139 @@
+
+/*
+    CHunkPtr.java, generated by gen_pointer_wrapper.py.
+
+    Copyright (C) 2010 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 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
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+    This file was developed as component of
+    "fniki" (a wiki implementation running over Freenet).
+
+    DON'T MODIFY THIS FILE BY HAND.
+*/
+
+package wormarc.hgdeltacoder.cptr;
+import wormarc.hgdeltacoder.ported.*;
+
+// Generated by gen_pointer_wrapper.py
+public class CHunkPtr {
+    public CHunkPtr(Hunk[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public CHunkPtr(Hunk[] rep) {
+        mRep = rep;
+        mPos = 0;
+    }
+
+    public final CHunkPtr copyPtr() {
+        return new CHunkPtr(mRep, mPos);
+    }
+
+    public final CHunkPtr copyPtrWithOffset(int offset) {
+        return new CHunkPtr(mRep, mPos + offset);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof CHunkPtr))) {
+            return false;
+        }
+
+        CHunkPtr other = (CHunkPtr)obj;
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+
+        return (mPos  == other.mPos);
+
+    }
+
+    public final int minus(CHunkPtr other) {
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+        return mPos - other.mPos;
+    }
+
+    public final Hunk deref() {
+        return mRep[mPos];
+    }
+
+    public final void plusPlus() {
+        mPos ++;
+    }
+
+    public final Hunk bracket(int index) {
+        return mRep[index + mPos];
+    }
+
+    public final Hunk setValueAt(int offset, Hunk value) {
+        mRep[offset + mPos] = value;
+        return mRep[offset + mPos];
+    }
+
+    public final static CHunkPtr alloc(int size) {
+        Hunk[] values = new Hunk[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Hunk();
+        }
+        return new CHunkPtr(values);
+    }
+
+    public final void realloc(int size) {
+        Hunk[] values = new Hunk[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Hunk();
+        }
+        mRep = values;
+        mPos = 0;
+    }
+
+    public final void free() {
+        mRep = null;
+        mPos = -1;
+    }
+
+    public final void set(Hunk[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public final int pos() {
+        return mPos;
+    }
+
+    public final Hunk[] unsafeRep() {
+        return mRep;
+    }
+
+    public String toString() {
+        int allocated = 0;
+        if (mRep != null) {
+           allocated = mRep.length;
+        }
+        return "{allocated=" + allocated + ", pos=" + mPos +"}";
+    }
+
+    private int mPos;
+    private Hunk[] mRep;
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cptr/CIntPtr.java b/alien/src/wormarc/hgdeltacoder/cptr/CIntPtr.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cptr/CIntPtr.java
@@ -0,0 +1,133 @@
+
+/*
+    CIntPtr.java, generated by gen_pointer_wrapper.py.
+
+    Copyright (C) 2010 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 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
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+    This file was developed as component of
+    "fniki" (a wiki implementation running over Freenet).
+
+    DON'T MODIFY THIS FILE BY HAND.
+*/
+
+package wormarc.hgdeltacoder.cptr;
+import wormarc.hgdeltacoder.ported.*;
+
+// Generated by gen_pointer_wrapper.py
+public class CIntPtr {
+    public CIntPtr(int[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public CIntPtr(int[] rep) {
+        mRep = rep;
+        mPos = 0;
+    }
+
+    public final CIntPtr copyPtr() {
+        return new CIntPtr(mRep, mPos);
+    }
+
+    public final CIntPtr copyPtrWithOffset(int offset) {
+        return new CIntPtr(mRep, mPos + offset);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof CIntPtr))) {
+            return false;
+        }
+
+        CIntPtr other = (CIntPtr)obj;
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+
+        return (mPos  == other.mPos);
+
+    }
+
+    public final int minus(CIntPtr other) {
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+        return mPos - other.mPos;
+    }
+
+    public final int deref() {
+        return mRep[mPos];
+    }
+
+    public final void plusPlus() {
+        mPos ++;
+    }
+
+    public final int bracket(int index) {
+        return mRep[index + mPos];
+    }
+
+    public final int setValueAt(int offset, int value) {
+        mRep[offset + mPos] = value;
+        return mRep[offset + mPos];
+    }
+
+    public final static CIntPtr alloc(int size) {
+        int[] values = new int[size];
+        return new CIntPtr(values);
+    }
+
+    public final void realloc(int size) {
+        int[] values = new int[size];
+        mRep = values;
+        mPos = 0;
+    }
+
+    public final void free() {
+        mRep = null;
+        mPos = -1;
+    }
+
+    public final void set(int[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public final int pos() {
+        return mPos;
+    }
+
+    public final int[] unsafeRep() {
+        return mRep;
+    }
+
+    public String toString() {
+        int allocated = 0;
+        if (mRep != null) {
+           allocated = mRep.length;
+        }
+        return "{allocated=" + allocated + ", pos=" + mPos +"}";
+    }
+
+    private int mPos;
+    private int[] mRep;
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cptr/CLinePtr.java b/alien/src/wormarc/hgdeltacoder/cptr/CLinePtr.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cptr/CLinePtr.java
@@ -0,0 +1,139 @@
+
+/*
+    CLinePtr.java, generated by gen_pointer_wrapper.py.
+
+    Copyright (C) 2010 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 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
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+    This file was developed as component of
+    "fniki" (a wiki implementation running over Freenet).
+
+    DON'T MODIFY THIS FILE BY HAND.
+*/
+
+package wormarc.hgdeltacoder.cptr;
+import wormarc.hgdeltacoder.ported.*;
+
+// Generated by gen_pointer_wrapper.py
+public class CLinePtr {
+    public CLinePtr(Line[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public CLinePtr(Line[] rep) {
+        mRep = rep;
+        mPos = 0;
+    }
+
+    public final CLinePtr copyPtr() {
+        return new CLinePtr(mRep, mPos);
+    }
+
+    public final CLinePtr copyPtrWithOffset(int offset) {
+        return new CLinePtr(mRep, mPos + offset);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof CLinePtr))) {
+            return false;
+        }
+
+        CLinePtr other = (CLinePtr)obj;
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+
+        return (mPos  == other.mPos);
+
+    }
+
+    public final int minus(CLinePtr other) {
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+        return mPos - other.mPos;
+    }
+
+    public final Line deref() {
+        return mRep[mPos];
+    }
+
+    public final void plusPlus() {
+        mPos ++;
+    }
+
+    public final Line bracket(int index) {
+        return mRep[index + mPos];
+    }
+
+    public final Line setValueAt(int offset, Line value) {
+        mRep[offset + mPos] = value;
+        return mRep[offset + mPos];
+    }
+
+    public final static CLinePtr alloc(int size) {
+        Line[] values = new Line[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Line();
+        }
+        return new CLinePtr(values);
+    }
+
+    public final void realloc(int size) {
+        Line[] values = new Line[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Line();
+        }
+        mRep = values;
+        mPos = 0;
+    }
+
+    public final void free() {
+        mRep = null;
+        mPos = -1;
+    }
+
+    public final void set(Line[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public final int pos() {
+        return mPos;
+    }
+
+    public final Line[] unsafeRep() {
+        return mRep;
+    }
+
+    public String toString() {
+        int allocated = 0;
+        if (mRep != null) {
+           allocated = mRep.length;
+        }
+        return "{allocated=" + allocated + ", pos=" + mPos +"}";
+    }
+
+    private int mPos;
+    private Line[] mRep;
+}
diff --git a/alien/src/wormarc/hgdeltacoder/cptr/CPosPtr.java b/alien/src/wormarc/hgdeltacoder/cptr/CPosPtr.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/cptr/CPosPtr.java
@@ -0,0 +1,139 @@
+
+/*
+    CPosPtr.java, generated by gen_pointer_wrapper.py.
+
+    Copyright (C) 2010 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 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
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+    This file was developed as component of
+    "fniki" (a wiki implementation running over Freenet).
+
+    DON'T MODIFY THIS FILE BY HAND.
+*/
+
+package wormarc.hgdeltacoder.cptr;
+import wormarc.hgdeltacoder.ported.*;
+
+// Generated by gen_pointer_wrapper.py
+public class CPosPtr {
+    public CPosPtr(Pos[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public CPosPtr(Pos[] rep) {
+        mRep = rep;
+        mPos = 0;
+    }
+
+    public final CPosPtr copyPtr() {
+        return new CPosPtr(mRep, mPos);
+    }
+
+    public final CPosPtr copyPtrWithOffset(int offset) {
+        return new CPosPtr(mRep, mPos + offset);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || (!(obj instanceof CPosPtr))) {
+            return false;
+        }
+
+        CPosPtr other = (CPosPtr)obj;
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+
+        return (mPos  == other.mPos);
+
+    }
+
+    public final int minus(CPosPtr other) {
+        if (mRep != other.mRep) {
+            throw new IllegalArgumentException("Illegal comparison, pointers " +
+                                               "reference different memory " +
+                                               "blocks!");
+        }
+        return mPos - other.mPos;
+    }
+
+    public final Pos deref() {
+        return mRep[mPos];
+    }
+
+    public final void plusPlus() {
+        mPos ++;
+    }
+
+    public final Pos bracket(int index) {
+        return mRep[index + mPos];
+    }
+
+    public final Pos setValueAt(int offset, Pos value) {
+        mRep[offset + mPos] = value;
+        return mRep[offset + mPos];
+    }
+
+    public final static CPosPtr alloc(int size) {
+        Pos[] values = new Pos[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Pos();
+        }
+        return new CPosPtr(values);
+    }
+
+    public final void realloc(int size) {
+        Pos[] values = new Pos[size];
+        for (int index = 0; index < values.length; index++) {
+           values[index] = new Pos();
+        }
+        mRep = values;
+        mPos = 0;
+    }
+
+    public final void free() {
+        mRep = null;
+        mPos = -1;
+    }
+
+    public final void set(Pos[] rep, int pos) {
+        mRep = rep;
+        mPos = pos;
+    }
+
+    public final int pos() {
+        return mPos;
+    }
+
+    public final Pos[] unsafeRep() {
+        return mRep;
+    }
+
+    public String toString() {
+        int allocated = 0;
+        if (mRep != null) {
+           allocated = mRep.length;
+        }
+        return "{allocated=" + allocated + ", pos=" + mPos +"}";
+    }
+
+    private int mPos;
+    private Pos[] mRep;
+}
diff --git a/alien/src/wormarc/hgdeltacoder/ported/BDiff.java b/alien/src/wormarc/hgdeltacoder/ported/BDiff.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/ported/BDiff.java
@@ -0,0 +1,453 @@
+/*
+  BDiff.java -- port of bdiff.c
+  Copyright 2010 Darrell Karbott.
+
+  Ported from rev: 16f6c13706df of bdiff.c from:
+  http://selenic.com/repo/hg-stable
+  sha1sum: ef62bd826dbcd1b1b15329cd90afd18f8c67974f
+
+  This is a quick and dirty Java port of bdiff.c from mercurial.
+  It is a derived work, licensed under the GPL. See the original
+  file header below.
+
+  I used the gen_pointer_wrapper.py script to generate wrapper classes
+  around primative arrays to support C pointer semantics so I could
+  port with minimal changes to the source.
+
+  This is admittedly kind of ugly, but I needed correct code
+  and didn't have much time to spend writing it.
+
+  This file was developed as component of
+  "fniki" (a wiki implementation running over Freenet).
+*/
+
+/*
+ bdiff.c - efficient binary diff extension for Mercurial
+
+ Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
+
+ This software may be used and distributed according to the terms of
+ the GNU General Public License, incorporated herein by reference.
+
+ Based roughly on Python difflib
+*/
+
+package wormarc.hgdeltacoder.ported;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import wormarc.hgdeltacoder.cptr.CBytePtr;
+import wormarc.hgdeltacoder.cptr.CIntPtr;
+import wormarc.hgdeltacoder.cptr.CHunkPtr;
+import wormarc.hgdeltacoder.cptr.CLinePtr;
+import wormarc.hgdeltacoder.cptr.CPosPtr;
+
+public class BDiff {
+    static class HunkList {
+        CHunkPtr base;
+        CHunkPtr head;
+        public String toString() {
+            return "{base=" + base + " head=" + head + "}";
+        }
+    };
+
+    static final int splitlines(CBytePtr a, int len, CLinePtr lr) {
+        // Copy to preserve pass by value semantics.
+        a = a.copyPtr();
+        // lr is passed by reference (**).
+
+	int h, i;
+        CBytePtr p = a.copyPtr();
+        CBytePtr b = a.copyPtr();
+
+        CBytePtr plast = a.copyPtrWithOffset(len - 1);
+
+        Line l = new Line();
+
+	/* count the lines */
+	i = 1; /* extra line for sentinel */
+        CBytePtr end = a.copyPtrWithOffset(len); // i.e. one past the last byte.
+	for (/*NOP*/; !p.equals(end); p.plusPlus()) {
+            if (p.deref() == '\n' || p.equals(plast)) {
+                i++;
+            }
+        }
+
+        try {
+            lr.realloc(i); // Hinky
+        }
+        catch (OutOfMemoryError ome) {
+            return -1;
+        }
+
+        lr = lr.copyPtr(); // Wacky. Point to the same rep, but don't modify the mPos for the caller.
+
+        l = lr.deref();
+
+	/* build the line array and calculate hashes */
+	h = 0;
+	for (p = a.copyPtr(); !p.equals(end); p.plusPlus()) {
+            /* Leonid Yuriev's hash */
+            h = (h * 1664525) + (0x000000FF & (int)(p.deref())) + 1013904223; // DCI: int math differences in Java?
+            if (p.deref() == '\n' || p.equals(plast)) {
+                l.h = h;
+                h = 0;
+                l.len = p.minus(b) + 1;
+                l.l = b.copyPtr();
+                l.n = Integer.MAX_VALUE;
+
+                lr.plusPlus();
+                l = lr.deref();
+                b = p.copyPtrWithOffset(1);
+            }
+	}
+
+	/* set up a sentinel */
+	l.h = 0;
+        l.len = 0;
+	l.l = a.copyPtrWithOffset(len);
+	return i - 1;
+    }
+
+    // 1 if not equal 0 otherwise.
+    final static int cmp(Line a,  Line b) {
+	if (a.h != b.h || a.len != b.len) {
+            return 1;
+        }
+
+        // bdiff.c used memcmp, but mapped all non-zero values to 1
+        // so this should be legit.
+
+        // Dog slow. Arrays.equals really has no offset?
+        // Reach into the rep.
+        final byte[] aBytes = a.l.unsafeRep();
+        final int aOffset = a.l.pos();
+        final byte[] bBytes = b.l.unsafeRep();
+        final int bOffset = b.l.pos();
+        for (int index = 0; index < a.len; index++) {
+            if (aBytes[index + aOffset] != bBytes[index +bOffset]) {
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+
+    // FIXED BRACKET BUG IN THIS FUNCTION CHECK IT IN
+    final static int equatelines(CLinePtr a, int an, CLinePtr b, int bn) {
+        // Copy to preserve pass by value semantics.
+        a = a.copyPtr();
+        b = b.copyPtr();
+
+	int i, j, buckets = 1, t, scale;
+	CPosPtr h = null;
+
+	/* build a hash table of the next highest power of 2 */
+	while (buckets < bn + 1) {
+		buckets *= 2;
+        }
+
+	/* try to allocate a large hash table to avoid collisions */
+	for (scale = 4; scale > 0; scale /= 2) {
+            try {
+                // DCI: test. too brutal for Java?
+                h = CPosPtr.alloc(scale * buckets);
+            }
+            catch (OutOfMemoryError ome) {
+                /*NOP*/
+            }
+            if (h != null) {
+                break;
+            }
+	}
+
+	if (h == null) {
+            return 0;
+        }
+
+	buckets = buckets * scale - 1;
+
+	/* clear the hash table */
+	for (i = 0; i <= buckets; i++) {
+            h.bracket(i).pos = Integer.MAX_VALUE;
+            h.bracket(i).len = 0;
+	}
+
+	/* add lines to the hash table chains */
+	for (i = bn - 1; i >= 0; i--) {
+            /* find the equivalence class */
+            for (j = b.bracket(i).h & buckets; h.bracket(j).pos != Integer.MAX_VALUE;
+                 j = (j + 1) & buckets) {
+                if (0 == cmp(b.bracket(i), b.bracket(h.bracket(j).pos))) {
+                    break;
+                }
+            }
+            /* add to the head of the equivalence class */
+            b.bracket(i).n = h.bracket(j).pos;
+            b.bracket(i).e = j;
+            h.bracket(j).pos = i;
+            h.bracket(j).len++; /* keep track of popularity */
+	}
+
+	/* compute popularity threshold */
+	t = (bn >= 4000) ? bn / 1000 : bn + 1; // DCI: math ok in Java?
+
+	/* match items in a to their equivalence class in b */
+	for (i = 0; i < an; i++) {
+            /* find the equivalence class */
+            for (j = a.bracket(i).h & buckets; h.bracket(j).pos != Integer.MAX_VALUE;
+                 j = (j + 1) & buckets) {
+                if (0 == cmp(a.bracket(i), b.bracket(h.bracket(j).pos))) {
+                    break;
+                }
+            }
+            a.bracket(i).e = j; /* use equivalence class for quick compare */
+            if (h.bracket(j).len <= t) {
+                a.bracket(i).n = h.bracket(j).pos; /* point to head of match list */
+            }
+            else {
+                a.bracket(i).n = Integer.MAX_VALUE; /* too popular */
+            }
+        }
+
+	/* discard hash tables */
+        h.free(); // Pedantic. Not required.
+	return 1;
+    }
+
+    static int longest_match(CLinePtr a, CLinePtr b, CPosPtr pos,
+                             int a1, int a2, int b1, int b2, CIntPtr omi, CIntPtr omj) {
+        // Copy to preserve pass by value semantics.
+        a = a.copyPtr();
+        b = b.copyPtr();
+        pos = pos.copyPtr();
+        // omi, omj are out parameters, passed by reference.
+
+	int mi = a1, mj = b1, mk = 0, mb = 0, i, j, k;
+
+	for (i = a1; i < a2; i++) {
+            /* skip things before the current block */
+            for (j = a.bracket(i).n; j < b1; j = b.bracket(j).n) {
+                /*NOP*/
+            }
+
+            /* loop through all lines match a[i] in b */
+            for (; j < b2; j = b.bracket(j).n) {
+                /* does this extend an earlier match? */
+                if (i > a1 && j > b1 && pos.bracket(j - 1).pos == i - 1) {
+                    k = pos.bracket(j - 1).len + 1;
+                } else {
+                    k = 1;
+                }
+
+                pos.bracket(j).pos = i;
+                pos.bracket(j).len = k;
+
+                /* best match so far? */
+                if (k > mk) {
+                    mi = i;
+                    mj = j;
+                    mk = k;
+                }
+            }
+	}
+
+	if (mk != 0) {
+            mi = mi - mk + 1;
+            mj = mj - mk + 1;
+	}
+
+	/* expand match to include neighboring popular lines */
+	while (mi - mb > a1 && mj - mb > b1 &&
+	       a.bracket(mi - mb - 1).e == b.bracket(mj - mb - 1).e) {
+            mb++;
+        }
+	while (mi + mk < a2 && mj + mk < b2 &&
+	       a.bracket(mi + mk).e == b.bracket(mj + mk).e) {
+            mk++;
+        }
+
+	omi.setValueAt(0, mi - mb);
+	omj.setValueAt(0, mj - mb);
+
+	return mk + mb;
+    }
+
+    static void recurse(CLinePtr a, CLinePtr b, CPosPtr pos,
+		    int a1, int a2, int b1, int b2, HunkList l) {
+        // Copy to preserve pass by value semantics.
+        a = a.copyPtr();
+        b = b.copyPtr();
+        pos = pos.copyPtr();
+
+	int i, j, k;
+
+        // New allocation on each frame. NOT tunneled up the stack.
+        CIntPtr ptrToI = CIntPtr.alloc(1);
+        CIntPtr ptrToJ = CIntPtr.alloc(1);
+	/* find the longest match in this chunk */
+	k = longest_match(a, b, pos, a1, a2, b1, b2, ptrToI, ptrToJ);
+        i = ptrToI.deref();
+        j = ptrToJ.deref();
+	if (k == 0) {
+            return;
+        }
+
+	/* and recurse on the remaining chunks on either side */
+	recurse(a, b, pos, a1, i, b1, j, l);
+	l.head.deref().a1 = i;
+	l.head.deref().a2 = i + k;
+	l.head.deref().b1 = j;
+	l.head.deref().b2 = j + k;
+	l.head.plusPlus();
+	recurse(a, b, pos, i + k, a2, j + k, b2, l);
+    }
+
+    static HunkList diff(CLinePtr a, int an, CLinePtr b, int bn) {
+        // Copy to preserve pass by value semantics.
+        a = a.copyPtr();
+        b = b.copyPtr();
+
+	HunkList l = new HunkList();
+	CHunkPtr curr;
+	CPosPtr pos;
+	int t;
+
+	/* allocate and fill arrays */
+	t = equatelines(a, an, b, bn);
+	pos = CPosPtr.alloc((bn != 0) ? bn : 1);
+
+	/* we can't have more matches than lines in the shorter file */
+        CHunkPtr allHunks = CHunkPtr.alloc((an<bn ? an:bn) + 1);
+        l.head = allHunks.copyPtr();
+        l.base = allHunks.copyPtr();
+
+	if (/*pos && */ (l.base != null)  && (t != 0)) { // DCI: checking for calloc failures right?
+            /* generate the matching block list */
+            recurse(a, b, pos, 0, an, 0, bn, /*&*/l);
+            l.head.deref().a1 = l.head.deref().a2 = an;
+            l.head.deref().b1 = l.head.deref().b2 = bn;
+            l.head.plusPlus();
+	}
+
+        pos.free(); // Pedantic, not required.
+
+	/* normalize the hunk list, try to push each hunk towards the end */
+	for (curr = l.base.copyPtr(); !curr.equals(l.head); curr.plusPlus()) {
+            CHunkPtr next = curr.copyPtrWithOffset(1);
+            int shift = 0;
+
+            if (next.equals(l.head)) {
+                break;
+            }
+
+            if (curr.deref().a2 == next.deref().a1) {
+                while (curr.deref().a2 + shift < an && curr.deref().b2 + shift < bn
+                       && 0 == cmp(a.copyPtrWithOffset(curr.deref().a2 + shift).deref(), // hmmm...object instantiations in loop
+                                   b.copyPtrWithOffset(curr.deref().b2 + shift).deref())) {
+                    shift++;
+                }
+            } else if (curr.deref().b2 == next.deref().b1) {
+                while (curr.deref().b2 + shift < bn && curr.deref().a2 + shift < an
+                       && 0 == cmp(b.copyPtrWithOffset(curr.deref().b2 + shift).deref(),
+                                   a.copyPtrWithOffset(curr.deref().a2 + shift).deref())) {
+                    shift++;
+                }
+            }
+
+            if (shift == 0) {
+                continue;
+            }
+            curr.deref().b2 += shift;
+            next.deref().b1 += shift;
+            curr.deref().a2 += shift;
+            next.deref().a1 += shift;
+        }
+
+        return l;
+    }
+
+    public static byte[] bdiff(byte[] saBytes, byte[] sbBytes) {
+	CBytePtr sa;
+        CBytePtr sb;
+	//PyObject *result = NULL;
+	CLinePtr al = CLinePtr.alloc(0); // splitlines reallocates
+        CLinePtr bl = CLinePtr.alloc(0); // splitlines reallocates
+	HunkList l;
+	CHunkPtr h;
+	//CBytePtr encode = new CBytePtr(new byte[12]);
+        CBytePtr rb;
+	int an, bn, len = 0, la, lb;
+
+        sa = new CBytePtr(saBytes);
+        la = saBytes.length;
+        sb = new CBytePtr(sbBytes);
+        lb = sbBytes.length;
+
+	an = splitlines(sa, la, al);
+	bn = splitlines(sb, lb, bl);
+
+        //if (!al || !bl)   // Was there a bug in the .c? i.e. splitlines
+        //      goto nomem; // on malloc failure.
+        // This was just trapping malloc failure, don't need it, java throws
+
+	l = diff(al, an, bl, bn);
+
+	if (l.head == null) {
+            //return null;
+            return new byte[0];
+        }
+	/* calculate length of output */
+	la = lb = 0;
+	for (h = l.base.copyPtr(); !h.equals(l.head); h.plusPlus()) {
+            if (h.deref().a1 != la || h.deref().b1 != lb) {
+                len += 12 + bl.bracket(h.deref().b1).l.minus(bl.bracket(lb).l);
+            }
+            la = h.deref().a2;
+            lb = h.deref().b2;
+	}
+
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        DataOutputStream dataOut = new DataOutputStream(buffer);
+
+	la = lb = 0;
+
+        try {
+            for (h = l.base.copyPtr(); !h.equals(l.head); h.plusPlus()) {
+                if (h.deref().a1 != la || h.deref().b1 != lb) {
+                    len = bl.bracket(h.deref().b1).l.minus(bl.bracket(lb).l);
+
+                    // This is just writing 3 unsigned 32 bit integers into a block
+                    // of memory in netork byte order, right?
+                    //*(uint32_t *)(encode)     = htonl(al[la].l - al->l);
+                    //*(uint32_t *)(encode + 4) = htonl(al[h->a1].l - al->l);
+                    //*(uint32_t *)(encode + 8) = htonl(len);
+
+                    int value = al.bracket(la).l.minus(al.deref().l);
+                    if (value < 0) { throw new RuntimeException("overflow in encoding???"); }
+                    dataOut.writeInt(value);
+
+                    value = al.bracket(h.deref().a1).l.minus(al.deref().l);
+                    if (value < 0) { throw new RuntimeException("overflow in encoding???"); }
+                    dataOut.writeInt(value);
+
+                    value = len;
+                    if (value < 0) { throw new RuntimeException("overflow in encoding???"); }
+                    dataOut.writeInt(value);
+
+                    CBytePtr linePtr = bl.bracket(lb).l;
+                    dataOut.write(linePtr.unsafeRep(), linePtr.pos(), len);
+                }
+                la = h.deref().a2;
+                lb = h.deref().b2;
+            }
+            dataOut.close();
+        }
+        catch (IOException ioe) {
+            throw new RuntimeException("Unexpected exception encoding patch: " + ioe);
+        }
+        return buffer.toByteArray();
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/hgdeltacoder/ported/Hunk.java b/alien/src/wormarc/hgdeltacoder/ported/Hunk.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/ported/Hunk.java
@@ -0,0 +1,38 @@
+/* Ported hunk struct.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.hgdeltacoder.ported;
+
+public final class Hunk {
+    int a1;
+    int a2;
+    int b1;
+    int b2;
+
+    public String toString() {
+        return "{a1=" + a1 + ", a2=" + a2 + ", b1=" + b1 + ", b2=" + b2 + "}";
+    }
+}
+
+
diff --git a/alien/src/wormarc/hgdeltacoder/ported/Line.java b/alien/src/wormarc/hgdeltacoder/ported/Line.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/ported/Line.java
@@ -0,0 +1,49 @@
+/* Ported line struct.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.hgdeltacoder.ported;
+
+import wormarc.hgdeltacoder.cptr.CBytePtr;
+
+public final class Line {
+    int h;
+    int len;
+    int n;
+    int e;
+    CBytePtr l;
+
+    public String toString() {
+        return "{h=" + h + ",len=" + len + ",n=" + n + ",e=" + e + ",l=" + l + "}";
+    }
+
+    // DCI: fails for non utf-8, for debugging, remove
+    public String lineValue() {
+        try {
+            return new String(l.unsafeRep(), l.pos(), len, "UTF-8");
+        } catch (java.io.IOException ioe) {
+            throw new RuntimeException("FAILED");
+        }
+    }
+
+}
diff --git a/alien/src/wormarc/hgdeltacoder/ported/MDiff.java b/alien/src/wormarc/hgdeltacoder/ported/MDiff.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/ported/MDiff.java
@@ -0,0 +1,320 @@
+/* MDiff.java class from the hgkit project.
+ *
+ * Written by: Mirko Friedenhagen <mfriedenhagen@users.berlios.de> (grokd from 'hg history MDiff.java')
+ * Licensed under the GPL as required since it was a derived work from
+ * Mercurial code under the GNU General Public Licencse.
+ *
+ */
+package wormarc.hgdeltacoder.ported;
+
+//package org.freehg.hgkit.core;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Simple dataholder class for mdiff.
+ */
+final class Fragment {
+    
+    /** Maximal line length of {@link Fragment#toString()}. */
+    private static final int LINE_LENGTH = 80;
+
+    /** Where in the "file" this fragment starts. **/
+    int start;
+
+    /** Where in the "file" this fragments ends. */
+    int end;
+
+    /** The data to be inserted into the file at {@code this.start} up to {@code this.end}. */
+    byte[] data;
+
+    /** Where in {@code this.data} to begin read. */
+    int offset = 0;
+
+    /**
+     * The length of the data to read, this can differ from {@code data.length}. Combine
+     * {@code offset}, {@code data} and {@code mlength} to get patch data.
+     */
+    int mlength = -1;
+
+    /** The length of the fragment, may differ from {@code end - start} and {@code data.length}. */
+    int len() {
+        if (mlength == -1) {
+            throw new IllegalStateException("Length not set yet");
+        }
+        return mlength;
+    }
+
+    /**
+     * Sets the length of the patch data.
+     * @param len new length.
+     */
+    public void len(int len) {
+        mlength = len;
+    }
+
+    @Override
+    public String toString() {
+        String txt = new String(this.data);
+        txt = txt.substring(this.offset);
+        int max = Math.min(LINE_LENGTH, len());
+        txt = txt.substring(0, max);
+        if (len() > LINE_LENGTH) {
+            txt += "...";
+        }
+        return start + " " + end + " " + len() + " " + txt;
+    }
+
+}
+
+/**
+ * Creates diffs.
+ */
+public final class MDiff {
+    
+    /** Static helper class. */
+    private MDiff() {
+        // nope
+    }
+    
+    /**
+     * Creates patches.
+     * 
+     * @param in
+     * @param bins
+     * @param out receives patches
+     */
+    public static void patches(byte[] in, List<byte[]> bins, OutputStream out) {
+        // if there are no fragments we don't have to do anything
+        try {
+            // convert binary to fragments
+            List<Fragment> patch = fold(bins, 0, bins.size());
+            if (patch == null) {
+                throw new IllegalStateException("Error folding patches");
+            }
+            // apply all fragments to in
+            apply(in, patch, out);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static LinkedList<Fragment> fold(List<byte[]> bins, int start, int end) {
+        if (bins.size() < 1) {
+            return new LinkedList<Fragment>();
+        }
+        /*
+         * recursively generate a patch of all bins between start and end
+         */
+        if (start + 1 == end) {
+            return decode(bins.get(start));
+        }
+
+        /* divide and conquer, memory management is elsewhere */
+        final int len = (end - start) / 2;
+        LinkedList<Fragment> left = fold(bins, start, start + len);
+        LinkedList<Fragment> right = fold(bins, start + len, end);
+        return combine(left, right);
+    }
+
+    /*
+     * Combines hunk lists a and b, while adjusting b for offset changes in a.
+     * This deletes a and b and returns the resultant list.
+     */
+    private static LinkedList<Fragment> combine(LinkedList<Fragment> a, LinkedList<Fragment> b) {
+
+        if (a == null || b == null) {
+            return null;
+        }
+        LinkedList<Fragment> combination = new LinkedList<Fragment>();
+        int offset = 0;
+        for (Fragment bFrag : b) {
+            /* save old hunks */
+            offset = gather(combination, a, bFrag.start, offset);
+
+            /* discard replaced hunks */
+            int post = discard(a, bFrag.end, offset);
+
+            // create a new fragment from an existing with ajustments
+            Fragment ct = new Fragment();
+            ct.start = bFrag.start - offset;
+            ct.end = bFrag.end - post;
+            ct.data = bFrag.data;
+            ct.offset = bFrag.offset;
+            ct.len(bFrag.len());
+            combination.add(ct);
+
+            offset = post;
+        }
+
+        /* hold on to tail from a */
+        combination.addAll(a);
+        a.clear();
+        b.clear();
+        return combination;
+    }
+
+    // static int discard(struct flist *src, int cut, int offset) {
+    private static int discard(LinkedList<Fragment> src, int cut, int poffset) {
+
+        int offset = poffset;
+        int postend, c, l;
+        // i think this discards everything up to "cut"
+        // a fragment may have to be split if it
+        // overlaps "cut"
+        for (Iterator<Fragment> iter = src.iterator(); iter.hasNext();) {
+            final Fragment s = iter.next();
+            if (cut <= s.start + offset) {
+                break;
+            }
+
+            postend = offset + s.start + s.len();
+            if (postend <= cut) {
+                // this one is discarded
+                offset += s.start + s.len() - s.end;
+                iter.remove();
+            } else {
+                // partial discarding, move the content of s.data so that it
+                // doesn't overlap cut
+                c = cut - offset;
+                if (s.end < c) {
+                    c = s.end;
+                }
+                l = cut - offset - s.start;
+                if (s.len() < l) {
+                    l = s.len();
+                }
+
+                offset += s.start + l - c;
+                s.start = c;
+                s.len(s.len() - l);
+
+                // s.data = s.data + l;
+                // this should work, but doesnt, bug?
+                // s.data = Arrays.copyOfRange(s.data, l, s.len());
+                s.offset += l;
+                // s.data = Arrays.copyOfRange(s.data, l, s.data.length);
+                // no more needs to be discarded
+                break;
+            }
+        }
+        return offset;
+    }
+
+    private static int gather(LinkedList<Fragment> dest, LinkedList<Fragment> src, int cut, int poffset) {
+
+        /*
+         * move hunks in source that are less than cut to dest, but compensate
+         * for changes in offset. The last hunk may be split if necessary
+         * (oberlaps cut).
+         */
+        int offset = poffset;
+        Fragment s = null;
+
+        for (Iterator<Fragment> iter = src.iterator(); iter.hasNext();) {
+            s = iter.next();
+            if (cut <= s.start + offset) {
+                break; /* we've gone far enough */
+            }
+
+            int postend = offset + s.start + s.len();
+            if (postend <= cut) {
+                /* save this hunk as it is */
+                offset += s.start + s.len() - s.end;
+                dest.add(s);
+                iter.remove();
+
+            } else {
+                /* This hunk must be broken up */
+                int cutAt = cut - offset;
+                if (s.end < cutAt) {
+                    cutAt = s.end;
+                }
+                int length = cut - offset - s.start;
+                if (s.len() < length) {
+                    length = s.len();
+                }
+
+                offset += s.start + length - cutAt;
+                Fragment d = new Fragment();
+                d.start = s.start;
+                d.end = cutAt;
+
+                d.data = s.data;
+                d.offset = s.offset;
+
+                d.len(length);
+                dest.add(d);
+
+                s.start = cutAt;
+                s.len(s.len() - length);
+                s.offset += length;
+                break;
+            }
+        }
+
+        if (0 < src.size() && s != src.get(0)) {
+            throw new IllegalStateException("src head should be s");
+        }
+        return offset;
+    }
+
+    /**
+     * Decodes a binary patch into a fragment list.
+     * 
+     * @param bin
+     *            the binary patch
+     * @return a list of fragments
+     */
+    private static LinkedList<Fragment> decode(byte[] bin) {
+        // int start, int end, int len, byte[...] data
+        LinkedList<Fragment> result = new LinkedList<Fragment>();
+        ByteBuffer reader = ByteBuffer.wrap(bin);
+        // while(reader.position() < length) {
+        while (reader.hasRemaining()) {
+            Fragment lt = new Fragment();
+            result.add(lt);
+
+            lt.start = reader.getInt();
+            lt.end = reader.getInt();
+            lt.len(reader.getInt());
+
+            if (lt.start > lt.end) {
+                break; /* sanity check */
+            }
+
+            lt.offset = reader.position();
+            lt.data = bin;
+
+            if (lt.len() < 0) {
+                throw new IllegalStateException(
+                        "Programmer Unsure of what  'big data + big (bogus) len can wrap around' means");
+            }
+            reader.position(reader.position() + lt.len());
+        }
+
+        if (reader.hasRemaining()) {
+            throw new IllegalStateException("patch cannot be decoded");
+        }
+        return result;
+    }
+
+    private static void apply(byte[] orig, List<Fragment> fragments, OutputStream out) throws IOException {
+        final int len = orig.length;
+        int last = 0;
+        for (final Fragment fragment : fragments) {
+            // if this fragment is not within the bounds
+            if (fragment.start < last || len < fragment.end) {
+                throw new IllegalStateException("invalid patch");
+            }
+            out.write(orig, last, fragment.start - last);
+            out.write(fragment.data, fragment.offset, fragment.len());
+            last = fragment.end;
+        }
+        out.write(orig, last, len - last);
+    }
+}
diff --git a/alien/src/wormarc/hgdeltacoder/ported/Pos.java b/alien/src/wormarc/hgdeltacoder/ported/Pos.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/hgdeltacoder/ported/Pos.java
@@ -0,0 +1,32 @@
+/* Ported pos struct.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+package wormarc.hgdeltacoder.ported;
+
+public final class Pos {
+    int pos;
+    int len;
+    public String toString() { return "{pos=" + pos + ", len=" + len + "}";}
+}
+
+
diff --git a/alien/src/wormarc/io/ArchiveCache.java b/alien/src/wormarc/io/ArchiveCache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/ArchiveCache.java
@@ -0,0 +1,112 @@
+/* An Archive.IO implementation which can read and write Archive's to the local file system.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+
+import java.util.List;
+
+import wormarc.Archive;
+import wormarc.ArchiveManifest;
+import wormarc.BinaryLinkRep;
+import wormarc.Block;
+import wormarc.IOUtil;
+import wormarc.HistoryLinkMap;
+import wormarc.LinkDataFactory;
+import wormarc.LinkDigest;
+
+
+// LATER: I reused ArchiveManifest as a quick hack.
+//        Should be rewritten to read / write ArchiveData.
+//        DCI: This breaks the IO contract which doesn't require an ARCHIVE_MANIFEST.
+public class ArchiveCache extends ArchiveCacheBase {
+    String mName;
+
+    protected Archive.ArchiveData readArchiveData(HistoryLinkMap linkMap,
+                                                  LinkDataFactory linkFactory)
+        throws IOException {
+        File file = new File(mDirectory, mName);
+        DataInputStream dis = new DataInputStream(new FileInputStream(file));
+        boolean raisedReadingDigest = true;
+        try {
+            // <digest><archive manifest bytes>
+            LinkDigest digest = BinaryLinkRep.readLinkDigest(dis);
+            raisedReadingDigest = false;
+            ArchiveManifest manifest = ArchiveManifest.fromBytes(dis, digest);
+            return manifest.makeArchiveData();
+        } finally {
+            if (raisedReadingDigest) {
+                dis.close();
+            }
+        }
+    }
+
+    public ArchiveCache(String directory) throws IOException {
+        super(directory);
+    }
+
+    // The named version of the archive to read / write next.
+    public void setName(String name) { mName = name; }
+    public String getName() { return mName; }
+
+    // Override to dump ArchiveData after dumping links.
+    public void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) throws IOException {
+        if (mName == null) {
+            throw new IllegalStateException("Name not set!");
+        }
+
+        // Raises for no / multiple ARCHIVE_MANIFEST objects.
+        LinkDigest digest = ArchiveManifest.getArchiveManifestDigest(rootObjects);
+
+        super.write(linkMap, blocks, rootObjects);
+
+        // Write the manifest so it's available for subsequent read.
+        File file = new File(mDirectory, mName);
+        OutputStream outputStream = new FileOutputStream(file);
+        InputStream inputStream = null;
+        boolean raised = true;
+        try {
+            inputStream = (new ArchiveManifest(rootObjects, blocks)).toBytes();
+            raised = false;
+        } finally {
+            if (raised) {
+                outputStream.close();
+            }
+        }
+
+        // <digest><archive manifest bytes>
+        IOUtil.copyAndClose(new SequenceInputStream(new ByteArrayInputStream(digest.getBytes()),
+                                                    inputStream),
+                            outputStream);
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/io/ArchiveCacheBase.java b/alien/src/wormarc/io/ArchiveCacheBase.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/ArchiveCacheBase.java
@@ -0,0 +1,80 @@
+/* An implementation helper class for local file system Archive.IO implementations.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+import java.io.IOException;
+import java.util.List;
+
+import wormarc.Archive;
+import wormarc.Block;
+import wormarc.HistoryLinkMap;
+import wormarc.LinkDataFactory;
+import wormarc.LinkDigest;
+
+public abstract class ArchiveCacheBase extends LinkCache implements Archive.IO {
+    // INTENT: Hook for LATER to allow CLI client to store version data by
+    //         ArchiveManifest chain head LinkDigest.
+    protected abstract Archive.ArchiveData readArchiveData(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException;
+
+    public ArchiveCacheBase(String directory) throws IOException {
+        super(directory);
+    }
+
+    public void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) throws IOException {
+        // There are legitimate cases where the root objects aren't in the blocks. i.e.
+        // latest link of the archive manifest file. DCI: HUH? not true.
+        for (Archive.RootObject obj : rootObjects) {
+            if (obj.mDigest.isNullDigest()) {
+                continue;
+            }
+            writeLink(linkMap.getLink(obj.mDigest));
+        }
+
+        for (Block block : blocks) {
+            for (LinkDigest digest : block.getDigests()) {
+                writeLink(linkMap.getLink(digest));
+            }
+        }
+    }
+
+    public Archive.ArchiveData read(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException {
+        Archive.ArchiveData data = readArchiveData(linkMap, linkFactory);
+
+        for (Archive.RootObject obj : data.mRootObjects) {
+            if (obj.mDigest.isNullDigest()) {
+                continue;
+            }
+            readLink(linkMap, linkFactory, obj.mDigest);
+        }
+
+        for (Block block : data.mBlocks) {
+            for (LinkDigest digest : block.getDigests()) {
+                readLink(linkMap, linkFactory, digest);
+            }
+        }
+
+        return data;
+    }
+}
diff --git a/alien/src/wormarc/io/Base64.java b/alien/src/wormarc/io/Base64.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/Base64.java
@@ -0,0 +1,223 @@
+package wormarc.io;
+
+// Pillaged from the Freenet codebase, licensed under GNU General Public License V2
+// package freenet.support;
+
+/**
+ * This class provides encoding of byte arrays into Base64-encoded strings,
+ * and decoding the other way.
+ *
+ * <P>NOTE!  This is modified Base64 with slightly different characters than
+ * usual, so it won't require escaping when used in URLs.
+ *
+ * <P>NOTE!  This class only does the padding that's normal in Base64
+ * if the 'true' flag is given to the encode() method.  This is because
+ * Base64 requires that the length of the encoded text be a multiple
+ * of four characters, padded with '='.  Without the 'true' flag, we don't
+ * add these '=' characters.
+ *
+ * @author Stephen Blackheath
+ */
+public class Base64
+{
+  private static char[] base64Alphabet = {
+    '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', '0', '1', '2', '3',
+    '4', '5', '6', '7', '8', '9', '~', '-'};
+
+  private static char[] base64StandardAlphabet = {
+	    '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', '0', '1', '2', '3',
+	    '4', '5', '6', '7', '8', '9', '+', '/'};
+
+  /**
+   * A reverse lookup table to convert base64 letters back into the
+   * a 6-bit sequence.
+   */
+  private static byte[] base64Reverse;
+  private static byte[] base64StandardReverse;
+
+   // Populate the base64Reverse lookup table from the base64Alphabet table.
+  static {
+    base64Reverse = new byte[128];
+    base64StandardReverse = new byte[base64Reverse.length];
+
+      // Set all entries to 0xFF, which means that that particular letter
+      // is not a legal base64 letter.
+    for (int i = 0; i < base64Reverse.length; i++) {
+      base64Reverse[i] = (byte) 0xFF;
+      base64StandardReverse[i] = (byte) 0xFF;
+    }
+    for (int i = 0; i < base64Alphabet.length; i++) {
+      base64Reverse[base64Alphabet[i]] = (byte) i;
+      base64StandardReverse[base64StandardAlphabet[i]] = (byte) i;
+    }
+  }
+
+  /**
+   * Encode to our shortened (non-standards-compliant) format.
+   */
+  public static String encode(byte[] in)
+  {
+    return encode(in, false);
+  }
+
+  /* FIXME: Figure out where this function is used and maybe remove it if its not
+   * used. Its old javadoc which has been here for a while fools the user into believing
+   * that the format is standard compliant */
+
+  /**
+   * Caller should specify equalsPad=true if they want a standards compliant padding,
+   * but not standard compliant encoding.
+   */
+  public static String encode(byte[] in, boolean equalsPad) {
+	  return encode(in, equalsPad, base64Alphabet);
+  }
+
+  /**
+   * Standard compliant encoding.
+   */
+  public static String encodeStandard(byte[] in) {
+	  return encode(in, true, base64StandardAlphabet);
+  }
+
+  /**
+   * Caller should specify equalsPad=true if they want a standards compliant encoding.
+   */
+  private static String encode(byte[] in, boolean equalsPad, char[] alphabet)
+  {
+    char[] out = new char[((in.length+2)/3)*4];
+    int rem = in.length%3;
+    int o = 0;
+    for (int i = 0; i < in.length;) {
+      int val = (in[i++] & 0xFF) << 16;
+      if (i < in.length)
+        val |= (in[i++] & 0xFF) << 8;
+      if (i < in.length)
+        val |= (in[i++] & 0xFF);
+      out[o++] = alphabet[(val>>18) & 0x3F];
+      out[o++] = alphabet[(val>>12) & 0x3F];
+      out[o++] = alphabet[(val>>6) & 0x3F];
+      out[o++] = alphabet[val & 0x3F];
+    }
+    int outLen = out.length;
+    switch (rem) {
+      case 1: outLen -= 2; break;
+      case 2: outLen -= 1; break;
+    }
+      // Pad with '=' signs up to a multiple of four if requested.
+    if (equalsPad)
+      while (outLen < out.length)
+        out[outLen++] = '=';
+    return new String(out, 0, outLen);
+  }
+
+  /**
+   * Handles the standards-compliant padding (padded with '=' signs) as well as our
+   * shortened form.
+ * @throws IllegalBase64Exception
+   */
+  public static byte[] decode(String inStr) throws IllegalBase64Exception {
+	  return decode(inStr, base64Reverse);
+  }
+
+  /**
+   * Handles the standards-compliant base64 encoding.
+   */
+  public static byte[] decodeStandard(String inStr) throws IllegalBase64Exception {
+	  return decode(inStr, base64StandardReverse);
+  }
+
+  /**
+   * Handles the standards-compliant (padded with '=' signs) as well as our
+   * shortened form.
+   */
+  private static byte[] decode(String inStr, byte[] reverseAlphabet)
+    throws IllegalBase64Exception
+  {
+    try {
+      char[] in = inStr.toCharArray();
+      int inLength = in.length;
+
+        // Strip trailing equals signs.
+      while ((inLength > 0) && (in[inLength-1] == '='))
+        inLength--;
+
+      int blocks = inLength/4;
+      int remainder = inLength & 3;
+        // wholeInLen and wholeOutLen are the the length of the input and output
+        // sequences respectively, not including any partial block at the end.
+      int wholeInLen  = blocks*4;
+      int wholeOutLen = blocks*3;
+      int outLen = wholeOutLen;
+      switch (remainder) {
+        case 1: throw new IllegalBase64Exception("illegal Base64 length");
+        case 2:  outLen = wholeOutLen+1; break;
+        case 3:  outLen = wholeOutLen+2; break;
+        default: outLen = wholeOutLen;
+      }
+      byte[] out = new byte[outLen];
+      int o = 0;
+      int i;
+      for (i = 0; i < wholeInLen;) {
+        int in1 = reverseAlphabet[in[i]];
+        int in2 = reverseAlphabet[in[i+1]];
+        int in3 = reverseAlphabet[in[i+2]];
+        int in4 = reverseAlphabet[in[i+3]];
+        int orValue = in1|in2|in3|in4;
+        if ((orValue & 0x80) != 0)
+          throw new IllegalBase64Exception("illegal Base64 character");
+        int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6) | in4;
+        out[o] = (byte) (outVal>>16);
+        out[o+1] = (byte) (outVal>>8);
+        out[o+2] = (byte) outVal;
+        i += 4;
+        o += 3;
+      }
+      int orValue;
+      switch (remainder) {
+        case 2:
+          {
+            int in1 = reverseAlphabet[in[i]];
+            int in2 = reverseAlphabet[in[i+1]];
+            orValue = in1|in2;
+            int outVal = (in1 << 18) | (in2 << 12);
+            out[o] = (byte) (outVal>>16);
+          }
+          break;
+        case 3:
+          {
+            int in1 = reverseAlphabet[in[i]];
+            int in2 = reverseAlphabet[in[i+1]];
+            int in3 = reverseAlphabet[in[i+2]];
+            orValue = in1|in2|in3;
+            int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6);
+            out[o] = (byte) (outVal>>16);
+            out[o+1] = (byte) (outVal>>8);
+          }
+          break;
+        default:
+            // Keep compiler happy
+          orValue = 0;
+      }
+      if ((orValue & 0x80) != 0)
+        throw new IllegalBase64Exception("illegal Base64 character");
+      return out;
+    }
+      // Illegal characters can cause an ArrayIndexOutOfBoundsException when
+      // looking up reverseAlphabet.
+    catch (ArrayIndexOutOfBoundsException e) {
+      throw new IllegalBase64Exception("illegal Base64 character");
+    }
+  }
+}
diff --git a/alien/src/wormarc/io/DirectoryIO.java b/alien/src/wormarc/io/DirectoryIO.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/DirectoryIO.java
@@ -0,0 +1,303 @@
+/* A FileManifest.IO implementation for reading / writing FileManifests from/to a directory on the file system.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+// DCI: SHA1 caching
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+
+import wormarc.FileManifest;
+import wormarc.IOUtil;
+import wormarc.LinkDigest;
+
+public class DirectoryIO implements FileManifest.IO {
+    private File mRootDirectory;
+    private Set<String> mIgnoreDirectories = new HashSet<String>();
+
+    public DirectoryIO(String rootDirectory) throws IOException  {
+        File dir = new File(rootDirectory);
+        if (!(dir.exists() && dir.isDirectory() && dir.canWrite() && dir.canRead())) {
+            throw new IOException("Directory must exist and have read and write access.");
+        }
+        mRootDirectory = dir;
+    }
+
+    // Hacks to deal with storing empty directories.
+    private final static String EMPTY_DIRECTORY_SENTINEL = "~3mpt1~"; // 31337, improbable value.
+    private final static String SLASH_EMPTY_DIRECTORY_SENTINEL = String.format("/%s", EMPTY_DIRECTORY_SENTINEL);
+
+    private class DirectoryTraverser {
+        private HashMap<String, LinkDigest> mFiles = new HashMap<String, LinkDigest>();
+        private boolean mCalculateDigests;
+
+        DirectoryTraverser(File rootDirectory, boolean calculateDigests) throws IOException {
+            mCalculateDigests = calculateDigests;
+            traverse(rootDirectory, "");
+        }
+
+        // Return mutable reference on purpose.
+        public HashMap<String, LinkDigest> getFiles() { return mFiles; }
+
+        private  String join(String relativePath, String name) {
+            if (relativePath.length() == 0) {
+                return name;
+            }
+            scrub(relativePath);
+            return String.format("%s/%s", relativePath, name);
+        }
+
+        private void addEmptyDirectory(String relativePath) {
+            mFiles.put(join(relativePath, EMPTY_DIRECTORY_SENTINEL), LinkDigest.EMPTY_DIGEST);
+        }
+
+        private void addFile(String relativePath, LinkDigest digest) {
+            mFiles.put(relativePath, digest);
+        }
+
+        private void traverse(File dir, String relativePath) throws IOException {
+            if (mIgnoreDirectories.contains(relativePath)) {
+                // DCI: ok for .git .hg, won't work for .svn sprinkled in every subdir...
+                return;
+            }
+            String[] values = dir.list();
+            if (values.length == 0) {
+                addEmptyDirectory(relativePath);
+            }
+
+            for (String value : values) {
+                File file = new File(dir, value);
+                if (file.isFile()) {
+                    LinkDigest digest = LinkDigest.NULL_DIGEST;
+                    if (mCalculateDigests) {
+                        digest = IOUtil.getFileDigest(new FileInputStream(file));
+                    }
+                    addFile(join(relativePath, value), digest);
+
+                }
+                if (file.isDirectory()) {
+                    traverse(file, join(relativePath, value));
+                }
+            }
+        }
+    }
+
+    public void ignore(String name) {
+        mIgnoreDirectories.add(name);
+    }
+
+    public Map<String, LinkDigest> getFiles() throws IOException {
+        DirectoryTraverser files = new DirectoryTraverser(mRootDirectory, true);
+        return files.getFiles();
+    }
+
+    public InputStream getFile(String name) throws IOException {
+        if (name.endsWith(EMPTY_DIRECTORY_SENTINEL)) {
+            return new ByteArrayInputStream(new byte[0]);
+        }
+        // DCI: check to make sure resulting path is under mRootDirectory.
+        return new FileInputStream(new File(mRootDirectory, name));
+    }
+
+    // LATER: Do better?
+    // Start and end sync are hacks so I can implment empty dir
+    // handling below the line.
+    public void startSync(Set<String> allFiles) throws IOException {}
+    public void putFile(String name, InputStream rawBytes) throws IOException {
+        if (name.endsWith(SLASH_EMPTY_DIRECTORY_SENTINEL)) {
+            File dir = new File(mRootDirectory, name).getParentFile();
+            if (dir == null) {
+                throw new IOException("Empty root directory???");
+            }
+            if (!dir.exists() || !dir.isDirectory()) {
+                if (!dir.mkdir()) {
+                    throw new IOException(String.format("Couldn't make directory: %s", dir));
+                }
+            }
+            return;
+        }
+
+        File file = new File(mRootDirectory, name);
+        File parent = file.getParentFile();
+        if (!parent.exists() || !parent.isDirectory()) {
+            if (!parent.mkdirs()) {
+                throw new IOException(String.format("Couldn't make directory: %s", parent));
+            }
+        }
+        OutputStream outputStream = null;
+        try {
+            outputStream = new FileOutputStream(new File(mRootDirectory, name));
+        } finally {
+            if (outputStream == null) {
+                rawBytes.close(); // I.e. in the process of throwing.
+            }
+        }
+        IOUtil.copyAndClose(rawBytes, outputStream);
+    }
+
+    public void deleteFile(String name) throws IOException {
+        if (name.endsWith(EMPTY_DIRECTORY_SENTINEL)) {
+            return; // Will be cleaned up by endsync. Might not be empty yet.
+        }
+        File file = new File(mRootDirectory, name);
+        if (!file.delete()) {
+            throw new IOException(String.format("Couldn't remove file: %s", file));
+        }
+    }
+
+
+    private static void scrub(String relativePath) {
+        if (relativePath.length() == 0 ) {
+            throw new IllegalArgumentException("relativePath is empty!");
+        }
+        if (relativePath.trim() != relativePath) {
+            throw new IllegalArgumentException("relativePath has external whitespace!");
+        }
+
+        if (relativePath.indexOf("//") != -1) {
+            throw new IllegalArgumentException("// in relativePath!");
+        }
+
+        if (relativePath.startsWith("/")) {
+            throw new IllegalArgumentException("relativePath can't start with / !");
+        }
+    }
+
+    // Hmmm... O(NxM) N = number of files, M = subdir depth.
+    private static Set<String> allowedDirectories(Set<String> allFiles) {
+        Set<String> allowed = new HashSet<String>();
+        for (String path : allFiles) {
+            scrub(path);
+            SplitResult result = split(path);
+            String candidate = result.mDirectory.trim();
+            while (!candidate.equals("")) {
+                // Add all parent components.
+                allowed.add(candidate);
+                result = split(candidate);
+                candidate = result.mDirectory.trim();
+            }
+        }
+        return allowed;
+    }
+
+    private static final class SplitResult {
+        final String mDirectory;
+        final String mName;
+        SplitResult(String directory, String name) {
+            mDirectory = directory;
+            mName = name;
+       }
+    }
+
+    private static SplitResult split(String relativePath) {
+        scrub(relativePath);
+
+        if (relativePath.equals("/")) {
+            return new SplitResult("", "");
+        }
+
+        int pos = relativePath.lastIndexOf("/");
+
+        // foo, empty string
+        if (pos == -1) {
+            return new SplitResult("", relativePath);
+        }
+
+        // /foo/bar/baz/qux/
+        if (pos == relativePath.length() - 1) {
+            return new SplitResult(relativePath.substring(0, pos), "");
+        }
+
+        return new SplitResult(relativePath.substring(0, pos),
+                               relativePath.substring(pos + 1, relativePath.length()));
+    }
+
+    // DCI: test file and dir with same name.
+    private void deleteEmptyDirectoryTree(String relativePath) throws IOException  {
+        if (relativePath.trim().equals("")) {
+            throw new IllegalArgumentException("Empty relativePath.");
+        }
+
+        File dir = new File(mRootDirectory, relativePath);
+        if (!(dir.exists() && dir.isDirectory())) {
+            return;
+        }
+        for (String subDir : dir.list()) {
+            if (mIgnoreDirectories.contains(subDir) || subDir.trim().equals("")) {
+                continue;
+            }
+            deleteEmptyDirectoryTree(String.format("%s/%s", relativePath, subDir));
+        }
+        // By the time we've recursed to here, all subdirectories should be empty.
+        if (dir.exists() && dir.isDirectory()) {
+            if (!dir.delete()) {
+                throw new IOException(String.format("Couldn't remove: %s", relativePath));
+            }
+        }
+    }
+
+    // DCI: shady, test.
+    public void endSync(Set<String> allFiles) throws IOException {
+        // By the time we get here, all files not in the version should
+        // have been deleted, but empty directories may be left behind.
+        // So we clean them up.
+
+        Set<String> allowedDirs = allowedDirectories(allFiles);
+
+        DirectoryTraverser files = new DirectoryTraverser(mRootDirectory, false);
+
+        Set<String> victims = allowedDirectories(files.getFiles().keySet());
+
+        victims.removeAll(allowedDirs);
+
+        while (!victims.isEmpty()) {
+            String victim = victims.iterator().next();
+            victims.remove(victim);
+            // Will fail if the directory isn't empty, so this isn't quite as dangerous
+            // as it looks.
+
+            deleteEmptyDirectoryTree(victim);
+            String parent = split(victim).mDirectory;
+            if (parent.trim().equals("")) {
+                continue;
+            }
+
+            if (!allowedDirs.contains(parent)) {
+                victims.add(parent);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/alien/src/wormarc/io/FCPCommandRunner.java b/alien/src/wormarc/io/FCPCommandRunner.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/FCPCommandRunner.java
@@ -0,0 +1,510 @@
+/* An implementation helper class for reading and writing data into Freenet.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import net.pterodactylus.fcp.AllData;
+import net.pterodactylus.fcp.ClientGet;
+import net.pterodactylus.fcp.ClientHello;
+import net.pterodactylus.fcp.ClientPut;
+import net.pterodactylus.fcp.CloseConnectionDuplicateClientName;
+import net.pterodactylus.fcp.FcpAdapter;
+import net.pterodactylus.fcp.FcpConnection;
+import net.pterodactylus.fcp.GetFailed;
+import net.pterodactylus.fcp.NodeHello;
+import net.pterodactylus.fcp.PutFailed;
+import net.pterodactylus.fcp.Priority;
+import net.pterodactylus.fcp.ProtocolError;
+import net.pterodactylus.fcp.PutSuccessful;
+import net.pterodactylus.fcp.SimpleProgress;
+import net.pterodactylus.fcp.Verbosity;
+
+import net.pterodactylus.fcp.FcpMessage;
+
+import wormarc.Block;
+import wormarc.HistoryLinkMap;
+import wormarc.IOUtil;
+
+// Christ on a bike! is jfcplib really this complicated?
+public class FCPCommandRunner {
+    private final static Verbosity VERBOSITY = Verbosity.ALL;
+    private final static Priority PRIORITY = Priority.interactive;
+    private final static int MAX_RETRIES = 3;
+    private final static boolean DONT_COMPRESS = true;
+    private final static String REAL_TIME_FIELD = "RealTimeFlag";
+    private final static String REAL_TIME_VALUE = "true";
+
+    private static PrintStream sDebugOut = System.err;
+
+    protected static void debug(String msg) {
+        sDebugOut.println(msg);
+    }
+
+    public static void setDebugOutput(PrintStream out) {
+        synchronized (FCPCommandRunner.class) {
+            sDebugOut = out;
+        }
+    }
+
+    static abstract class Command extends FcpAdapter {
+        protected String mName;
+        protected String mUri;
+        protected FCPCommandRunner mRunner;
+        protected String mFcpId;
+
+        protected boolean mDone = false;
+        protected String mFailureMsg = "";
+
+        protected abstract void handleData(long length, InputStream data) throws IOException;
+        protected abstract FcpMessage getStartMessage();
+
+        protected void handleProgress(SimpleProgress simpleProgress) {
+            debug(String.format("[%s]:(%d, %d, %d, %d, %d, %s)",
+                                mName,
+                                simpleProgress.getTotal(),
+                                simpleProgress.getRequired(),
+                                simpleProgress.getFailed(),
+                                simpleProgress.getFatallyFailed(),
+                                simpleProgress.getSucceeded(),
+                                simpleProgress.isFinalizedTotal() ? "true" : "false"
+                                ));
+        }
+
+        protected String makeFcpId() {
+            return IOUtil.randomHexString(16);
+        }
+
+        protected Command(String name, String uri, FCPCommandRunner runner) {
+            mName = name;
+            mUri = uri;
+            mRunner = runner;
+            mFcpId = makeFcpId();
+        }
+
+        public synchronized String getUri() {
+            return mUri;
+        }
+
+        public synchronized boolean finished() {
+            return mDone;
+        }
+
+        public synchronized boolean succeeded() {
+            if (!mDone) {
+                throw new IllegalStateException("Not finished.");
+            }
+            return mFailureMsg.equals("");
+        }
+
+        public synchronized void raiseOnFailure() throws IOException {
+            if (!mDone) {
+                throw new IllegalStateException("Not finished.");
+            }
+            if (mFailureMsg.equals("")) {
+                return;
+            }
+            if (mName == null) {
+                mName = "unknown";
+            }
+            throw new IOException(String.format("%s:%s", mName, mFailureMsg));
+        }
+
+        protected void handleDone(String failureMsg) {
+            if (failureMsg == null) {
+                throw new IllegalArgumentException("failureMsg == null");
+            }
+            //debug("handleDone: " + failureMsg);
+
+            synchronized (this) {
+                mFailureMsg = failureMsg;
+                mDone = true;
+            }
+            synchronized (mRunner) {
+                mRunner.commandFinished(this);
+            }
+        }
+
+        public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
+            if (!simpleProgress.getIdentifier().equals(mFcpId)) {
+                return;
+            }
+            handleProgress(simpleProgress);
+        }
+
+        public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
+            if (!getFailed.getIdentifier().equals(mFcpId)) {
+                return;
+            }
+
+            // DCI: Handle too big! (code == 21). It means the
+            handleDone(String.format("ClientGet failed: %d", getFailed.getCode()));
+        }
+
+	public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed) {
+            if (!putFailed.getIdentifier().equals(mFcpId)) {
+                return;
+            }
+
+            handleDone(String.format("ClientPut failed: %d", putFailed.getCode()));
+        }
+
+        public void receivedAllData(FcpConnection fcpConnection, AllData allData) {
+            if (!allData.getIdentifier().equals(mFcpId)) {
+                return;
+            }
+            String msg = "";
+            try {
+                handleData(allData.getDataLength(), allData.getPayloadInputStream());
+            } catch (IOException ioe) {
+                msg = "Failed processing downloaded data: " + ioe.getMessage();;
+            } finally {
+                handleDone(msg);
+            }
+        }
+
+	public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful) {
+            if (!putSuccessful.getIdentifier().equals(mFcpId)) {
+                return;
+            }
+            mUri = putSuccessful.getURI();
+            handleDone("");
+        }
+
+        public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello) {
+            handleDone("");
+        }
+
+        public void
+            receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection,
+                                                       CloseConnectionDuplicateClientName
+                                                       closeConnectionDuplicateClientName) {
+            handleDone("Duplicate Connection");
+	}
+
+        public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
+            handleDone(String.format("Protocol Error[%d]: %s, %s",
+                                     protocolError.getCode(),
+                                     protocolError.getCodeDescription(),
+                                     protocolError.getExtraDescription()
+                                     ));
+
+	}
+
+	public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
+            handleDone("Connection Closed");
+	}
+    }
+
+    static class HelloCommand extends Command {
+        private String mClientName;
+        protected HelloCommand(String clientName, FCPCommandRunner runner) {
+            super("client_hello", "", runner);
+            mClientName = clientName;
+        }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            handleDone("Not expecting AllData");
+        }
+
+        protected FcpMessage getStartMessage() {
+            mFcpId = "only_client_hello";
+            return new ClientHello(mClientName);
+        }
+    }
+
+    static class GetBlock extends Command {
+        private long mLength;
+        private Block mBlock;
+        private FreenetIO mIO;
+
+        protected GetBlock(String name, String uri, long length, FreenetIO io, FCPCommandRunner runner) {
+            super(name, uri, runner);
+            mIO = io;
+            mLength = length;
+        }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            debug(String.format("[%s]:handleData received %d bytes", mName, length));
+            if (mLength != length) {
+                System.err.println("BLOCK IS WRONG LENGTH.");
+                throw new IOException("Block is wrong length!");
+            }
+            if (data == null) {
+                throw new IllegalArgumentException("data == null");
+            }
+            mBlock = mIO.readLinks(data);
+        }
+
+        protected FcpMessage getStartMessage() {
+            ClientGet msg = new ClientGet(mUri, mFcpId);
+            msg.setVerbosity(VERBOSITY);
+            msg.setPriority(PRIORITY);
+            msg.setMaxSize(mLength); // DCI: test?
+            msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
+            msg.setMaxRetries(MAX_RETRIES);
+            return msg;
+        }
+
+        public Block getBlock() { return mBlock; }
+    }
+
+    static class PutBlock extends Command { // DCI: sleazy. How does stream get closed in failure cases?
+        private long mLength;
+        private InputStream mData;
+        public PutBlock(String name, long length, InputStream data, FCPCommandRunner runner) {
+            super(name, "CHK@", runner);
+            mLength = length;
+            mData = data;
+        }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            handleDone("Not expecting AllData");
+        }
+
+        protected FcpMessage getStartMessage() {
+            ClientPut msg = new ClientPut(mUri, mFcpId);
+            msg.setDataLength(mLength);
+            msg.setPayloadInputStream(mData);
+            msg.setVerbosity(VERBOSITY);
+            msg.setDontCompress(DONT_COMPRESS);
+            msg.setPriority(PRIORITY);
+            msg.setMaxRetries(MAX_RETRIES);
+            msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
+            return msg;
+        }
+    }
+
+    static class GetBlockChk extends Command { // DCI: sleazy. How does stream get closed in failure cases?
+        private long mLength;
+        private InputStream mData;
+        public GetBlockChk(String name, long length, InputStream data, FCPCommandRunner runner) {
+            super(name, "CHK@", runner);
+            mLength = length;
+            mData = data;
+        }
+
+        public long getLength() { return mLength; }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            handleDone("Not expecting AllData");
+        }
+
+        protected FcpMessage getStartMessage() {
+            ClientPut msg = new ClientPut(mUri, mFcpId);
+            msg.setDataLength(mLength);
+            msg.setPayloadInputStream(mData);
+            msg.setVerbosity(VERBOSITY);
+            msg.setDontCompress(DONT_COMPRESS);
+            msg.setPriority(PRIORITY);
+            msg.setGetCHKOnly(true);
+            return msg;
+        }
+    }
+
+
+    static class GetTopKey extends Command {
+        private FreenetTopKey mTopKey;
+        protected GetTopKey(String name, String uri, FCPCommandRunner runner) {
+            super(name, uri, runner);
+        }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            debug(String.format("[%s]:handleData received %d bytes", mName, length));
+            mTopKey = FreenetTopKey.fromBytes(data);
+        }
+
+        protected FcpMessage getStartMessage() {
+            ClientGet msg = new ClientGet(mUri, mFcpId);
+            msg.setVerbosity(VERBOSITY);
+            msg.setPriority(PRIORITY);
+            msg.setMaxSize(FreenetTopKey.MAX_LENGTH);
+            msg.setMaxRetries(MAX_RETRIES);
+            msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
+            return msg;
+        }
+
+        public FreenetTopKey getTopKey() { return mTopKey; }
+    }
+
+    static class PutTopKey extends Command {
+        private FreenetTopKey mTopKey;
+
+        protected PutTopKey(String name, String uri, FreenetTopKey topKey, FCPCommandRunner runner)
+            throws IOException {
+            super(name,  uri, runner);
+            mTopKey = topKey;
+        }
+
+        protected void handleData(long length, InputStream data) throws IOException {
+            handleDone("Not expecting AllData");
+        }
+
+        protected FcpMessage getStartMessage() {
+            try {
+                ClientPut msg = new ClientPut(mUri, mFcpId);
+                // Hmmm... double read. Ok. it's small.
+                long length = IOUtil.readAndClose(mTopKey.toBytes()).length;
+                msg.setDataLength(length);
+                msg.setPayloadInputStream(mTopKey.toBytes());
+                msg.setVerbosity(VERBOSITY);
+                msg.setDontCompress(DONT_COMPRESS);
+                msg.setPriority(PRIORITY);
+                msg.setMaxRetries(MAX_RETRIES);
+                msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
+                return msg;
+            } catch (IOException ioe) {
+                // Should never happen.
+                throw new RuntimeException("Assertion Failure: Read of topkey bytes failed.", ioe);
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    private List<Command> mPending = new ArrayList<Command>();
+    private FcpConnection mConnection;
+
+    public FCPCommandRunner(String host, int port, String clientName)
+        throws IOException, InterruptedException  {
+        mConnection = new FcpConnection(host, port);
+        mConnection.connect();
+        sendClientHello(clientName);
+        waitUntilAllFinished();
+    }
+
+    public synchronized void disconnect() {
+        mConnection.disconnect();
+    }
+
+    protected synchronized HelloCommand sendClientHello(String clientName) throws IOException  {
+        HelloCommand cmd = new HelloCommand(clientName, this);
+        start(cmd);
+        return cmd;
+    }
+
+    public synchronized GetBlock sendGetBlock(String uri, long length, int ordinal, FreenetIO io) throws IOException  {
+        GetBlock cmd = new GetBlock(String.format("get_block_%d", ordinal),
+                                    uri, length, io, this);
+        start(cmd);
+        return cmd;
+    }
+
+    public synchronized PutBlock sendPutBlock(int ordinal, HistoryLinkMap linkMap, Block block) throws IOException  {
+        PutBlock cmd = new PutBlock(String.format("put_block_%d", ordinal),
+                                    linkMap.getLength(block),  // Hmmm... Expensive?
+                                    linkMap.getBinaryRep(block),
+                                    this);
+        start(cmd);
+        return cmd;
+    }
+
+    public synchronized GetBlockChk sendGetBlockChk(int ordinal, HistoryLinkMap linkMap, Block block) throws IOException  {
+        GetBlockChk cmd = new GetBlockChk(String.format("get_block_chk_%d", ordinal),
+                                          linkMap.getLength(block),  // Hmmm... Expensive?
+                                          linkMap.getBinaryRep(block),
+                                          this);
+        start(cmd);
+        return cmd;
+    }
+
+
+    public synchronized GetTopKey sendGetTopKey(String uri) throws IOException  {
+        GetTopKey cmd = new GetTopKey("get_top_key", uri, this);
+        start(cmd);
+        return cmd;
+    }
+
+    public synchronized PutTopKey sendPutTopKey(String uri, FreenetTopKey topKey) throws IOException  {
+        PutTopKey cmd = new PutTopKey("put_top_key", uri, topKey, this);
+        start(cmd);
+        return cmd;
+    }
+
+    public synchronized int getPendingCount() {
+        return mPending.size();
+    }
+
+    // DCI: make this call raiseOnFailure for each request?
+    // DCI: timeout
+    public synchronized void waitUntilAllFinished() throws InterruptedException {
+        while (mPending.size() > 0) {
+            wait(250);
+        }
+    }
+
+    protected synchronized void start(Command cmd) throws IOException {
+        if (mPending.contains(cmd)) {
+            throw new IllegalStateException("Command already started!");
+        }
+        mPending.add(cmd);
+        mConnection.addFcpListener(cmd);
+        boolean raised = true;
+        try {
+            debug("Starting: " + cmd.mName);
+            FcpMessage fcpMsg = cmd.getStartMessage();
+            mConnection.sendMessage(fcpMsg);
+            raised = false;
+        } finally {
+            if (raised) {
+                if (cmd.mFailureMsg.equals("")) {
+                    cmd.mFailureMsg = "Aborted before start. Maybe the connection dropped?";
+                }
+                commandFinished(cmd);
+            }
+        }
+    }
+
+    protected void commandFinished(Command cmd) {
+        synchronized (this) { // Can wait on runner
+            if (!mPending.contains(cmd)) {
+                return;
+            }
+            debug("Finished: " + cmd.mName + (cmd.mFailureMsg.equals("") ? ": SUCCEEDED" : ": FAILED: " + cmd.mFailureMsg));
+            mPending.remove(cmd);
+            mConnection.removeFcpListener(cmd);
+            notifyAll();
+        }
+        synchronized (cmd) { // or individual commands.
+            cmd.notifyAll();
+        }
+    }
+
+    public static void dump(FcpMessage msg) {
+        debug(msg.getName());
+        Iterator<String> itr = msg.iterator();
+        while(itr.hasNext()) {
+            String key = itr.next();
+            debug(key + "=" + msg.getFields().get(key));
+        }
+        debug("EndMessage");
+        // DCI: way to see if payloadinputstream is set without dorking FcpMessage?
+    }
+}
diff --git a/alien/src/wormarc/io/FreenetIO.java b/alien/src/wormarc/io/FreenetIO.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/FreenetIO.java
@@ -0,0 +1,376 @@
+/* An Archive.IO and ArchiveResolver implementation which read and writes to Freenet.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import wormarc.Archive;
+import wormarc.ArchiveResolver;
+import wormarc.BinaryLinkRep;
+import wormarc.Block;
+import wormarc.ExternalRefs;
+import wormarc.HistoryLink;
+import wormarc.HistoryLinkMap;
+import wormarc.IOUtil;
+import wormarc.LinkDataFactory;
+import wormarc.LinkDigest;
+import wormarc.RootObjectKind;
+
+public class FreenetIO implements Archive.IO, ArchiveResolver {
+    private LinkCache mCache;
+
+    // Transient
+    private HistoryLinkMap mLinkMap;
+    private LinkDataFactory mLinkDataFactory;
+
+    private String mHost;
+    private int mPort;
+    private String mClientName = "FreenetIO_";
+
+    private int mMaxBlockLength = 8 * 1024 * 1024;
+    private int mMaxBlockCount = 4;
+
+    private String mInsertUri;
+    private String mRequestUri;
+    private FreenetTopKey mPreviousTopKey;
+
+    private static PrintStream sDebugOut = System.err;
+
+    // Cache can be null.
+    // When it is non-null all links read from Freenet are dumped to the cache.
+    public FreenetIO(String host, int port, LinkCache cache) {
+        mHost = host;
+        mPort = port;
+        mCache = cache;
+    }
+
+    public FreenetIO(String host, int port) {
+        this(host, port, null);
+    }
+
+    public static void setDebugOutput(PrintStream out) {
+        synchronized(FreenetIO.class) {
+            sDebugOut = out;
+            FCPCommandRunner.setDebugOutput(sDebugOut);
+        }
+    }
+
+    public String getInsertUri() { return mInsertUri; }
+    public void setInsertUri(String uri) { mInsertUri = uri; }
+
+    public String getRequestUri() { return mRequestUri; }
+    public void setRequestUri(String uri) { mRequestUri = uri; }
+
+    public FreenetTopKey readTopKey(String uri) throws IOException {
+        FCPCommandRunner runner = null;
+        try {
+            runner = new FCPCommandRunner(mHost, mPort,
+                                          mClientName +
+                                          IOUtil.randomHexString(12));
+            FCPCommandRunner.GetTopKey requestTopKey =
+                runner.sendGetTopKey(uri);
+
+            runner.waitUntilAllFinished();
+            requestTopKey.raiseOnFailure();
+            return requestTopKey.getTopKey();
+
+        } catch (InterruptedException ie) {
+            throw new IOException("FreenetTopKey read timed out.", ie);
+        } finally {
+            if (runner != null) {
+                runner.disconnect();
+            }
+        }
+    }
+
+    // Speeds up inserting by allowing write() to skip blocks that were
+    // already inserted.
+    // For this to work, the PARENT_REFERENCES root object must be up to date.
+    public void maybeLoadPreviousTopKey(Archive archive) throws IOException {
+        mPreviousTopKey = null;
+        if (archive.getRootObject(RootObjectKind.PARENT_REFERENCES).isNullDigest()) {
+            return;
+        }
+
+        ExternalRefs refs =
+            ExternalRefs.fromBytes(archive.
+                                   getFile(archive.
+                                           getRootObject(RootObjectKind.PARENT_REFERENCES)));
+
+        if (refs.mRefs.size() != 1 ||
+            refs.mRefs.get(0).mKind != ExternalRefs.KIND_FREENET) {
+            throw new IOException("Expected a single Freenet URI!");
+            // LATER: Must remove this constraint to allow merging.
+        }
+
+        String topKeyUri = refs.mRefs.get(0).mExternalKey;
+        mPreviousTopKey = readTopKey(topKeyUri);
+    }
+
+    // DCI: BUG: redundant inserts are not supported yet. False assumption Block <-> CHK
+    // Updates the request URI on success.
+    public void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) throws IOException {
+        if (mInsertUri == null) {
+            throw new IllegalStateException("Set the uri!");
+        }
+
+        // DCI: fail early for inserts that are too big.
+        FCPCommandRunner runner = null;
+        try {
+            runner = new FCPCommandRunner(mHost, mPort,
+                                          mClientName +
+                                          IOUtil.randomHexString(12));
+
+            // Precompute the block CHKs so we can skip blocks that
+            // are already in Freenet.
+            List<FreenetTopKey.BlockDescription> descriptions =
+                precomputeDescriptions(runner, linkMap, blocks);
+
+            if (blocks.size() != descriptions.size()) {
+                throw new RuntimeException("Assertion Failure: blocks.size() != descriptions.size()");
+            }
+
+            Set<String> previousChks =  new HashSet<String>();
+            if (mPreviousTopKey != null) {
+                for (FreenetTopKey.BlockDescription desc : mPreviousTopKey.mBlockDescriptions) {
+                    for (int subIndex = 0; subIndex < desc.mCHKs.size(); subIndex++) {
+                        previousChks.add(desc.getCHK(subIndex));
+                    }
+                }
+            }
+
+            List<FCPCommandRunner.PutBlock> puts = new ArrayList<FCPCommandRunner.PutBlock>();
+            for (int index = 0; index < descriptions.size(); index++) {
+                FreenetTopKey.BlockDescription desc = descriptions.get(index);
+                if (previousChks.contains(desc.getCHK(0))) {
+                    // i.e. the block was already inserted, so skip it.
+                    continue;
+                }
+                puts.add(runner.sendPutBlock(index, linkMap, blocks.get(index)));
+            }
+
+            runner.waitUntilAllFinished();
+
+            for (FCPCommandRunner.PutBlock put : puts) {
+                put.raiseOnFailure();
+            }
+
+            FreenetTopKey topKey = new FreenetTopKey(rootObjects, descriptions);
+            raiseOnSuspectTopKey(topKey); // Fails, but too late!
+
+            FCPCommandRunner.PutTopKey putTopKey =
+                runner.sendPutTopKey(mInsertUri, topKey);
+
+            runner.waitUntilAllFinished();
+            putTopKey.raiseOnFailure();
+            mRequestUri = putTopKey.getUri();
+        } catch (InterruptedException ie) {
+            throw new IOException("Write timed out.", ie);
+        } catch (IllegalBase64Exception ibe) {
+            throw new IOException("Binary URI decode failed", ibe);
+        } finally {
+            if (runner != null) {
+                runner.disconnect();
+            }
+        }
+    }
+
+    private void raiseOnSuspectTopKey(FreenetTopKey topKey) throws IOException {
+        if (topKey.mBlockDescriptions.size() > mMaxBlockCount) {
+            throw new IOException(String.format("To many blocks in FreenetTopKey: %d",
+                                                topKey.mBlockDescriptions.size()));
+        }
+
+        for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions ) {
+            if (desc.mLength > mMaxBlockLength) {
+                throw new IOException(String.format("Block too big: %d",
+                                                    desc.mLength));
+            }
+        }
+        int length = IOUtil.readAndClose(topKey.toBytes()).length;
+        if (length > FreenetTopKey.MAX_LENGTH) {
+            throw new IOException("FreenetTopKey is too big!");
+        }
+    }
+
+    public Archive.ArchiveData read(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException {
+        // For now, we request everything.
+        // LATER: Think through incremental requesting. We can do much better.
+        if (linkMap == null) {
+            throw new IllegalArgumentException("linkMap == null");
+        }
+
+        if (linkFactory == null) {
+            throw new IllegalArgumentException("linkFactory == null");
+        }
+
+        FCPCommandRunner runner = null;
+        try {
+            // Read topkey
+            mLinkMap = linkMap;
+            mLinkDataFactory = linkFactory;
+
+            runner = new FCPCommandRunner(mHost, mPort,
+                                          mClientName +
+                                          IOUtil.randomHexString(12));
+
+            // Read the topkey from Freenet.
+            FCPCommandRunner.GetTopKey requestTopKey =
+                runner.sendGetTopKey(mRequestUri);
+
+            runner.waitUntilAllFinished();
+            requestTopKey.raiseOnFailure();
+
+            FreenetTopKey topKey = requestTopKey.getTopKey();
+            raiseOnSuspectTopKey(topKey);
+
+            // Read all the blocks listed in the top key.
+            // Note: The GetBlock requests read and cache the links
+            //       by calling back into readLinks(). See below.
+            int count = 0;
+            List<FCPCommandRunner.GetBlock> gets = new ArrayList<FCPCommandRunner.GetBlock>();
+            for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions ) {
+                // LATER: Handle redundant block fetches.
+                sDebugOut.println(String.format("Requesting[%d]: %s",
+                                                desc.mLength,
+                                                desc.getCHK(0)));
+                gets.add(runner.sendGetBlock(desc.getCHK(0), desc.mLength, count++, this));
+            }
+            runner.waitUntilAllFinished();
+
+            // Collect the Blocks.
+            List<Block> blocks = new ArrayList<Block>();
+            for (FCPCommandRunner.GetBlock get : gets) {
+                get.raiseOnFailure();
+                blocks.add(get.getBlock());
+            }
+            return new Archive.ArchiveData(blocks, topKey.mRootObjects);
+
+        } catch (InterruptedException ie) {
+            throw new IOException("Read timed out.", ie);
+        } catch (IllegalBase64Exception ibe) {
+            throw new IOException("Binary URI decode failed", ibe);
+        } finally {
+            mLinkMap = null;
+            mLinkDataFactory = null;
+            if (runner != null) {
+                sDebugOut.println("FCP Connection -- DISCONNECTING!");
+                runner.disconnect();
+            }
+        }
+    }
+
+    // Used by FCPCommandRunner.
+    protected Block readLinks(InputStream data) throws IOException {
+        if (mLinkDataFactory == null || mLinkMap  == null) {
+            throw new IllegalStateException("Not expecting call.");
+        }
+
+        List<LinkDigest> digests = new ArrayList<LinkDigest>();
+        while (true) {
+            HistoryLink link = BinaryLinkRep.fromBytes(data, mLinkDataFactory);
+            if (link == null) {
+                break;
+            }
+            digests.add(link.mHash);
+            mLinkMap.addLink(link);
+            if (mCache != null) {
+                mCache.writeLink(link);
+            }
+        }
+        return new Block(digests);
+    }
+
+    ////////////////////////////////////////////////////////////
+    private List<FreenetTopKey.BlockDescription> precomputeDescriptions(FCPCommandRunner runner,
+                                                                        HistoryLinkMap linkMap,
+                                                                        List<Block> blocks)
+        throws IllegalBase64Exception,
+               InterruptedException,
+               IOException {
+
+        // Use the Freenet node to tell us the CHKs for the new blocks without
+        // inserting them.
+        int count = 0;
+        List<FCPCommandRunner.GetBlockChk> getChks = new ArrayList<FCPCommandRunner.GetBlockChk>();
+        for (Block block : blocks) {
+            getChks.add(runner.sendGetBlockChk(count++, linkMap, block));
+        }
+
+        runner.waitUntilAllFinished();
+
+        int index = 0;
+        List<FreenetTopKey.BlockDescription> descriptions = new ArrayList<FreenetTopKey.BlockDescription>();
+        for (FCPCommandRunner.GetBlockChk get : getChks) {
+            get.raiseOnFailure();
+            descriptions.add(FreenetTopKey.makeDescription(get.getLength(), Arrays.asList(get.getUri())));
+            index++;
+        }
+        return descriptions;
+    }
+
+    ////////////////////////////////////////////////////////////
+
+    public Archive resolve(ExternalRefs.Reference fromReference) throws IOException {
+        String previousRequestUri = mRequestUri;
+        try {
+            if (fromReference.mKind != ExternalRefs.KIND_FREENET) {
+                throw new IOException("Reference is not a Freenet URI");
+            }
+            sDebugOut.println("resolving Archive from: " + fromReference.mExternalKey);
+            mRequestUri = fromReference.mExternalKey;
+            Archive loaded = Archive.load(this); // Hmmmm... slurps stuff into the cache. ???
+            if (!loaded.getRootObject(RootObjectKind.ARCHIVE_MANIFEST).isNullDigest()) {
+                if (!loaded.hasValidArchiveManifest()) {
+                    throw new IOException("Invalid ARCHIVE_MANIFEST: " + fromReference.mExternalKey);
+                }
+            }
+            return loaded;
+
+        } finally {
+            mRequestUri = previousRequestUri;
+        }
+    }
+
+    public String getNym(ExternalRefs.Reference fromReference) throws IOException {
+        if (fromReference.mKind != ExternalRefs.KIND_FREENET ||
+            (!fromReference.mExternalKey.startsWith("SSK@") ||
+            fromReference.mExternalKey.indexOf("/") == -1)) {
+            return "notfreenetssk";
+        }
+
+        // Public key part of the SSK.
+        return fromReference.mExternalKey.
+            substring(4, fromReference.mExternalKey.indexOf(","));
+    }
+}
diff --git a/alien/src/wormarc/io/FreenetTopKey.java b/alien/src/wormarc/io/FreenetTopKey.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/FreenetTopKey.java
@@ -0,0 +1,253 @@
+/* A class to represent the data stored in Freenet for a single Archive version.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+// DCI: decide, do exception messages have final period?
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import wormarc.Archive;
+import wormarc.BinaryLinkRep;
+import wormarc.LinkDigest;
+
+/*
+WORM0200
+<root objects>
+<block descriptions>
+
+DCI: better format doc
+*/
+public final class FreenetTopKey {
+    // MUST be 8 bytes
+    // Old incompatible binary link rep: {'W', 'O', 'R', 'M', '0', '2', '0', '0',};
+    public final static byte[] HEADER = new byte[] {'W', 'O', 'R', 'M', '0', '2', '0', '1',};
+    public final static int BINARY_CHK_LENGTH = 69;
+    public final static int ENCODED_CHK_LENGTH = 99;
+
+    public final static int MAX_LENGTH = 1024;
+
+    public final static class BlockDescription {
+        public final long mLength;
+        public final List<byte[]> mCHKs; // INTENT: Allow redundant insertion of the same data.
+        public BlockDescription(long length, List<byte[]> chks) {
+            mLength = length;
+            mCHKs = Collections.unmodifiableList(chks);
+            if (mCHKs.size() < 1) {
+                throw new IllegalArgumentException("Must have at least one CHK!");
+            }
+        }
+
+        public String getCHK(int index) throws IllegalBase64Exception {
+            return binaryToUri(mCHKs.get(index));
+        }
+    }
+
+    public final String mVersion;
+    public final List<Archive.RootObject> mRootObjects;
+    public final List<BlockDescription> mBlockDescriptions;
+
+    public FreenetTopKey(String version,
+                         List<Archive.RootObject> rootObjects,
+                         List<BlockDescription> blockDescriptions)
+        throws UnsupportedEncodingException {
+        mVersion = version;
+        mRootObjects = Collections.unmodifiableList(rootObjects);
+        mBlockDescriptions = Collections.unmodifiableList(blockDescriptions);
+        if (mVersion.getBytes("ascii").length != 8) {
+            throw new RuntimeException("version header != 8 bytes");
+        }
+        if (mBlockDescriptions.size() < 1) {
+            throw new IllegalArgumentException("Must have at least one block description.");
+        }
+    }
+
+    public FreenetTopKey(List<Archive.RootObject> rootObjects,
+                         List<BlockDescription> blockDescriptions)
+        throws UnsupportedEncodingException {
+        this(new String(HEADER, "ascii"), rootObjects, blockDescriptions);
+    }
+
+    private final static void checkIsShort(int value) {
+        if (value < 0 || value > 32767) {
+            throw new IllegalArgumentException("Expected value between 0 and 32767.");
+        }
+    }
+
+    public InputStream toBytes() throws IOException {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(buffer);
+        outputStream.write(HEADER);
+
+        checkIsShort(mRootObjects.size());
+        outputStream.writeShort(mRootObjects.size());
+        for (Archive.RootObject obj : mRootObjects) {
+            outputStream.write(obj.mDigest.getBytes());
+            outputStream.writeInt(obj.mKind);
+        }
+
+        checkIsShort(mBlockDescriptions.size());
+        outputStream.writeShort(mBlockDescriptions.size());
+        for (BlockDescription desc : mBlockDescriptions) {
+            outputStream.writeLong(desc.mLength);
+            checkIsShort(desc.mCHKs.size());
+            outputStream.writeShort(desc.mCHKs.size());
+            for (byte[] bytes : desc.mCHKs) {
+                if (bytes.length != BINARY_CHK_LENGTH) {
+                    throw new IllegalArgumentException("Not a binary CHK???");
+                }
+                outputStream.write(bytes);
+            }
+        }
+        outputStream.close();
+        byte[] bytes = buffer.toByteArray();
+        if (bytes.length > MAX_LENGTH) {
+            throw new IOException("Too big");
+        }
+        return new ByteArrayInputStream(bytes);
+    }
+
+    public static FreenetTopKey fromBytes(InputStream rawBytes) throws IOException {
+        DataInputStream inputStream = new DataInputStream(rawBytes);
+        try {
+            byte[] versionBytes = new byte[HEADER.length];
+            inputStream.readFully(versionBytes);
+            if (!Arrays.equals(HEADER, versionBytes)) {
+                throw new IOException("Version mismatch or bad data.");
+            }
+
+            List<Archive.RootObject> rootObjects = new ArrayList<Archive.RootObject>();
+            int rootObjectCount = inputStream.readShort();
+            while (rootObjectCount > 0) {
+                LinkDigest digest = BinaryLinkRep.readLinkDigest(inputStream);
+                int kind = inputStream.readInt();
+                rootObjects.add(new Archive.RootObject(digest, kind));
+                rootObjectCount--;
+            }
+
+            List<BlockDescription> blockDescriptions = new ArrayList<BlockDescription>();
+            int blockDescriptionCount = inputStream.readShort();
+            while (blockDescriptionCount > 0) {
+                long blockLength = inputStream.readLong();
+                List<byte[]> blockChks = new ArrayList<byte[]>();
+                int blockChkCount = inputStream.readShort();
+                while (blockChkCount > 0) {
+                    byte[] chkBytes = new byte[BINARY_CHK_LENGTH];
+                    inputStream.readFully(chkBytes);
+                    blockChks.add(chkBytes);
+                    blockChkCount--;
+                }
+                if (blockChks.size() < 1) {
+                    throw new IOException("Block description must have at least one CHK.");
+                }
+                blockDescriptions.add(new BlockDescription(blockLength, blockChks));
+                blockDescriptionCount--;
+            }
+            if (blockDescriptions.size() < 1) {
+                throw new IOException("Must have at least one block description.");
+            }
+            return new FreenetTopKey(new String(versionBytes, "ascii"),
+                                     Collections.unmodifiableList(rootObjects),
+                                     Collections.unmodifiableList(blockDescriptions));
+        } finally {
+            //rawBytes.close(); DCI: Don't close the stream passed to you by AllData!
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    public final static byte[] chkUriToBinary(String chkUri) throws IllegalBase64Exception {
+        if (!chkUri.startsWith("CHK@")) {
+            throw new IllegalArgumentException("Must start with 'CHK@'.");
+        }
+        if (chkUri.length() != ENCODED_CHK_LENGTH) {
+            throw new IllegalArgumentException("Only raw CHKs allowed. No trailing '\' or filename part.");
+        }
+        String[] fields = chkUri.substring(4).split(",");
+        if (fields.length != 3) {
+            throw new IllegalArgumentException("Couldn't parse ',' delimited fields.");
+        }
+
+        byte[] binaryRep = new byte[BINARY_CHK_LENGTH];
+
+        int offset = 0;
+        for (int index : new int[] {2, 0, 1}) {
+            byte[] decoded = Base64.decode(fields[index]);
+            System.arraycopy(decoded, 0, binaryRep, offset, decoded.length);
+            offset += decoded.length;
+        }
+        return binaryRep;
+    }
+    private final static String encodeRange(byte[] binaryRep, int startPos, int endPos) {
+        int length = endPos - startPos;
+        byte[] decoded = new byte[length];
+        System.arraycopy(binaryRep, startPos, decoded, 0, length);
+        return Base64.encode(decoded);
+    }
+
+    public final static String binaryToUri(byte[] binaryRep) {
+        if (binaryRep.length != BINARY_CHK_LENGTH) {
+            throw new IllegalArgumentException(String.format("Expected %d bytes.", BINARY_CHK_LENGTH));
+        }
+
+        StringBuilder buffer = new StringBuilder("CHK@");
+        buffer.append(encodeRange(binaryRep, 5, 37));
+        buffer.append(',');
+        buffer.append(encodeRange(binaryRep, 37, 69));
+        buffer.append(',');
+        buffer.append(encodeRange(binaryRep, 0, 5));
+        String asString = buffer.toString();
+        if (asString.length() != ENCODED_CHK_LENGTH) {
+            throw new RuntimeException("Assertion Failure: Wrong length?");
+        }
+
+        return asString;
+    }
+
+    public final static List<byte[]> chkUrisToBinary(List<String> chkUris) throws IllegalBase64Exception {
+        List<byte[]> binaryList = new ArrayList<byte[]>();
+        for (String uri : chkUris) {
+            binaryList.add(chkUriToBinary(uri));
+        }
+        return binaryList;
+    }
+
+    // Java is teh suXOR.
+    // You can't overload the BlockDescription ctr because of "same erasure" error.
+    // http://download.oracle.com/javase/1.4.2/docs/api/java/lang/System.html \
+    //      #arraycopy%28java.lang.Object,%20int,%20java.lang.Object,%20int,%20int%29
+    public final static BlockDescription makeDescription(long length, List<String> chkUris) throws IllegalBase64Exception {
+        return new BlockDescription(length, chkUrisToBinary(chkUris));
+    }
+}
+
diff --git a/alien/src/wormarc/io/IllegalBase64Exception.java b/alien/src/wormarc/io/IllegalBase64Exception.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/IllegalBase64Exception.java
@@ -0,0 +1,18 @@
+// Pillaged from the Freenet codebase, licensed under GNU General Public License V2
+// package freenet.support;
+
+package wormarc.io;
+
+/**
+ * This exception is thrown if a Base64-encoded string is of an illegal length
+ * or contains an illegal character.
+ */
+public class IllegalBase64Exception
+    extends Exception
+{
+    private static final long serialVersionUID = -1;
+    public IllegalBase64Exception(String descr)
+    {
+        super(descr);
+    }
+}
diff --git a/alien/src/wormarc/io/LinkCache.java b/alien/src/wormarc/io/LinkCache.java
new file mode 100644
--- /dev/null
+++ b/alien/src/wormarc/io/LinkCache.java
@@ -0,0 +1,98 @@
+/* A helper class to store links on the local file system.
+ *
+ *  Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.0 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package wormarc.io;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import wormarc.BinaryLinkRep;
+import wormarc.HistoryLink;
+import wormarc.HistoryLinkMap;
+import wormarc.LinkDataFactory;
+import wormarc.LinkDigest;
+
+public class LinkCache {
+    protected File mDirectory;
+
+    // Flat directory for now. Could use subdirs based on start of digest, like git.
+    protected File getLinkFile(LinkDigest digest) {
+        return new File(mDirectory, digest.hexDigest(20));
+    }
+
+    public LinkCache(String directory) throws IOException {
+        if (directory == null) {
+            throw new IllegalArgumentException("directory is null.");
+        }
+
+       File file =  new File(directory);
+       if (!(file.exists() && file.isDirectory() && file.canWrite() && file.canRead())) {
+           throw new IOException("Directory must exist and have read and write access.");
+       }
+       mDirectory = file;
+    }
+
+    public HistoryLink readLink(HistoryLinkMap linkMap, LinkDataFactory linkFactory, LinkDigest digest)
+        throws IOException {
+
+        if (digest.isNullDigest()) {
+            throw new IOException("Refused to read NULL_DIGEST link.");
+        }
+
+        InputStream inputStream = new FileInputStream(getLinkFile(digest));
+        try {
+            HistoryLink link = BinaryLinkRep.fromBytes(inputStream, linkFactory);
+            if (link == null) {
+                throw new IOException(String.format("Link: %s not found on file system.", digest.toString()));
+            }
+            linkMap.addLink(link);
+            return link;
+        } finally {
+            inputStream.close();
+        }
+    }
+
+    public void writeLink(HistoryLink link) throws IOException {
+        if (link.mHash.isNullDigest()) {
+            throw new IOException("Refused to write NULL_DIGEST link.");
+        }
+
+        OutputStream outputStream = new FileOutputStream(getLinkFile(link.mHash));
+        boolean raised = true;
+        try {
+            BinaryLinkRep.write(outputStream, link);
+            raised = false;
+        } finally {
+            outputStream.close();
+            if (raised && getLinkFile(link.mHash).exists()) {
+                // Don't leave corrupt link files on disk.
+                getLinkFile(link.mHash).delete();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/alien/src/ys/wikiparser/Utils.java b/alien/src/ys/wikiparser/Utils.java
new file mode 100644
--- /dev/null
+++ b/alien/src/ys/wikiparser/Utils.java
@@ -0,0 +1,282 @@
+// djk20101227 Manually converted this file from ISO-8859-1 to utf-8 using:
+// iconv -f ISO-8859-1 -t utf-8 Utils.java > converted.java
+/*
+ * Copyright (c) 2007 Yaroslav Stavnichiy, yarosla@gmail.com
+ *
+ * Latest version of this software can be obtained from:
+ *   http://web-tec.info/WikiParser/
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * If you make use of this code, I'd appreciate hearing about it.
+ * Comments, suggestions, and bug reports welcome: yarosla@gmail.com
+ */
+
+package ys.wikiparser;
+
+import java.util.*;
+
+public class Utils {
+
+  public static boolean isUrlChar(char c) {
+    // From MediaWiki: "._\\/~%-+&#?!=()@"
+    // From http://www.ietf.org/rfc/rfc2396.txt :
+    //   reserved:   ";/?:@&=+$,"
+    //   unreserved: "-_.!~*'()"
+    //   delim:      "%#"
+    if (isLatinLetterOrDigit(c)) return true;
+    return "/?@&=+,-_.!~()%#;:$*".indexOf(c)>=0; // I excluded '\''
+  }
+
+  public static boolean isLatinLetterOrDigit(char c) {
+    return (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9');
+  }
+
+  /**
+   * Filters text so there are no '\r' chars in it ("\r\n" -> "\n"; then "\r" -> "\n").
+   * Most importantly makes all blank lines (lines with only spaces) exactly like this: "\n\n".
+   * WikiParser relies on that.
+   *
+   * @param text
+   * @return filtered text
+   */
+  public static String preprocessWikiText(String text) {
+    if (text==null) return "";
+    text=text.trim();
+    int length=text.length();
+    char[] chars=new char[length];
+    text.getChars(0, length, chars, 0);
+    StringBuilder sb=new StringBuilder();
+    boolean blankLine=true;
+    StringBuilder spaces=new StringBuilder();
+    for (int p=0; p<length; p++) {
+      char c=chars[p];
+      if (c=='\r') { // "\r\n" -> "\n"; then "\r" -> "\n"
+        if (p+1<length && chars[p+1]=='\n') p++;
+        sb.append('\n');
+        spaces.delete(0, spaces.length()); // discard spaces if there is nothing else on the line
+        blankLine=true;
+      }
+      else if (c=='\n') {
+        sb.append(c);
+        spaces.delete(0, spaces.length()); // discard spaces if there is nothing else on the line
+        blankLine=true;
+      }
+      else if (blankLine) {
+        if (c<=' '/* && c!='\n'*/) {
+          spaces.append(c);
+        }
+        else {
+          sb.append(spaces);
+          blankLine=false;
+          sb.append(c);
+        }
+      }
+      else {
+        sb.append(c);
+      }
+    }
+    return sb.toString();
+  }
+
+  public static String escapeHTML(String s) {
+    if (s==null) return "";
+    StringBuffer sb=new StringBuffer(s.length()+100);
+    int length=s.length();
+
+    for (int i=0; i<length; i++) {
+      char ch=s.charAt(i);
+
+      if ('<'==ch) {
+        sb.append("<");
+      }
+      else if ('>'==ch) {
+        sb.append(">");
+      }
+      else if ('&'==ch) {
+        sb.append("&");
+      }
+      else if ('\''==ch) {
+        sb.append("'");
+      }
+      else if ('"'==ch) {
+        sb.append(""");
+      }
+      else {
+        sb.append(ch);
+      }
+    }
+    return sb.toString();
+  }
+
+  private static HashMap<String,Character> entities=null;
+
+  private static synchronized HashMap<String,Character> getHtmlEntities() {
+    if (entities==null) {
+      entities=new HashMap<String, Character>();
+      entities.put("lt", '<');
+      entities.put("gt", '>');
+      entities.put("amp", '&');
+      entities.put("quot", '"');
+      entities.put("apos", '\'');
+      entities.put("nbsp", '\u00A0');
+      entities.put("shy", '\u00AD');
+      entities.put("copy", '\u00A9');
+      entities.put("reg", '\u00AE');
+      entities.put("trade", '\u2122');
+      entities.put("mdash", '\u2014');
+      entities.put("ndash", '\u2013');
+      entities.put("ldquo", '\u201C');
+      entities.put("rdquo", '\u201D');
+      entities.put("euro", '\u20AC');
+      entities.put("middot", '\u00B7');
+      entities.put("bull", '\u2022');
+      entities.put("laquo", '\u00AB');
+      entities.put("raquo", '\u00BB');
+    }
+    return entities;
+  }
+
+  public static String unescapeHTML(String value) {
+    if (value==null) return null;
+    if (value.indexOf('&')<0) return value;
+    HashMap<String,Character> ent=getHtmlEntities();
+    StringBuffer sb=new StringBuffer();
+    final int length=value.length();
+    for (int i=0; i<length; i++) {
+      char c=value.charAt(i);
+      if (c=='&') {
+        char ce=0;
+        int i1=value.indexOf(';', i+1);
+        if (i1>i && i1-i<=12) {
+          if (value.charAt(i+1)=='#') {
+            if (value.charAt(i+2)=='x') {
+              ce=(char)atoi(value.substring(i+3, i1), 16);
+            }
+            else {
+              ce=(char)atoi(value.substring(i+2, i1));
+            }
+          }
+          else {
+            synchronized (ent) {
+              Character ceObj=ent.get(value.substring(i+1, i1));
+              ce=ceObj==null?0:ceObj.charValue();
+            }
+          }
+        }
+        if (ce>0) {
+          sb.append(ce);
+          i=i1;
+        }
+        else sb.append(c);
+      }
+      else {
+        sb.append(c);
+      }
+    }
+    return sb.toString();
+  }
+
+  static public int atoi(String s) {
+    try {
+      return Integer.parseInt(s);
+    }
+    catch (Throwable ex) {
+      return 0;
+    }
+  }
+
+  static public int atoi(String s, int base) {
+    try {
+      return Integer.parseInt(s, base);
+    }
+    catch (Throwable ex) {
+      return 0;
+    }
+  }
+
+  public static String replaceString(String str, String from, String to) {
+    StringBuffer buf = new StringBuffer();
+    int flen = from.length();
+    int i1=0, i2=0;
+    while ( (i2 = str.indexOf(from,i1)) >= 0 ) {
+      buf.append(str.substring(i1, i2));
+      buf.append(to);
+      i1 = i2 + flen;
+    }
+    buf.append(str.substring(i1));
+    return buf.toString();
+  }
+
+  public static String[] split(String s, char separator) {
+    // this is meant to be faster than String.split() when separator is not regexp
+    if (s==null) return null;
+    ArrayList<String> parts=new ArrayList<String>();
+    int beginIndex=0, endIndex;
+    while ((endIndex=s.indexOf(separator, beginIndex))>=0) {
+      parts.add(s.substring(beginIndex, endIndex));
+      beginIndex=endIndex+1;
+    }
+    parts.add(s.substring(beginIndex));
+    String[] a=new String[parts.size()];
+    return parts.toArray(a);
+  }
+
+  private static final String translitTable="àaábâvãgädåe¸eæzhçzèiéyêkëlìmínîoïpðrñsòtóuôfõhöts÷chøshùschüûyúýeþyuÿyaÀAÁBÂVÃGÄDÅE¨EÆZHÇZÈIÉYÊKËLÌMÍNÎOÏPÐRÑSÒTÓUÔFÕHÖTS×CHØSHÙSCHÜÛYÚÝEÞYUßYA";
+
+  /**
+   * Translates all non-basic-latin-letters characters into latin ones for use in URLs etc.
+   * Here is the implementation for cyrillic (Russian) alphabet. Unknown characters are omitted.
+   *
+   * @param s string to be translated
+   * @return translated string
+   */
+  public static String translit(String s) {
+    if (s==null) return "";
+    StringBuilder sb=new StringBuilder(s.length()+100);
+    final int length=s.length();
+    final int translitTableLength=translitTable.length();
+
+    for (int i=0; i<length; i++) {
+      char ch=s.charAt(i);
+      //System.err.println("ch="+(int)ch);
+
+      if ((ch>='à' && ch<='ÿ') || (ch>='À' && ch<='ß') || ch=='¸' || ch=='¨') {
+        int idx=translitTable.indexOf(ch);
+        char c;
+        if (idx>=0) {
+          for (idx++; idx<translitTableLength; idx++) {
+            c=translitTable.charAt(idx);
+            if ((c>='à' && c<='ÿ') || (c>='À' && c<='ß') || c=='¸' || c=='¨') break;
+            sb.append(c);
+          }
+        }
+      }
+      else {
+        sb.append(ch);
+      }
+    }
+    return sb.toString();
+  }
+
+  public static String emptyToNull(String s) { return "".equals(s)?null:s; }
+  public static String noNull(String s) { return s==null?"":s; }
+  public static String noNull(String s, String val) { return s==null?val:s; }
+  public static boolean isEmpty(String s) { return (s == null || s.length() == 0); }
+}
diff --git a/alien/src/ys/wikiparser/WikiParser.java b/alien/src/ys/wikiparser/WikiParser.java
new file mode 100644
--- /dev/null
+++ b/alien/src/ys/wikiparser/WikiParser.java
@@ -0,0 +1,789 @@
+/*
+ * Copyright 2007-2009 Yaroslav Stavnichiy, yarosla@gmail.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Latest version of this software can be obtained from:
+ *
+ *     http://t4-wiki-parser.googlecode.com/
+ *
+ * If you make use of this code, I'd appreciate hearing about it.
+ * Comments, suggestions, and bug reports welcome: yarosla@gmail.com
+ */
+
+package ys.wikiparser;
+
+import static ys.wikiparser.Utils.*;
+
+import java.net.*;
+import java.util.HashSet;
+
+/**
+ * WikiParser.renderXHTML() is the main method of this class.
+ * It takes wiki-text and returns XHTML.
+ *
+ * WikiParser's behavior can be customized by overriding appendXxx() methods,
+ * which should make integration of this class into any wiki/blog/forum software
+ * easy and painless.
+ *
+ * @author Yaroslav Stavnichiy (yarosla@gmail.com)
+ *
+ */
+public class WikiParser {
+
+  private int wikiLength;
+  private char wikiChars[];
+  protected StringBuilder sb=new StringBuilder();
+  protected StringBuilder toc=new StringBuilder();
+  protected int tocLevel=0;
+  private HashSet<String> tocAnchorIds=new HashSet<String>();
+  private String wikiText;
+  private int pos=0;
+  private int listLevel=-1;
+  private static final int MAX_LIST_LEVELS=100;
+  private char listLevels[]=new char[MAX_LIST_LEVELS+1]; // max number of levels allowed
+  private boolean blockquoteBR=false;
+  private boolean inTable=false;
+  private int mediawikiTableLevel=0;
+
+  protected int HEADING_LEVEL_SHIFT=1; // make =h2, ==h3, ...
+  protected String HEADING_ID_PREFIX=null;
+
+  private static enum ContextType {PARAGRAPH, LIST_ITEM, TABLE_CELL, HEADER, NOWIKI_BLOCK};
+
+  private static final String[] ESCAPED_INLINE_SEQUENCES= {"{{{", "{{", "}}}", "**", "//", "__", "##", "\\\\", "[[", "<<<", "~", "--", "|"};
+
+  private static final String LIST_CHARS="*-#>:!";
+  private static final String[] LIST_OPEN= {"<ul><li>", "<ul><li>", "<ol><li>", "<blockquote>", "<div class='indent'>", "<div class='center'>"};
+  private static final String[] LIST_CLOSE= {"</li></ul>\n", "</li></ul>\n", "</li></ol>\n", "</blockquote>\n", "</div>\n", "</div>\n"};
+
+  private static final String FORMAT_CHARS="*/_#";
+  private static final String[] FORMAT_DELIM= {"**", "//", "__", "##"};
+  private static final String[] FORMAT_TAG_OPEN= {"<strong>", "<em>", "<span class=\"underline\">", "<tt>"};
+  private static final String[] FORMAT_TAG_CLOSE= {"</strong>", "</em>", "</span>", "</tt>"};
+
+
+  public static String renderXHTML(String wikiText) {
+    return new WikiParser(wikiText).toString();
+  }
+
+  protected void parse(String wikiText) {
+    wikiText=preprocessWikiText(wikiText);
+
+    this.wikiText=wikiText;
+    wikiLength=this.wikiText.length();
+    wikiChars=new char[wikiLength];
+    this.wikiText.getChars(0, wikiLength, wikiChars, 0);
+
+    while (parseBlock());
+
+    closeListsAndTables();
+
+    while (mediawikiTableLevel-->0) sb.append("</td></tr></table>\n");
+
+    completeTOC();
+  }
+
+  protected WikiParser() {
+    // for use by subclasses only
+    // subclasses should call parse() to complete construction
+  }
+
+  protected WikiParser(String wikiText) {
+    parse(wikiText);
+  }
+
+  public String toString() {
+    return sb.toString();
+  }
+
+  private void closeListsAndTables() {
+    // close unclosed lists
+    while (listLevel>=0) {
+      sb.append(LIST_CLOSE[LIST_CHARS.indexOf(listLevels[listLevel--])]);
+    }
+    if (inTable) {
+      sb.append("</table>\n");
+      inTable=false;
+    }
+  }
+
+  private boolean parseBlock() {
+    for (; pos<wikiLength && wikiChars[pos]<=' ' && wikiChars[pos]!='\n'; pos++) ; // skip whitespace
+    if (pos>=wikiLength) return false;
+
+    char c=wikiChars[pos];
+
+    if (c=='\n') { // blank line => end of list/table; no other meaning
+      closeListsAndTables();
+      pos++;
+      return true;
+    }
+
+    if (c=='|') { // table
+      if (mediawikiTableLevel>0) {
+        int pp=pos+1;
+        if (pp<wikiLength) {
+          boolean newRow=false, endTable=false;
+          if(wikiChars[pp]=='-') { // mediawiki-table new row
+            newRow=true;
+            pp++;
+          }
+          else if(wikiChars[pp]=='}') { // mediawiki-table end table
+            endTable=true;
+            pp++;
+          }
+          for (; pp<wikiLength && (wikiChars[pp]==' ' || wikiChars[pp]=='\t'); pp++) ; // skip spaces
+          if (pp==wikiLength || wikiChars[pp]=='\n') { // nothing else on the line => it's mediawiki-table markup
+            closeListsAndTables(); // close lists if any
+            sb.append(newRow? "</td></tr>\n<tr><td>":(endTable? "</td></tr></table>\n":"</td>\n<td>"));
+            if (endTable) mediawikiTableLevel--;
+            pos=pp+1;
+            return pp<wikiLength;
+          }
+        }
+      }
+
+      if (!inTable) {
+        closeListsAndTables(); // close lists if any
+        sb.append("<table border=\"1\">");
+        inTable=true;
+      }
+      pos=parseTableRow(pos+1);
+      return true;
+    }
+    else {
+      if (inTable) {
+        sb.append("</table>\n");
+        inTable=false;
+      }
+    }
+
+    if (listLevel>=0 || LIST_CHARS.indexOf(c)>=0) { // lists
+      int lc;
+      // count list level
+      for (lc=0; lc<=listLevel && pos+lc<wikiLength && wikiChars[pos+lc]==listLevels[lc]; lc++) ;
+
+      if (lc<=listLevel) { // end list block(s)
+        do {
+          sb.append(LIST_CLOSE[LIST_CHARS.indexOf(listLevels[listLevel--])]);
+        } while (lc<=listLevel);
+        // list(s) closed => retry from the same position
+        blockquoteBR=true;
+        return true;
+      }
+      else {
+        if (pos+lc>=wikiLength) return false;
+        char cc=wikiChars[pos+lc];
+        int listType=LIST_CHARS.indexOf(cc);
+        if (listType>=0 && pos+lc+1<wikiLength && wikiChars[pos+lc+1]!=cc && listLevel<MAX_LIST_LEVELS) { // new list block
+          sb.append(LIST_OPEN[listType]);
+          listLevels[++listLevel]=cc;
+          blockquoteBR=true;
+          pos=parseListItem(pos+lc+1);
+          return true;
+        }
+        else if (listLevel>=0) { // list item - same level
+          if (listLevels[listLevel]=='>' || listLevels[listLevel]==':') sb.append('\n');
+          else if (listLevels[listLevel]=='!') sb.append("</div>\n<div class='center'>");
+          else sb.append("</li>\n<li>");
+          pos=parseListItem(pos+lc);
+          return true;
+        }
+      }
+    }
+
+    if (c=='=') { // heading
+      int hc;
+      // count heading level
+      for (hc=1; hc<6 && pos+hc<wikiLength && wikiChars[pos+hc]=='='; hc++) ;
+      if (pos+hc>=wikiLength) return false;
+      int p;
+      for (p=pos+hc; p<wikiLength && (wikiChars[p]==' ' || wikiChars[p]=='\t'); p++) ; // skip spaces
+      String tagName="h"+(hc+HEADING_LEVEL_SHIFT);
+      sb.append("<"+tagName+" id=''>"); // real id to be inserted after parsing this item
+      int hStart=sb.length();
+      pos=parseItem(p, wikiText.substring(pos, pos+hc), ContextType.HEADER);
+      String hText=sb.substring(hStart, sb.length());
+      sb.append("</"+tagName+">\n");
+      String anchorId=generateTOCAnchorId(hc, hText);
+      sb.insert(hStart-2, anchorId);
+      appendTOCItem(hc, anchorId, hText);
+      return true;
+    }
+    else if (c=='{') { // nowiki-block?
+      if (pos+2<wikiLength && wikiChars[pos+1]=='{' && wikiChars[pos+2]=='{') {
+        int startNowiki=pos+3;
+        int endNowiki=findEndOfNowiki(startNowiki);
+        int endPos=endNowiki+3;
+        if (wikiText.lastIndexOf('\n', endNowiki)>=startNowiki) { // block <pre>
+          if (wikiChars[startNowiki]=='\n') startNowiki++; // skip the very first '\n'
+          if (wikiChars[endNowiki-1]=='\n') endNowiki--; // omit the very last '\n'
+          sb.append("<pre>");
+          appendNowiki(wikiText.substring(startNowiki, endNowiki));
+          sb.append("</pre>\n");
+          pos=endPos;
+          return true;
+        }
+        // else inline <nowiki> - proceed to regular paragraph handling
+      }
+      else if (pos+1<wikiLength && wikiChars[pos+1]=='|') { // mediawiki-table?
+        int pp;
+        for (pp=pos+2; pp<wikiLength && (wikiChars[pp]==' ' || wikiChars[pp]=='\t'); pp++) ; // skip spaces
+        if (pp==wikiLength || wikiChars[pp]=='\n') { // yes, it's start of a table
+          sb.append("<table border=\"1\"><tr><td>");
+          mediawikiTableLevel++;
+          pos=pp+1;
+          return pp<wikiLength;
+        }
+      }
+    }
+    else if (c=='-' && wikiText.startsWith("----", pos)) {
+      int p;
+      for (p=pos+4; p<wikiLength && (wikiChars[p]==' ' || wikiChars[p]=='\t'); p++) ; // skip spaces
+      if (p==wikiLength || wikiChars[p]=='\n') {
+        sb.append("\n<hr/>\n");
+        pos=p;
+        return true;
+      }
+    }
+    else if (c=='~') { // block-level escaping: '*' '-' '#' '>' ':' '!' '|' '='
+      if (pos+1<wikiLength) {
+        char nc=wikiChars[pos+1];
+        if (nc=='>' || nc==':' || nc=='-' || nc=='|' || nc=='=' || nc=='!') { // can't be inline markup
+          pos++; // skip '~' and proceed to regular paragraph handling
+          c=nc;
+        }
+        else if (nc=='*' || nc=='#') { // might be inline markup so need to double check
+          char nnc=pos+2<wikiLength? wikiChars[pos+2]:0;
+          if (nnc!=nc) {
+            pos++; // skip '~' and proceed to regular paragraph handling
+            c=nc;
+          }
+          // otherwise escaping will be done at line level
+        }
+        else if (nc=='{') { // might be inline {{{ markup so need to double check
+          char nnc=pos+2<wikiLength? wikiChars[pos+2]:0;
+          if (nnc=='|') { // mediawiki-table?
+            pos++; // skip '~' and proceed to regular paragraph handling
+            c=nc;
+          }
+          // otherwise escaping will be done at line level
+        }
+      }
+    }
+
+    { // paragraph handling
+      sb.append("<p>");
+      pos=parseItem(pos, null, ContextType.PARAGRAPH);
+      sb.append("</p>\n");
+      return true;
+    }
+  }
+
+  /**
+   * Finds first closing '}}}' for nowiki block or span.
+   * Skips escaped sequences: '~}}}'.
+   *
+   * @param startBlock points to first char after '{{{'
+   * @return position of first '}' in closing '}}}'
+   */
+  private int findEndOfNowiki(int startBlock) {
+    // NOTE: this method could step back one char from startBlock position
+    int endBlock=startBlock-3;
+    do {
+      endBlock=wikiText.indexOf("}}}", endBlock+3);
+      if (endBlock<0) return wikiLength; // no matching '}}}' found
+      while (endBlock+3<wikiLength && wikiChars[endBlock+3]=='}')
+        endBlock++; // shift to end of sequence of more than 3x'}' (eg. '}}}}}')
+    } while (wikiChars[endBlock-1]=='~');
+    return endBlock;
+  }
+
+  /**
+   * Greedy version of findEndOfNowiki().
+   * It finds the last possible closing '}}}' before next opening '{{{'.
+   * Also uses escapes '~{{{' and '~}}}'.
+   *
+   * @param startBlock points to first char after '{{{'
+   * @return position of first '}' in closing '}}}'
+   */
+  @SuppressWarnings("unused")
+  private int findEndOfNowikiGreedy(int startBlock) {
+    // NOTE: this method could step back one char from startBlock position
+    int nextBlock=startBlock-3;
+    do {
+      do {
+        nextBlock=wikiText.indexOf("{{{", nextBlock+3);
+      } while (nextBlock>0 && wikiChars[nextBlock-1]=='~');
+      if (nextBlock<0) nextBlock=wikiLength;
+      int endBlock=wikiText.lastIndexOf("}}}", nextBlock);
+      if (endBlock>=startBlock && wikiChars[endBlock-1]!='~') return endBlock;
+    } while (nextBlock<wikiLength);
+    return wikiLength;
+  }
+
+  /**
+   * @param start points to first char after pipe '|'
+   * @return
+   */
+  private int parseTableRow(int start) {
+    if (start>=wikiLength) return wikiLength;
+
+    sb.append("<tr>");
+    boolean endOfRow=false;
+    do {
+      int colspan=0;
+      while (start+colspan<wikiLength && wikiChars[start+colspan]=='|') colspan++;
+      start+=colspan;
+      colspan++;
+      boolean th=start<wikiLength && wikiChars[start]=='=';
+      start+=(th?1:0);
+      while (start<wikiLength && wikiChars[start]<=' ' && wikiChars[start]!='\n') start++; // trim whitespace from the start
+
+      if (start>=wikiLength || wikiChars[start]=='\n') { // skip last empty column
+        start++; // eat '\n'
+        break;
+      }
+
+      sb.append(th? "<th":"<td");
+      if (colspan>1) sb.append(" colspan=\""+colspan+"\"");
+      sb.append('>');
+      try {
+        parseItemThrow(start, null, ContextType.TABLE_CELL);
+      }
+      catch (EndOfSubContextException e) { // end of cell
+        start=e.position;
+        if (start>=wikiLength) endOfRow=true;
+        else if (wikiChars[start]=='\n') {
+          start++; // eat '\n'
+          endOfRow=true;
+        }
+      }
+      catch (EndOfContextException e) {
+        start=e.position;
+        endOfRow=true;
+      }
+      sb.append(th? "</th>":"</td>");
+    } while (!endOfRow/* && start<wikiLength && wikiChars[start]!='\n'*/);
+    sb.append("</tr>\n");
+    return start;
+  }
+
+  /**
+   * Same as parseItem(); blank line adds <br/><br/>
+   *
+   * @param start
+   */
+  private int parseListItem(int start) {
+    while (start<wikiLength && wikiChars[start]<=' ' && wikiChars[start]!='\n') start++; // skip spaces
+    int end=parseItem(start, null, ContextType.LIST_ITEM);
+    if ((listLevels[listLevel]=='>' || listLevels[listLevel]==':') && wikiText.substring(start, end).trim().length()==0) { // empty line within blockquote/div
+      if (!blockquoteBR) {
+        sb.append("<br/><br/>");
+        blockquoteBR=true;
+      }
+    }
+    else {
+      blockquoteBR=false;
+    }
+    return end;
+  }
+
+  /**
+   * @param p points to first slash in suspected URI (scheme://etc)
+   * @param start points to beginning of parsed item
+   * @param end points to end of parsed item
+   *
+   * @return array of two integer offsets [begin_uri, end_uri] if matched, null otherwise
+   */
+  private int[] checkURI(int p, int start, int end) {
+    if (p>start && wikiChars[p-1]==':') { // "://" found
+      int pb=p-1;
+      while (pb>start && isLatinLetterOrDigit(wikiChars[pb-1])) pb--;
+      int pe=p+2;
+      while (pe<end && isUrlChar(wikiChars[pe])) pe++;
+      URI uri=null;
+      do {
+        while (pe>p+2 && ",.;:?!%)".indexOf(wikiChars[pe-1])>=0) pe--; // don't want these chars at the end of URI
+        try { // verify URL syntax
+          uri=new URI(wikiText.substring(pb, pe));
+        }
+        catch (URISyntaxException e) {
+          pe--; // try chopping from the end
+        }
+      } while (uri==null && pe>p+2);
+      if (uri!=null && uri.isAbsolute() && !uri.isOpaque()) {
+        int offs[]= {pb, pe};
+        return offs;
+      }
+    }
+    return null;
+  }
+
+  private int parseItem(int start, String delimiter, ContextType context) {
+    try {
+      return parseItemThrow(start, delimiter, context);
+    }
+    catch (EndOfContextException e) {
+      return e.position;
+    }
+  }
+
+  private int parseItemThrow(int start, String delimiter, ContextType context) throws EndOfContextException {
+    StringBuilder tb=new StringBuilder();
+
+    boolean specialCaseDelimiterHandling="//".equals(delimiter);
+    int p=start;
+    int end=wikiLength;
+
+    try {
+      nextChar: while(true) {
+        if (p>=end) throw new EndOfContextException(end); //break;
+
+        if (delimiter!=null && wikiText.startsWith(delimiter, p)) {
+          if (!specialCaseDelimiterHandling || checkURI(p, start, end)==null) {
+            p+=delimiter.length();
+            return p;
+          }
+        }
+
+        char c=wikiChars[p];
+        boolean atLineStart=false;
+
+        // context-defined break test
+        if (c=='\n') {
+          if (context==ContextType.HEADER || context==ContextType.TABLE_CELL) {
+            p++;
+            throw new EndOfContextException(p);
+          }
+          if (p+1<end && wikiChars[p+1]=='\n') { // blank line delimits everything
+            p++; // eat one '\n' and leave another one unparsed so parseBlock() can close all lists
+            throw new EndOfContextException(p);
+          }
+          for (p++; p<end && wikiChars[p]<=' ' && wikiChars[p]!='\n'; p++) ; // skip whitespace
+          if (p>=end) throw new EndOfContextException(p); // end of text reached
+
+          c=wikiChars[p];
+          atLineStart=true;
+
+          if (c=='-' && wikiText.startsWith("----", p)) { // check for ---- <hr>
+            int pp;
+            for (pp=p+4; pp<end && (wikiChars[pp]==' ' || wikiChars[pp]=='\t'); pp++) ; // skip spaces
+            if (pp==end || wikiChars[pp]=='\n') throw new EndOfContextException(p); // yes, it's <hr>
+          }
+
+          if (LIST_CHARS.indexOf(c)>=0) { // start of list item?
+            if (FORMAT_CHARS.indexOf(c)<0) throw new EndOfContextException(p);
+            // here we have a list char, which also happen to be a format char
+            if (p+1<end && wikiChars[p+1]!=c) throw new EndOfContextException(p); // format chars go in pairs
+            if (/*context==ContextType.LIST_ITEM*/ listLevel>=0 && c==listLevels[0]) {
+              // c matches current list's first level, so it must be new list item
+              throw new EndOfContextException(p);
+            }
+            // otherwise it must be just formatting sequence => no break of context
+          }
+          else if (c=='=') { // header
+            throw new EndOfContextException(p);
+          }
+          else if (c=='|') { // table or mediawiki-table
+            throw new EndOfContextException(p);
+          }
+          else if (c=='{') { // mediawiki-table?
+            if (p+1<end && wikiChars[p+1]=='|') {
+              int pp;
+              for (pp=p+2; pp<end && (wikiChars[pp]==' ' || wikiChars[pp]=='\t'); pp++) ; // skip spaces
+              if (pp==end || wikiChars[pp]=='\n') throw new EndOfContextException(p); // yes, it's start of a table
+            }
+          }
+
+          // if none matched add '\n' to text buffer
+          tb.append('\n');
+          // p and c already shifted past the '\n' and whitespace after, so go on
+        }
+        else if (c=='|') {
+          if (context==ContextType.TABLE_CELL) {
+            p++;
+            throw new EndOfSubContextException(p);
+          }
+        }
+
+        int formatType;
+
+        if (c=='{') {
+          if (p+1<end && wikiChars[p+1]=='{') {
+            if (p+2<end && wikiChars[p+2]=='{') { // inline or block <nowiki>
+              appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+              int startNowiki=p+3;
+              int endNowiki=findEndOfNowiki(startNowiki);
+              p=endNowiki+3;
+              if (wikiText.lastIndexOf('\n', endNowiki)>=startNowiki) { // block <pre>
+                if (wikiChars[startNowiki]=='\n') startNowiki++; // skip the very first '\n'
+                if (wikiChars[endNowiki-1]=='\n') endNowiki--; // omit the very last '\n'
+                if (context==ContextType.PARAGRAPH) sb.append("</p>"); // break the paragraph because XHTML does not allow <pre> children of <p>
+                sb.append("<pre>");
+                appendNowiki(wikiText.substring(startNowiki, endNowiki));
+                sb.append("</pre>\n");
+                if (context==ContextType.PARAGRAPH) sb.append("<p>"); // continue the paragraph
+                //if (context==ContextType.NOWIKI_BLOCK) return p; // in this context return immediately after nowiki
+              }
+              else { // inline <nowiki>
+                appendNowiki(wikiText.substring(startNowiki, endNowiki));
+              }
+              continue;
+            }
+            else if (p+2<end) { // {{image}}
+              int endImg=wikiText.indexOf("}}", p+2);
+              if (endImg>=0 && endImg<end) {
+                appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+                appendImage(wikiText.substring(p+2, endImg));
+                p=endImg+2;
+                continue;
+              }
+            }
+          }
+        }
+        else if (c=='[') {
+          if (p+1<end && wikiChars[p+1]=='[') { // [[link]]
+            int endLink=wikiText.indexOf("]]", p+2);
+            if (endLink>=0 && endLink<end) {
+              appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+              appendLink(wikiText.substring(p+2, endLink));
+              p=endLink+2;
+              continue;
+            }
+          }
+        }
+        else if (c=='\\') {
+          if (p+1<end && wikiChars[p+1]=='\\') { // \\ = <br/>
+            appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+            sb.append("<br/>");
+            p+=2;
+            continue;
+          }
+        }
+        else if (c=='<') {
+          if (p+1<end && wikiChars[p+1]=='<') {
+            if (p+2<end && wikiChars[p+2]=='<') { // <<<macro>>>
+              int endMacro=wikiText.indexOf(">>>", p+3);
+              if (endMacro>=0 && endMacro<end) {
+                appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+                appendMacro(wikiText.substring(p+3, endMacro));
+                p=endMacro+3;
+                continue;
+              }
+            }
+          }
+        }
+        else if ((formatType=FORMAT_CHARS.indexOf(c))>=0) {
+          if (p+1<end && wikiChars[p+1]==c) {
+            appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+            if (c=='/') { // special case for "//" - check if it is part of URL (scheme://etc)
+              int[] uriOffs=checkURI(p, start, end);
+              if (uriOffs!=null) {
+                int pb=uriOffs[0], pe=uriOffs[1];
+                if (pb>start && wikiChars[pb-1]=='~') {
+                  sb.delete(sb.length()-(p-pb+1), sb.length()); // roll back URL + tilde
+                  sb.append(escapeHTML(wikiText.substring(pb, pe)));
+                }
+                else {
+                  sb.delete(sb.length()-(p-pb), sb.length()); // roll back URL
+                  appendLink(wikiText.substring(pb, pe));
+                }
+                p=pe;
+                continue;
+              }
+            }
+            sb.append(FORMAT_TAG_OPEN[formatType]);
+            try {
+              p=parseItemThrow(p+2, FORMAT_DELIM[formatType], context);
+            }
+            finally {
+              sb.append(FORMAT_TAG_CLOSE[formatType]);
+            }
+            continue;
+          }
+        }
+        else if (c=='~') { // escape
+          // most start line escapes are dealt with in parseBlock()
+          if (atLineStart) {
+            // same as block-level escaping: '*' '-' '#' '>' ':' '|' '='
+            if (p+1<end) {
+              char nc=wikiChars[p+1];
+              if (nc=='>' || nc==':' || nc=='-' || nc=='|' || nc=='=' || nc=='!') { // can't be inline markup
+                tb.append(nc);
+                p+=2; // skip '~' and nc
+                continue nextChar;
+              }
+              else if (nc=='*' || nc=='#') { // might be inline markup so need to double check
+                char nnc=p+2<end? wikiChars[p+2]:0;
+                if (nnc!=nc) {
+                  tb.append(nc);
+                  p+=2; // skip '~' and nc
+                  continue nextChar;
+                }
+                // otherwise escaping will be done at line level
+              }
+              else if (nc=='{') { // might be inline {{{ markup so need to double check
+                char nnc=p+2<end? wikiChars[p+2]:0;
+                if (nnc=='|') { // mediawiki-table?
+                  tb.append(nc);
+                  tb.append(nnc);
+                  p+=3; // skip '~', nc and nnc
+                  continue nextChar;
+                }
+                // otherwise escaping will be done as usual at line level
+              }
+            }
+          }
+          for (String e: ESCAPED_INLINE_SEQUENCES) {
+            if (wikiText.startsWith(e, p+1)) {
+              tb.append(e);
+              p+=1+e.length();
+              continue nextChar;
+            }
+          }
+        }
+        else if (c=='-') { // ' -- ' => –
+          if (p+2<end && wikiChars[p+1]=='-' && wikiChars[p+2]==' ' && p>start && wikiChars[p-1]==' ') {
+            //appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+            //sb.append("– ");
+            tb.append("– "); // – = "\u2013 "
+            p+=3;
+            continue;
+          }
+        }
+        tb.append(c);
+        p++;
+      }
+    }
+    finally {
+      appendText(tb.toString()); tb.delete(0, tb.length()); // flush text buffer
+    }
+  }
+
+
+  protected void appendMacro(String text) {
+    if ("TOC".equals(text)) {
+      sb.append("<<<TOC>>>"); // put TOC placeholder for replacing it later with real TOC
+    }
+    else {
+      sb.append("<<<Macro:");
+      sb.append(escapeHTML(unescapeHTML(text)));
+      sb.append(">>>");
+    }
+  }
+
+  protected void appendLink(String text) {
+    String[] link=split(text, '|');
+    URI uri=null;
+    try { // validate URI
+      uri=new URI(link[0].trim());
+    }
+    catch (URISyntaxException e) {
+    }
+    if (uri!=null && uri.isAbsolute() && !uri.isOpaque()) {
+      sb.append("<a href=\""+escapeHTML(uri.toString())+"\" rel=\"nofollow\">");
+      sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
+      sb.append("</a>");
+    }
+    else {
+      sb.append("<a href=\"#\" title=\"Internal link\">");
+      sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
+      sb.append("</a>");
+    }
+  }
+
+  protected void appendImage(String text) {
+    String[] link=split(text, '|');
+    URI uri=null;
+    try { // validate URI
+      uri=new URI(link[0].trim());
+    }
+    catch (URISyntaxException e) {
+    }
+    if (uri!=null && uri.isAbsolute() && !uri.isOpaque()) {
+      String alt=escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0]));
+      sb.append("<img src=\""+escapeHTML(uri.toString())+"\" alt=\""+alt+"\" title=\""+alt+"\" />");
+    }
+    else {
+      sb.append("<<<Internal image(?): ");
+      sb.append(escapeHTML(unescapeHTML(text)));
+      sb.append(">>>");
+    }
+  }
+
+  protected void appendText(String text) {
+    sb.append(escapeHTML(unescapeHTML(text)));
+  }
+
+  protected String generateTOCAnchorId(int hLevel, String text) {
+    int i=0;
+    String id=(HEADING_ID_PREFIX!=null?HEADING_ID_PREFIX:"H"+hLevel+"_")+translit(text.replaceAll("<.+?>", "")).trim().replaceAll("\\s+", "_").replaceAll("[^a-zA-Z0-9_-]", "");
+    while (tocAnchorIds.contains(id)) { // avoid duplicates
+      i++;
+      id=text+"_"+i;
+    }
+    tocAnchorIds.add(id);
+    return id;
+  }
+
+  protected void appendTOCItem(int level, String anchorId, String text) {
+    if (level>tocLevel) {
+      while (level>tocLevel) {
+        toc.append("<ul><li>");
+        tocLevel++;
+      }
+    }
+    else {
+      while (level<tocLevel) {
+        toc.append("</li></ul>");
+        tocLevel--;
+      }
+      toc.append("</li>\n<li>");
+    }
+    toc.append("<a href='#"+anchorId+"'>"+text+"</a>");
+  }
+
+  protected void completeTOC() {
+    while (0<tocLevel) {
+      toc.append("</li></ul>");
+      tocLevel--;
+    }
+    int idx;
+    String tocDiv="<div class='toc'>"+toc.toString()+"</div>";
+    while ((idx=sb.indexOf("<<<TOC>>>"))>=0) {
+      sb.replace(idx, idx+9, tocDiv);
+    }
+  }
+
+  protected void appendNowiki(String text) {
+    sb.append(escapeHTML(replaceString(replaceString(text, "~{{{", "{{{"), "~}}}", "}}}")));
+  }
+
+  private static class EndOfContextException extends Exception {
+    private static final long serialVersionUID=1L;
+    int position;
+    public EndOfContextException(int position) {
+      super();
+      this.position=position;
+    }
+  }
+
+  private static class EndOfSubContextException extends EndOfContextException {
+    private static final long serialVersionUID=1L;
+    public EndOfSubContextException(int position) {
+      super(position);
+    }
+  }
+}
diff --git a/build.xml b/build.xml
new file mode 100644
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,74 @@
+<project>
+  <property name="src" value="./src" />
+  <property name="alien.src" value="./alien/src" />
+  <property name="alien.libs" value="./alien/libs" />
+  <property name="plugin.src" value="./plugin/src" />
+  <property name="classes" value="./build/classes" />
+  <property name="jars" value="./build/jar" />
+
+  <target name="clean">
+    <delete dir="${jars}"/>
+    <delete dir="${classes}"/>
+  </target>
+
+  <!--Build external dependencies from source, but keep them separate. -->
+  <target name="compile.alien.src">
+    <mkdir dir="${classes}"/>
+    <javac srcdir="${alien.src}" destdir="${classes}" debug="true">
+    <compilerarg line="-encoding utf8"/>
+    </javac>
+  </target>
+
+  <target name="compile" depends="compile.alien.src">
+    <mkdir dir="${classes}"/>
+    <javac srcdir="${src}" destdir="${classes}" debug="true">
+    <compilerarg line="-encoding utf8"/>
+    </javac>
+  </target>
+
+  <target name="compile.plugin.src" depends="compile">
+    <mkdir dir="${alien.libs}"/>
+    <fail message="No freenet.jar! Copy freenet.jar into: ${alien.libs}">
+      <condition>
+        <not>
+          <resourcecount count="1">
+            <fileset id="fs" dir="${alien.libs}" includes="freenet.jar"/>
+          </resourcecount>
+        </not>
+      </condition>
+    </fail>
+
+    <mkdir dir="${classes}"/>
+    <javac srcdir="${plugin.src}" destdir="${classes}" debug="true">
+      <compilerarg line="-encoding utf8"/>
+      <classpath>
+        <pathelement location="${alien.libs}/freenet.jar"/>
+      </classpath>
+    </javac>
+  </target>
+
+  <target name="jar" depends="compile">
+    <mkdir dir="${jars}"/>
+    <jar destfile="${jars}/jfniki.jar" basedir="${classes}">
+      <manifest>
+        <attribute name="Main-Class" value="fniki.standalone.ServeHttp"/>
+      </manifest>
+    </jar>
+  </target>
+
+  <!-- jfniki-plugin.jar contains all the standalone code too. -->
+  <target name="plugin" depends="compile.plugin.src">
+    <mkdir dir="${jars}"/>
+    <jar destfile="${jars}/jfniki-plugin.jar" basedir="${classes}">
+      <manifest>
+        <attribute name="Main-Class" value="fniki.standalone.ServeHttp"/>
+        <attribute name="Plugin-Main-Class" value="fniki.plugin.Fniki"/>
+      </manifest>
+    </jar>
+  </target>
+
+  <target name="run" depends="jar">
+    <java jar="${jars}/jfniki.jar" fork="true"/>
+  </target>
+
+</project>
diff --git a/plugin/src/fniki/plugin/Fniki.java b/plugin/src/fniki/plugin/Fniki.java
new file mode 100644
--- /dev/null
+++ b/plugin/src/fniki/plugin/Fniki.java
@@ -0,0 +1,252 @@
+/* Freenet Plugin to run WikiApp.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.plugin;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import freenet.pluginmanager.AccessDeniedPluginHTTPException;
+import freenet.pluginmanager.FredPlugin;
+import freenet.pluginmanager.FredPluginHTTP;
+import freenet.pluginmanager.FredPluginThreadless;
+import freenet.pluginmanager.NotFoundPluginHTTPException;
+import freenet.pluginmanager.PluginHTTPException;
+import freenet.pluginmanager.PluginRespirator;
+import freenet.pluginmanager.RedirectPluginHTTPException;
+import freenet.support.api.HTTPRequest;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.Query;
+import fniki.wiki.Request;
+import fniki.wiki.WikiApp;
+
+import fniki.wiki.AccessDeniedException;
+import fniki.wiki.NotFoundException;
+import fniki.wiki.RedirectException;
+import fniki.wiki.ChildContainerException;
+
+public class Fniki implements FredPlugin, FredPluginHTTP, FredPluginThreadless {
+    private WikiApp mWikiApp;
+
+    public void terminate() {
+        System.err.println("terminating...");
+    }
+
+    public void runPlugin(PluginRespirator pr) {
+        try {
+            ArchiveManager archiveManager = new ArchiveManager();
+
+            // DCI: Parameter handling?
+            archiveManager.setFcpHost("127.0.0.1");
+            archiveManager.setFcpPort(9481);
+
+            archiveManager.setFmsHost("127.0.0.1");
+            archiveManager.setFmsPort(1119);
+
+            // YOU MUST SET THESE OR THE PLUGIN WON'T LOAD.
+            archiveManager.setPrivateSSK("FMS_PRIVATE_SSK");
+            archiveManager.setFmsId("FMS_ID");
+
+            archiveManager.setFmsGroup("biss.test000");
+            archiveManager.setBissName("testwiki");
+
+            String fproxyPrefix = "http://127.0.0.1:8888/";
+            boolean enableImages = true;
+
+            archiveManager.createEmptyArchive();
+
+            WikiApp wikiApp = new WikiApp(archiveManager);
+            final String containerPrefix = wikiApp.getString("container_prefix", null);
+            if (containerPrefix == null) {
+                throw new RuntimeException("Assertion Failure: container_prefix not set!");
+            }
+            wikiApp.setFproxyPrefix(fproxyPrefix);
+            wikiApp.setAllowImages(enableImages);
+
+            // IMPORTANT:
+            // HTTP POSTS will be rejected without any useful error message if your form
+            // doesn't contain a hidden field with the freenet per boot form password.
+            wikiApp.setFormPassword(pr.getNode().clientCore.formPassword);
+
+            // I couldn't get application/x-www-form-urlencoded forms to work.
+            wikiApp.setUseMultiPartForms(true);
+
+            mWikiApp = wikiApp;
+
+        } catch (IOException ioe) {
+            System.err.println("EPIC FAIL!");
+            ioe.printStackTrace();
+        }
+    }
+
+    private static class ServerPluginHTTPException extends PluginHTTPException {
+	private static final long serialVersionUID = -1;
+
+	public static final short code = 500; // Bad Request
+	public ServerPluginHTTPException(String errorMessage, String location) {
+            super(errorMessage, location);
+	}
+    }
+
+    private static class PluginQuery implements Query {
+        private final HTTPRequest mParent;
+        private final String mTitle;
+        private final String mAction;
+        private final String mSaveText;
+        private final String mSavePage;
+
+        PluginQuery(HTTPRequest parent, String path) {
+            mParent = parent;
+
+            String title = path;
+            if (parent.isParameterSet("title")) {
+                title = parent.getParam("title");
+            }
+            mTitle = title;
+
+            // DCI: validate title here
+
+            String action = "view";
+            if (parent.isParameterSet("action")) {
+                action = parent.getParam("action");
+            }
+            mAction = action;
+
+            // Handle multipart form parameters.
+            System.err.println("Dumping list of parts...");
+            String saveText = "";
+            String savePage = "";
+            try {
+                for (String part : parent.getParts()) {
+                    if (part.equals("savetext")) {
+                        // DCI: magic numbers
+                        saveText = new String(parent.getPartAsBytesFailsafe(part, 64 * 1024), "utf-8");
+                        continue;
+                    }
+                    if (part.equals("savepage")) {
+                        savePage = new String(parent.getPartAsBytesFailsafe(part, 64 * 1024), "utf-8");
+                    }
+                }
+            } catch (UnsupportedEncodingException ue) {
+                // Shouldn't happen.
+                ue.printStackTrace();
+            }
+            mSaveText = saveText;
+            mSavePage = savePage;
+
+            parent.freeParts(); // DCI: test!, put in finally?
+
+        }
+
+        public boolean containsKey(String paramName) {
+            if (paramName.equals("title") || paramName.equals("action") ||
+                paramName.equals("savetext") || paramName.equals("savepage")) {
+                return true;
+            }
+            return mParent.isParameterSet(paramName);
+        }
+
+        public String get(String paramName) {
+            if (paramName.equals("title")) {
+                return mTitle;
+            }
+            if (paramName.equals("action")) {
+                return mAction;
+            }
+            if (paramName.equals("savetext")) {
+                return mSaveText;
+            }
+            if (paramName.equals("savepage")) {
+                return mSavePage;
+            }
+            if (!containsKey(paramName)) {
+                return null;
+            }
+            return mParent.getParam(paramName);
+        }
+    }
+
+    private static class PluginRequest implements Request {
+        private final Query mQuery;
+        private final String mPath;
+        PluginRequest(HTTPRequest parent, String containerPrefix) { // DCI throws IOException {
+            for (String key : parent.getParameterNames()) {
+                String value = parent.getParam(key);
+                if (value.length() > 128) {
+                    value = value.substring(0, 128) + "...";
+                }
+                System.err.println(String.format("[%s] => [%s]", key, value));
+            }
+
+            String path = parent.getPath();
+            if (!path.startsWith(containerPrefix)) {
+                // This should be impossible because of the way plugin requests are routed.
+                throw new RuntimeException("Request doesn't start with: " + containerPrefix);
+            }
+
+            System.err.println("Raw path: " + path);
+            path = path.substring(containerPrefix.length());
+
+            while(path.startsWith("/")) {
+                path = path.substring(1).trim();
+            }
+            mQuery = new PluginQuery(parent, path);
+            mPath = path;
+        }
+
+        public String getPath() { return mPath; }
+        public Query getQuery() { return mQuery; }
+    }
+
+    public String handle(HTTPRequest request) throws PluginHTTPException {
+        // DCI: cleanup container_prefix usage
+
+        try {
+            mWikiApp.setRequest(new PluginRequest(request, mWikiApp.getString("container_prefix", null)));
+            return mWikiApp.handle(mWikiApp);
+        } catch(AccessDeniedException accessDenied) {
+            throw new AccessDeniedPluginHTTPException(accessDenied.getMessage(),
+                                                      mWikiApp.getString("container_prefix", null));
+        } catch(NotFoundException notFound) {
+            throw new NotFoundPluginHTTPException(notFound.getMessage(),
+                                                  mWikiApp.getString("container_prefix", null));
+        } catch(RedirectException redirected) {
+            throw new RedirectPluginHTTPException(redirected.getMessage(),
+                                              redirected.getLocation());
+        } catch(ChildContainerException serverError) {
+            throw new ServerPluginHTTPException(serverError.getMessage(),
+                                                mWikiApp.getString("container_prefix", null));
+        }
+    }
+
+    public String handleHTTPGet(HTTPRequest request) throws PluginHTTPException {
+        return handle(request);
+    }
+
+    public String handleHTTPPost(HTTPRequest request) throws PluginHTTPException {
+        return handle(request);
+    }
+}
+
diff --git a/readme.txt b/readme.txt
new file mode 100644
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,38 @@
+20110122
+djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+
+WARNING:
+THIS IS RAW ALPHA CODE.
+
+I'm releasing this so that other developers in the Freenet community
+can audit the source code.
+
+DON'T USE IT if violation of your anonymity would put you at risk.
+
+ABOUT:
+* jfniki is an experimental serverless wiki implementation which runs over Freenet / FMS.
+* It is written in Java and has no external build dependencies.
+* jfniki is INCOMPATIBLE with the existing server based python fniki implementation.
+
+REQUIREMENTS:
+ant
+java 1.5 or better
+Access to a running Freenet Node and FMS daemon.
+
+BUILD:
+ant jar
+
+RUN:
+Edit the script/jfniki.sh to set PRIVATE_FMS_SSK and FMS_ID correctly and comment out the warning lines.
+
+./script/jfniki.sh
+
+Look at http://127.0.0.1:8083 with your web browser.
+
+BUILD FREENET PLUGIN:
+Manually edit plugin/src/fniki/plugin/Fniki.java to include your FMS_ID and PRIVATE_FMS_SSK.
+
+ant plugin
+load the jar file from ./build/jar/jfniki-plugin.jar
+
+
diff --git a/script/jfniki.sh b/script/jfniki.sh
new file mode 100755
--- /dev/null
+++ b/script/jfniki.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env sh
+
+# TIP:
+# You should be able to copy this script anywhere you want and make a symlink
+# to the jfniki.jar file in your build directory, in the directory you copy it to.
+
+# TIP:
+# Look in the XML file generate by FMS when you export and identity on
+# the "Local Identities"  page to find these values.
+
+# MUST set this to post. i.e. you can run read only without it if you want.
+# The <PrivateKey> value for the FMS identity you want post wiki submissions with.
+export set PRIVATE_FMS_SSK="SSK@XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,AQECAAE/"
+
+# MUST set this to read new version from FMS. i.e. Don't try to run with setting it!
+# The correponding <name> value for that private key.
+export set FMS_ID="YOUR_FMS_HERE"
+
+# FAIL in an obvious way until properly configured.
+echo "MANUAL CONFIGURATION REQUIRED!"
+echo "Edit PRIVATE_FMS_SSK and FMS_ID in this script, then comment out these 3 lines."
+exit -1
+
+export set ENABLE_IMAGES=1
+export set LISTEN_PORT=8083
+
+export set JAR_NAME="jfniki.jar"
+# Look for the jfniki.jar file in the build dir.
+# If you want to move it to somewhere else, modify the line below.
+#export set JAR_PATH="${0%%/*}/../build/jar"
+export set SCRIPT_DIR=`dirname $0`
+export set JAR_PATH="${SCRIPT_DIR}/../build/jar"
+export set JAR_FILE="${JAR_PATH}/${JAR_NAME}"
+
+if [ ! -f ${JAR_FILE} ];
+then
+    export set JAR_PATH=${SCRIPT_DIR}
+    export set JAR_FILE="${JAR_PATH}/${JAR_NAME}"
+    if [ ! -f ${JAR_FILE} ];
+    then
+        echo "Looked in:"
+        echo "${SCRIPT_DIR}/../build/jar/${JAR_NAME}"
+        echo "and"
+        echo "${JAR_FILE}"
+        echo
+        echo "but still can't find the jar file!"
+        echo "Maybe run: ant jar?"
+        exit -1
+    fi
+fi
+
+echo "Using jar file: ${JAR_FILE}"
+echo
+
+# FCP configuration
+export set FCP_HOST="127.0.0.1"
+export set FCP_PORT=9481
+
+# FMS configuration
+export set FMS_HOST="127.0.0.1"
+export set FMS_PORT=1119
+
+export set FMS_GROUP="biss.test000"
+export set WIKI_NAME="testwiki"
+
+# fproxy configuration.
+export set FPROXY_PREFIX="http://127.0.0.1:8888/"
+
+export set JAVA_CMD="java"
+
+${JAVA_CMD} -jar ${JAR_FILE} \
+    ${LISTEN_PORT} \
+    ${FCP_HOST} \
+    ${FCP_PORT} \
+    ${FMS_HOST} \
+    ${FMS_PORT} \
+    ${PRIVATE_FMS_SSK} \
+    "${FMS_ID}" \
+    ${FMS_GROUP} \
+    ${WIKI_NAME} \
+    ${FPROXY_PREFIX} \
+    ${ENABLE_IMAGES} \
+    $1
diff --git a/src/fniki/standalone/FnikiContextHandler.java b/src/fniki/standalone/FnikiContextHandler.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/standalone/FnikiContextHandler.java
@@ -0,0 +1,164 @@
+/* Glue code to run WikiApp to run from within HTTPServer.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.standalone;
+
+import java.io.IOException;
+import net.freeutils.httpserver.HTTPServer;
+
+import fniki.wiki.Query;
+import fniki.wiki.Request;
+import fniki.wiki.WikiApp;
+
+import fniki.wiki.AccessDeniedException;
+import fniki.wiki.NotFoundException;
+import fniki.wiki.RedirectException;
+import fniki.wiki.ChildContainerException;
+
+// Adapter class to run WikiApp from HTTPServer
+public class FnikiContextHandler implements HTTPServer.ContextHandler {
+    private final WikiApp mApp;
+    private final String mContainerPrefix;
+
+    private static class WikiQuery implements Query {
+        private final HTTPServer.Request mParent;
+
+        WikiQuery(HTTPServer.Request parent) {
+            mParent = parent;
+        }
+
+        public boolean containsKey(String paramName) {
+            try {
+                return mParent.getParams().containsKey(paramName);
+            } catch (IOException ioe) {
+                return false;
+            }
+        }
+
+        public String get(String paramName) {
+            try {
+                return mParent.getParams().get(paramName);
+            } catch (IOException ioe) {
+                return null;
+            }
+        }
+    }
+
+    private static class WikiRequest implements Request {
+        private final Query mQuery;
+        private final String mPath;
+
+        WikiRequest(HTTPServer.Request parent, String containerPrefix) throws IOException {
+            mQuery = new WikiQuery(parent);
+            for (String key : parent.getParams().keySet()) {
+                String value = parent.getParams().get(key);
+                if (value.length() > 128) {
+                    value = value.substring(0, 128) + "...";
+                }
+                System.err.println(String.format("[%s] => [%s]", key, value));
+            }
+
+            String path = parent.getPath();
+            if (!path.startsWith(containerPrefix)) {
+                // This should be impossible because of the way HTTPServer routes requests.
+                throw new RuntimeException("Request doesn't start with: " + containerPrefix);
+            }
+
+            System.err.println("Raw path: " + path);
+
+            path = path.substring(containerPrefix.length());
+            while(path.startsWith("/")) {
+                path = path.substring(1).trim();
+            }
+
+            // DCI: not sure that this stuff belongs here.
+            String title = path;
+            if (mQuery.containsKey("title")) {
+                title = mQuery.get("title");
+            } else {
+                parent.getParams().put("title", title);
+            }
+
+            // DCI: validate title here
+
+            String action = "view";
+            if (mQuery.containsKey("action")) {
+                action = mQuery.get("action");
+            } else {
+                parent.getParams().put("action", action);
+            }
+            mPath = path;
+
+
+
+        }
+
+        public String getPath() { return mPath; }
+        public Query getQuery() { return mQuery; }
+    }
+
+    public FnikiContextHandler(WikiApp app) {
+        mApp = app;
+        mContainerPrefix = mApp.getString("container_prefix", null);
+        if (mContainerPrefix == null) {
+            throw new IllegalArgumentException("mContainerPrefix == null");
+        }
+    }
+
+    /**
+     * Serves the given request using the given response.
+     * 
+     * @param req the request to be served
+     * @param resp the response to be filled
+     * @return an HTTP status code, which will be used in returning
+     *         a default response appropriate for this status. If this
+     *         method invocation already sent anything in the response
+     *         (headers or content), it must return 0, and no further
+     *         processing will be done
+     * @throws IOException if an IO error occurs
+     */
+    public int serve(HTTPServer.Request req, HTTPServer.Response resp) throws IOException {
+        synchronized (mApp) { // Only allow one thread to touch app at a time.
+            mApp.setRequest(new WikiRequest(req, mContainerPrefix));
+            try {
+                String html = mApp.handle(mApp);
+                resp.send(200, html);
+                return 0;
+            } catch(AccessDeniedException accessDenied) {
+                resp.sendError(403, accessDenied.getMessage());
+                return 0;
+            } catch(NotFoundException notFound) {
+                resp.sendError(404, notFound.getMessage());
+                return 0;
+            } catch(RedirectException redirected) {
+                resp.redirect(redirected.getLocation(), false);
+                return 0;
+            } catch(ChildContainerException serverError) {
+                // This also handles ServerErrorException.
+                resp.sendError(500, serverError.getMessage());
+                return 0;
+            }
+        }
+    }
+}
diff --git a/src/fniki/standalone/ServeHttp.java b/src/fniki/standalone/ServeHttp.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/standalone/ServeHttp.java
@@ -0,0 +1,121 @@
+/* Stand alone client to serve WikiApp on localhost.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+package fniki.standalone;
+
+import java.io.IOException;
+
+import net.freeutils.httpserver.HTTPServer;
+
+import fniki.wiki.WikiApp;
+import fniki.wiki.ArchiveManager;
+
+public class ServeHttp {
+    private final static int DEFAULT_PORT = 8080;
+
+    private final static String HELP_TEXT =
+        "ServeHttp: Experimental distributed anonymous wiki over Freenet + FMS\n" +
+        "written as part of the fniki Freenet Wiki project\n" +
+        "Copyright (C) 2010, 2011 Darrell Karbott, GPL2 (or later)\n\n" +
+        "SUMMARY:\n" +
+        "Launch a wiki viewer / editor on localhost.\n" +
+        "This is experimental code. Use it at your own peril.\n\n" +
+        "USAGE:\n" +
+        "java -jar jfniki.jar <listen_port> <fcp_host> <fcp_port> <fms_host> <fms_port> " +
+        "<private_fms_ssk> <fms_id> <fms_group> <wiki_name> <fproxy_prefix> <enable_images> [uri]\n\n";
+
+
+    private final static String ARG_NAMES[] = new String[] {
+        "<listen_port>", "<fcp_host>", "<fcp_port>", "<fms_host>","<fms_port>",
+        "<private_fms_ssk>", "<fms_id>", "<fms_group>", "<wiki_name>",
+        "<fproxy_prefix>", "<enable_images>", "[uri]", };
+
+    public static void debugDumpArgs(String[] args) {
+        for (int index = 0; index < args.length; index++) {
+            String name = "???";
+            if (index < ARG_NAMES.length) {
+                name = ARG_NAMES[index];
+            }
+            System.out.println(String.format("[%d]:{%s}[%s]", index, name, args[index]));
+        }
+    }
+    public static int asInt(String value) { return Integer.parseInt(value); }
+    public static void main(String[] args) throws Exception {
+        if (args.length < 11) {
+            System.err.println(HELP_TEXT);
+            System.exit(-1);
+        }
+        debugDumpArgs(args);
+        int listenPort = Integer.parseInt(args[0]);
+
+        ArchiveManager archiveManager = new ArchiveManager();
+
+        archiveManager.setFcpHost(args[1]);
+        archiveManager.setFcpPort(asInt(args[2]));
+
+        archiveManager.setFmsHost(args[3]);
+        archiveManager.setFmsPort(asInt(args[4]));
+
+        archiveManager.setPrivateSSK(args[5]);
+        archiveManager.setFmsId(args[6]);
+
+        archiveManager.setFmsGroup(args[7]);
+        archiveManager.setBissName(args[8]);
+
+        String fproxyPrefix = args[9];
+        boolean enableImages = (args[10].equals("1") || args[10].toLowerCase().equals("true")) ? true : false;
+
+        if (args.length > 11) {
+            archiveManager.load(args[11]);
+        } else {
+            archiveManager.createEmptyArchive();
+        }
+
+        WikiApp wikiApp = new WikiApp(archiveManager);
+        final String containerPrefix = wikiApp.getString("container_prefix", null);
+        if (containerPrefix == null) {
+            throw new RuntimeException("Assertion Failure: container_prefix not set!");
+        }
+        wikiApp.setFproxyPrefix(fproxyPrefix);
+        wikiApp.setAllowImages(enableImages);
+
+        // Redirect non-routed requests to the wiki app.
+        HTTPServer.ContextHandler defaultRedirect = new HTTPServer.ContextHandler() {
+                public int serve(HTTPServer.Request req, HTTPServer.Response resp) throws IOException {
+                    resp.redirect(containerPrefix, false);
+                    return 0;
+                }
+            };
+
+        HTTPServer server = new HTTPServer(listenPort);
+        HTTPServer.VirtualHost host = server.getVirtualHost(null); // default host
+        host.setAllowGeneratedIndex(false);
+        host.setDirectoryIndex("/"); // Keep from sending default "index.html"
+        host.addContext(containerPrefix, new FnikiContextHandler(wikiApp));
+        host.addContext("/", defaultRedirect);
+        server.start();
+
+        System.err.println(String.format("Serving wiki on: http://127.0.0.1:%d%s",
+                                         listenPort, containerPrefix));
+    }
+}
diff --git a/src/fniki/wiki/AccessDeniedException.java b/src/fniki/wiki/AccessDeniedException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/AccessDeniedException.java
@@ -0,0 +1,33 @@
+/* Exception raised for HTTP 403 errors.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+// 403
+public class AccessDeniedException extends ChildContainerException {
+    public AccessDeniedException(String msg) {
+        super(msg);
+    }
+}
+
diff --git a/src/fniki/wiki/ArchiveManager.java b/src/fniki/wiki/ArchiveManager.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ArchiveManager.java
@@ -0,0 +1,258 @@
+/* Class to manage reading from and writing Freenet WORM Archives.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import fmsutil.FMSUtil;
+import wormarc.Archive;
+import wormarc.AuditArchive;
+import wormarc.ExternalRefs;
+import wormarc.FileManifest;
+import wormarc.IOUtil;
+import wormarc.LinkDigest;
+import wormarc.RootObjectKind;
+import wormarc.io.FreenetIO;
+
+public class ArchiveManager {
+    private final static String FCP_HOST = "127.0.0.1";
+    private final static int FCP_PORT = 9481;
+
+    private final static String FMS_HOST = "127.0.0.1";
+    private final static int FMS_PORT = 1119;
+    private final static String FMS_GROUP = "biss.test000";
+    private final static String BISS_NAME = "jfniki";
+    // Maximum number of versions to read from FMS.
+    private final static int MAX_VERSIONS = 50;
+
+    String mFcpHost = FCP_HOST;
+    int mFcpPort = FCP_PORT;
+    String mFmsHost = FMS_HOST;
+    int mFmsPort = FMS_PORT;
+    String mFmsGroup = FMS_GROUP;
+    String mBissName= BISS_NAME;
+
+    String mPrivateSSK;
+    String mFmsId;
+
+    // Base64 SSK public key hash to FMS name. i.e. the part before '@'.
+    Map<String, String> mNymLut = new HashMap<String, String>();
+
+    String mParentUri;
+    Archive mArchive;
+    FileManifest mFileManifest;
+    LocalWikiChanges mOverlay;
+
+    public void setDebugOutput(PrintStream out) {
+        FreenetIO.setDebugOutput(out);
+    }
+
+    public void setPrivateSSK(String value) {
+        if (!value.startsWith("SSK@") || !value.endsWith(",AQECAAE/")) {
+            throw new IllegalArgumentException("That doesn't look like a private SSK. " +
+                                           "Did you forget the trailing '/'?");
+        }
+        mPrivateSSK = value;
+    }
+
+    public String getPrivateSSK() { return mPrivateSSK; }
+    public String getParentUri() { return mParentUri; }
+
+    public void setFmsId(String value) {
+        if (value.indexOf("@") != -1) {
+            throw new IllegalArgumentException("FMS Id Should only include the part before the '@'!");
+        }
+
+        mFmsId = value;
+    }
+
+    public void setFcpHost(String value) {mFcpHost = value; }
+    public void setFcpPort(int value) {mFcpPort = value; }
+    public void setFmsHost(String value) { mFmsHost = value; }
+    public void setFmsPort(int value) { mFmsPort = value; }
+    public void setFmsGroup(String value) { mFmsGroup = value; }
+    public void setBissName(String value) { mBissName= value; }
+
+    // DCI: Fix this to roll back state on exceptions.
+    public void load(String uri) throws IOException {
+        FreenetIO io = new FreenetIO(mFcpHost, mFcpPort);
+        io.setRequestUri(uri);
+        mArchive = Archive.load(io);
+        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
+        mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
+        mParentUri = uri;
+    }
+
+    public void createEmptyArchive() throws IOException {
+        mArchive = new Archive();
+        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
+        mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
+        mParentUri = null;
+    }
+
+    public static class UpToDateException extends IOException {
+        public UpToDateException() {
+            super("There are no local changes to submit.");
+        }
+    }
+
+    private String getInsertUri(Archive archive) throws IOException {
+        // Generate a unique SSK.
+        LinkDigest digest = archive.getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
+        // The hash of the actual file, not just the chain head SHA.
+        LinkDigest fileHash = IOUtil.getFileDigest(archive.getFile(digest));
+        return mPrivateSSK + fileHash.hexDigest(8);
+    }
+
+    // DCI: commitAndPushToFreenet() ?
+    public String pushToFreenet(PrintStream out) throws IOException {
+        FileManifest.Changes changes = mFileManifest.diffTo(mArchive, mOverlay);
+        if (changes.isUnmodified()) {
+            throw new IOException("Didn't find any local changes to submit.");
+        }
+
+        // Copy so that we can cleanly role back if an exception is raised.
+        Archive copy = mArchive.deepCopy();
+        FileManifest files = FileManifest.fromArchiveRootObject(copy);
+
+        // Commit local changes to the Archive.
+        copy.unsetRootObject(RootObjectKind.PARENT_REFERENCES);
+
+        // Update the archive
+        copy.startUpdate();
+        files.updateFrom(copy, mOverlay);
+
+        LinkDigest digest = copy.updateRootObject(files.toBytes(), RootObjectKind.FILE_MANIFEST);
+
+        if (mParentUri != null) {
+            out.println("Set PARENT_REFERENCES: " + mParentUri);
+            List<String> keys = Arrays.asList(mParentUri);
+            LinkDigest refs =
+                copy.updateRootObject(ExternalRefs.create(keys, ExternalRefs.KIND_FREENET)
+                                      .toBytes(),
+                                      RootObjectKind.PARENT_REFERENCES);
+        }
+
+        copy.commitUpdate();
+        copy.compressAndUpdateArchiveManifest();
+
+        // Generate a unique SSK based on the SHA hash of the archive manifest.
+        String insertUri = getInsertUri(copy);
+
+        out.println("Insert URI: " + insertUri);
+
+        // Push the updated version into Freenet.
+        FreenetIO io = new FreenetIO(mFcpHost, mFcpPort);
+        io.setInsertUri(insertUri);
+        out.println("Trying to read previous top key if possible...");
+        io.maybeLoadPreviousTopKey(copy);
+        out.println("Writing to Freenet...");
+        copy.write(io);
+
+        if (mFmsId != null) {
+            try {
+                out.println("Sending FMS update notification to: " + mFmsGroup);
+                FMSUtil.sendBISSMsg(mFmsHost, mFmsPort, mFmsId, mFmsGroup,
+                                    mBissName, io.getRequestUri());
+            } catch (IOException ioe) {
+                out.println("FMS send failed: " + ioe.getMessage());
+            }
+        }
+
+        // Don't update any state until all calls which could raise have finished.
+        mArchive = copy;
+        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
+        mOverlay = new LocalWikiChanges(mArchive, mFileManifest);
+        mParentUri = io.getRequestUri();
+        out.println("Request URI: " + mParentUri);
+
+        return mParentUri;
+    }
+
+    public FileManifest.Changes getLocalChanges() throws IOException {
+        return mFileManifest.diffTo(mArchive, mOverlay);
+    }
+
+    public void readChangeLog(PrintStream out,
+                              AuditArchive.ChangeLogCallback callback) throws IOException {
+
+        if (mParentUri == null) {
+            throw new IOException("URI not set!");
+        }
+
+        ExternalRefs.Reference head =
+            new ExternalRefs.Reference(ExternalRefs.KIND_FREENET, mParentUri);
+
+        FreenetIO freenetResolver = new FreenetIO(mFcpHost, mFcpPort);
+        Archive archive = freenetResolver.resolve(head);
+        AuditArchive.getManifestChangeLog(head, archive, freenetResolver, callback);
+    }
+
+    public List<FMSUtil.BISSRecord> getRecentWikiVersions(PrintStream out) throws IOException {
+        List<FMSUtil.BISSRecord> records =
+            FMSUtil.getBISSRecords(mFmsHost, mFmsPort, mFmsId, mFmsGroup, mBissName, MAX_VERSIONS);
+
+        // LATER: do better.
+        for (FMSUtil.BISSRecord record : records) {
+            String fields[] = record.mFmsId.split("@");
+            if (fields.length != 2) {
+                continue;
+            }
+            mNymLut.put(fields[1].trim(), fields[0].trim());
+        }
+
+        return records;
+    }
+
+    public WikiTextStorage getStorage() throws IOException {
+        if (mOverlay == null) {
+            throw new IllegalStateException("No archive loaded!");
+        }
+        return mOverlay;
+    }
+
+    public String getNym(String sskRequestUri) {
+        int start = sskRequestUri.indexOf("@");
+        int end = sskRequestUri.indexOf(",");
+        if (start == -1 || end == -1 || start >= end) {
+            return "???";
+        }
+
+        String publicKeyHash = sskRequestUri.substring(start + 1, end - start + 3);
+
+        // SSK@THIS_PART,
+        String nym = mNymLut.get(publicKeyHash);
+        if (nym == null) {
+            return "???";
+        }
+        return nym;
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/ChildContainer.java b/src/fniki/wiki/ChildContainer.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ChildContainer.java
@@ -0,0 +1,30 @@
+/* Interface for UI subcomponents of the WikiApp.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+package fniki.wiki;
+
+public interface ChildContainer {
+    // Should return well formed html or raise on 302, 403, 500
+    String handle(WikiContext context) throws ChildContainerException;
+}
+
diff --git a/src/fniki/wiki/ChildContainerException.java b/src/fniki/wiki/ChildContainerException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ChildContainerException.java
@@ -0,0 +1,32 @@
+/* Base class for exceptions thrown from ChildContainer.handle
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+public class ChildContainerException extends Exception {
+    ChildContainerException(String msg) {
+        super(msg);
+    }
+}
+
diff --git a/src/fniki/wiki/FreenetWikiTextParser.java b/src/fniki/wiki/FreenetWikiTextParser.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/FreenetWikiTextParser.java
@@ -0,0 +1,104 @@
+// Attribution: Derived from WikiParserDemo by Yaroslav Stavnichiy.
+// Changes Copyright (c) 2010, 2011 Darrell Karbott
+// Changes licensed under the GPL2 (or later).
+// Original Copywright notice follows.
+/*
+ * Copyright (c) 2007 Yaroslav Stavnichiy, yarosla@gmail.com
+ *
+ * Latest version of this software can be obtained from:
+ *   http://web-tec.info/WikiParser/
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * If you make use of this code, I'd appreciate hearing about it.
+ * Comments, suggestions, and bug reports welcome: yarosla@gmail.com
+ */
+
+package fniki.wiki;
+
+import java.io.UnsupportedEncodingException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+
+
+import static ys.wikiparser.Utils.*;
+import ys.wikiparser.WikiParser;
+
+public class FreenetWikiTextParser extends WikiParser {
+    public interface ParserDelegate {
+        // Return false to invoke the base class hander, true otherwise.
+        boolean processedMacro(StringBuilder sb, String text);
+
+        // We never want to call the base class.
+        void appendLink(StringBuilder sb, String text);
+
+        // We never want to call the base class.
+        void appendImage(StringBuilder sb, String text);
+    }
+
+    private final ParserDelegate mParserDelegate;
+
+    public FreenetWikiTextParser(String wikiText,
+                                 ParserDelegate parserDelegate) {
+        super();
+        HEADING_LEVEL_SHIFT=0;
+        mParserDelegate = parserDelegate;
+        parse(wikiText);
+    }
+
+    @Override
+    protected void appendImage(String text) {
+        if (mParserDelegate == null) {
+            return;
+        }
+        mParserDelegate.appendImage(sb, text);
+    }
+
+    public static String renderXHTML(String wikiText) {
+        throw new RuntimeException("Use the constructor so you can pass in a ParserDelegate.");
+    }
+
+    @Override
+    protected void appendLink(String text) {
+        if (mParserDelegate == null) {
+            return;
+        }
+        mParserDelegate.appendLink(sb, text);
+    }
+
+    @Override
+    protected void appendMacro(String text) {
+        if (mParserDelegate != null && mParserDelegate.processedMacro(sb, text)) {
+            return;
+        }
+        super.appendMacro(text);
+    }
+
+    public static String escapeURL(String s) {
+        try {
+            return URLEncoder.encode(s, "utf-8");
+        }
+        catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/HtmlUtils.java b/src/fniki/wiki/HtmlUtils.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/HtmlUtils.java
@@ -0,0 +1,169 @@
+/* Utility class for rendering snippets of HTML.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static ys.wikiparser.Utils.*;
+
+import wormarc.FileManifest;
+
+public class HtmlUtils {
+
+    public static String makeHref(String fullPath,
+                                  String actionValue, String titleValue,
+                                  String uriValue, String gotoValue) {
+
+        String query = "";
+        if (actionValue != null) {
+            query += "action=" + actionValue;
+        }
+
+        if (titleValue != null) {
+            if (query.length() > 0) { query += "&"; }
+            query += "title=" + titleValue;
+        }
+
+        if (uriValue != null) {
+            if (query.length() > 0) { query += "&"; }
+            query += "uri=" + uriValue;
+        }
+
+        if (gotoValue != null) {
+            if (query.length() > 0) { query += "&"; }
+            query += "goto=" + gotoValue;
+        }
+
+        if (query.length() == 0) {
+            query = null;
+        }
+
+        try {
+            return new URI(null,
+                           null,
+                           fullPath,
+                           query,
+                           null).toString();
+        } catch (URISyntaxException se) {
+            System.err.println("HtmlUtils.makeHref failed: " +
+                               fullPath + " : " + query);
+            return "HTML_UTILS_MAKE_HREF_FAILED";
+        }
+    }
+
+    public static String makeHref(String fullPath) {
+        return makeHref(fullPath, null, null, null, null);
+    }
+
+    public static String makeFproxyHref(String fproxyPrefix, String freenetUri) {
+        // DCI: url encode? use key=?
+        return fproxyPrefix + freenetUri;
+    }
+
+    // DCI: option to convert '_' -> ' '
+    public static void appendPageLink(String prefix, StringBuilder sb, String name, String action, boolean asTitle) {
+        String title = name;
+        if (asTitle) {
+            title = title.replace("_", " ");
+        }
+
+        String href = makeHref(prefix + '/' + name,
+                               action, null, null, name);
+
+        sb.append("<a href=\"" + href + "\">" + escapeHTML(title) + "</a>");
+    }
+
+    public static void appendChangesSet(String prefix, StringBuilder sb, String label, Set<String> values) {
+        if (values.size() == 0) {
+            return;
+        }
+        sb.append(label);
+        sb.append(": ");
+        List<String> sorted = new ArrayList<String>(values);
+        Collections.sort(sorted);
+        // No join in Java? wtf?
+        for (int index = 0; index < sorted.size(); index++) {
+            appendPageLink(prefix, sb, sorted.get(index), "finished", false);
+            if (index < sorted.size() - 1) {
+                sb.append(", ");
+            }
+        }
+        sb.append("<br>\n");
+    }
+
+    public static void appendChangesHtml(FileManifest.Changes changes, String prefix, StringBuilder sb) {
+        appendChangesSet(prefix, sb, "Deleted", changes.mDeleted);
+        appendChangesSet(prefix, sb, "Added", changes.mAdded);
+        appendChangesSet(prefix, sb, "Modified", changes.mModified);
+    }
+
+    // Path is the ony variable that has potentially dangerous data.
+    public static String buttonHtml(String fullPath, String label, String action) {
+        final String fmt =
+            "<form method=\"get\" action=\"%s\" accept-charset=\"UTF-8\">" +
+            "   <input type=submit value=\"%s\">" +
+            "   <input type=hidden name=\"action\" value=\"%s\">" +
+            "</form>";
+        return String.format(fmt, makeHref(fullPath), label, action);
+    }
+
+    public static String getVersionLink(String prefix, String name, String uri, String action) {
+        String href = makeHref(prefix + name, action, null, uri, null);
+
+        return String.format("<a href=\"%s\">%s</a>", href, escapeHTML(uri));
+    }
+
+    // Hmmmm...
+    public static String getVersionLink(String prefix, String name, String uri) {
+        return getVersionLink(prefix, name, uri, "finished");
+    }
+
+    public static boolean isValidFreenetUri(String link) {
+        // DCI: do much better!
+        return (link.startsWith("freenet:CHK@") ||
+                link.startsWith("freenet:SSK@") ||
+                link.startsWith("freenet:USK@"));
+    }
+
+    public static boolean isValidLocalLink(String link) {
+        for (int index = 0; index < link.length(); index++) {
+            char c  = link.charAt(index);
+            if ((c >= '0' && c <= '9') ||
+                (c >= 'a' && c <= 'z') ||
+                (c >= 'A' && c <= 'Z') ||
+                c == '_') {
+                continue;
+            }
+            return false;
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/LocalWikiChanges.java b/src/fniki/wiki/LocalWikiChanges.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/LocalWikiChanges.java
@@ -0,0 +1,147 @@
+/* Class to overlay local changes on top of a version from a FileManifest.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import wormarc.Archive;
+import wormarc.FileManifest;
+import wormarc.IOUtil;
+import wormarc.LinkDigest;
+
+public class LocalWikiChanges implements WikiTextStorage, FileManifest.IO {
+    final static class LocalChange {
+        public final String mData;
+        public final boolean mDeleted;
+        LocalChange(String data, boolean deleted) {
+            mData = data;
+            mDeleted = deleted;
+        }
+    }
+
+    private Map<String, LocalChange> mMap = new HashMap<String, LocalChange> ();
+    private Archive mArchive;
+    private FileManifest mBaseVersion;
+
+    public LocalWikiChanges(Archive archive, FileManifest manifest) {
+        mArchive = archive;
+        mBaseVersion = manifest;
+    }
+
+    ////////////////////////////////////////////////////////////
+    // WikiTextStorage implementation
+    public boolean hasPage(String name) throws IOException {
+        if (mMap.containsKey(name)) {
+            return !mMap.get(name).mDeleted;
+        }
+
+        return mBaseVersion.contains(name);
+    }
+
+    public String getPage(String name) throws IOException {
+        if (mMap.containsKey(name)) {
+            if (mMap.get(name).mDeleted) {
+                throw new FileNotFoundException("Deleted by overlay: " + name);
+            }
+            return mMap.get(name).mData;
+        }
+
+        return IOUtil.readUtf8StringAndClose(mBaseVersion.getFile(mArchive, name));
+    }
+
+    public void putPage(String name, String text) throws IOException {
+        mMap.put(name, new LocalChange(text, false));
+    }
+
+    public List<String> getNames() throws IOException {
+        Set<String> baseNames = new HashSet(mBaseVersion.allFiles()); // Must copy!
+
+        for (String name : mMap.keySet()) {
+            if (mMap.get(name).mDeleted) {
+                baseNames.remove(name);
+            } else {
+                baseNames.add(name);
+            }
+        }
+
+        List<String> names = new ArrayList<String>(baseNames);
+        Collections.sort(names);
+        return names;
+    }
+
+    public void deletePage(String name) throws IOException {
+        mMap.put(name, new LocalChange(null, true));
+    }
+
+    public boolean hasLocalChange(String pageName) {
+        return mMap.containsKey(pageName);
+    }
+
+    public void revertLocalChange(String pageName) {
+        if (!mMap.containsKey(pageName)) {
+            return;
+        }
+        mMap.remove(pageName);
+    }
+
+    ////////////////////////////////////////////////////////////
+    // FileManifest.IO implementation
+    public Map<String, LinkDigest> getFiles() throws IOException {
+        Map<String, LinkDigest> files = new HashMap<String, LinkDigest>();
+        for (String name : getNames()) {
+            files.put(name, LinkDigest.NULL_DIGEST);
+        }
+        return files;
+    }
+
+    public InputStream getFile(String name) throws IOException {
+        if (mMap.containsKey(name)) {
+            if (mMap.get(name).mDeleted) {
+                throw new FileNotFoundException("Deleted by overlay: " + name);
+            }
+            return new ByteArrayInputStream(mMap.get(name).mData.getBytes(IOUtil.UTF8));
+        }
+        return mBaseVersion.getFile(mArchive, name);
+    }
+
+    private static void enotimpl() throws IOException {
+        throw new IOException("Only reading is implemented!");
+    }
+    public void putFile(String name, InputStream rawBytes) throws IOException { enotimpl(); }
+    public void deleteFile(String name) throws IOException { enotimpl(); }
+    public void startSync(Set<String> allFiles) throws IOException { enotimpl(); }
+    public void endSync(Set<String> allFiles) throws IOException { enotimpl(); }
+}
diff --git a/src/fniki/wiki/ModalContainer.java b/src/fniki/wiki/ModalContainer.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ModalContainer.java
@@ -0,0 +1,39 @@
+/* A ChildContainer that can keep the UI from transitioning to another state until it is finished.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+public interface ModalContainer extends ChildContainer {
+    // Return false to prevent transitions out of the current UI State.
+    boolean isFinished();
+
+    void cancel();
+
+    // Entered the UI state handled by this container.
+    void entered(WikiContext context);
+
+    // Exited the UI state handled by this container.
+    void exited();
+}
+
diff --git a/src/fniki/wiki/NotFoundException.java b/src/fniki/wiki/NotFoundException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/NotFoundException.java
@@ -0,0 +1,32 @@
+/* Exception raised for HTTP 404 errors.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+public class NotFoundException extends ChildContainerException {
+    public NotFoundException(String msg) {
+        super(msg);
+    }
+}
+
diff --git a/src/fniki/wiki/Query.java b/src/fniki/wiki/Query.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/Query.java
@@ -0,0 +1,30 @@
+/* An interface to represent HTTP Request query and POST parameters.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+public interface Query {
+    boolean containsKey(String paramName);
+    String get(String paramName);
+}
diff --git a/src/fniki/wiki/RedirectException.java b/src/fniki/wiki/RedirectException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/RedirectException.java
@@ -0,0 +1,37 @@
+/* Exception raised for HTTP 302 redirects.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+
+package fniki.wiki;
+
+public class RedirectException extends ChildContainerException {
+    private final String mToLocation;
+    public RedirectException(String toLocation, String msg) {
+        super(msg);
+        mToLocation = toLocation;
+    }
+
+    public String getLocation() { return mToLocation; }
+}
+
diff --git a/src/fniki/wiki/Request.java b/src/fniki/wiki/Request.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/Request.java
@@ -0,0 +1,33 @@
+/* Interface to represent the parts of an HTTP Request that are important to WikiApp.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+// Dynamic per request state.
+public interface Request {
+    String getPath();
+    Query getQuery();
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/ServerErrorException.java b/src/fniki/wiki/ServerErrorException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ServerErrorException.java
@@ -0,0 +1,32 @@
+/* Exception raised for HTTP 500 errors.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+public class ServerErrorException extends ChildContainerException {
+    public ServerErrorException(String msg) {
+        super(msg);
+    }
+}
+
diff --git a/src/fniki/wiki/WikiApp.java b/src/fniki/wiki/WikiApp.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/WikiApp.java
@@ -0,0 +1,477 @@
+/* A web application to read, edit and submit changes to a jfniki wiki in Freenet.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import static ys.wikiparser.Utils.*; // DCI: clean up
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import wormarc.FileManifest;
+
+import static fniki.wiki.HtmlUtils.*;
+
+import fniki.wiki.child.AsyncTaskContainer;
+import fniki.wiki.child.DefaultRedirect;
+import fniki.wiki.child.GotoRedirect;
+import fniki.wiki.child.LoadingArchive;
+import fniki.wiki.child.LoadingChangeLog;
+import fniki.wiki.child.LoadingVersionList;
+import fniki.wiki.child.QueryError;
+import fniki.wiki.child.Submitting;
+import fniki.wiki.child.WikiContainer;
+
+// Aggregates a bunch of other Containers and runs UI state machine.
+public class WikiApp implements ChildContainer, WikiContext {
+    // Delegate to implement link, image and macro handling in wikitext.
+    private final FreenetWikiTextParser.ParserDelegate mParserDelegate;
+
+    private final ChildContainer mDefaultRedirect;
+    private final ChildContainer mGotoRedirect;
+    private final ChildContainer mQueryError;
+    private final ChildContainer mWikiContainer;
+
+    // Containers for asynchronous tasks.
+    private final ChildContainer mLoadingVersionList;
+    private final ChildContainer mLoadingArchive;
+    private final ChildContainer mSubmitting;
+    private final ChildContainer mLoadingChangeLog;
+
+    // The current default UI state.
+    private ChildContainer mState;
+
+    // Transient, per request state.
+    private Request mRequest;
+
+    private ArchiveManager mArchiveManager;
+
+    private String mFproxyPrefix = "http://127.0.0.1:8888/";
+    private boolean mAllowImages = true;
+    private String mFormPassword;
+    private boolean mUseMultiPartForms;
+
+    public WikiApp(ArchiveManager archiveManager) {
+        mParserDelegate = new LocalParserDelegate(this, archiveManager);
+
+        mDefaultRedirect = new DefaultRedirect();
+        mGotoRedirect = new GotoRedirect();
+        mQueryError = new QueryError();
+        mWikiContainer = new WikiContainer();
+
+        mLoadingVersionList = new LoadingVersionList(archiveManager);
+        mLoadingArchive = new LoadingArchive(archiveManager);
+        mSubmitting = new Submitting(archiveManager);
+        mLoadingChangeLog = new LoadingChangeLog(archiveManager);
+
+        mState = mWikiContainer;
+        mArchiveManager = archiveManager;
+    }
+
+    public void setFproxyPrefix(String value) {
+        if (!value.startsWith("http") && value.endsWith("/")) {
+            throw new IllegalArgumentException("Expected a value starting with 'http' and ending with '/'");
+        }
+        mFproxyPrefix = value;
+    }
+
+    public void setAllowImages(boolean value) {
+        mAllowImages = value;
+    }
+
+    public void setFormPassword(String value) {
+        mFormPassword = value;
+    }
+
+    public void setUseMultiPartForms(boolean value) {
+        mUseMultiPartForms = value;
+    }
+
+    private ChildContainer setState(WikiContext context, ChildContainer container) {
+        if (mState == container) {
+            return mState;
+        }
+
+        System.err.println(String.format("[%s] => [%s]",
+                                         mState.getClass().getName(),
+                                         container.getClass().getName()));
+        if (mState != null && mState instanceof ModalContainer) {
+            ((ModalContainer)mState).exited();
+        }
+
+        mState = container;
+
+        if (mState instanceof ModalContainer) {
+            ((ModalContainer)mState).entered(context);
+        }
+
+        return mState;
+    }
+
+    // This function defines the UI state machine.
+    private ChildContainer routeRequest(WikiContext request)
+        throws IOException {
+
+        // DCI: move this into the freenet plugin implementation
+        // mPath = "";
+        // mQuery = null;
+        // mAction = "";
+        // mTitle = "";
+
+        // System.err.println("Method: " + request.getMethod());
+        // String path = request.getPath();
+        // System.err.println(String.format("Raw Path: [%s]", path));
+
+        // String prefix = containerPrefix();
+        // if (!path.startsWith(prefix)) {
+        //     return mQueryError;
+        // }
+
+        // path = path.substring(prefix.length());
+        // if (path.equals("")) {
+        //     path = "/";
+        // }
+
+        // System.err.println(String.format("Local Path: [%s]", path));
+
+        // int slashCount = 0;
+        // for (int index = 0; index < path.length(); index++) {
+        //     if (path.charAt(index) == '/') {
+        //         slashCount++;
+        //     }
+        // }
+
+        // if (!path.startsWith("/")) {
+        //     System.err.println("Bad path!");
+        //     return mQueryError;
+        // }
+
+        // path = path.substring(1);
+
+        // Query query = new Query(request);
+        // System.err.println("Query: " + query.toString());
+        // String title = path;
+        // if (query.containsKey("title")) {
+        //     title = query.get("title");
+        // }
+
+        // // DCI: validate title here
+
+        // String action = "view";
+        // if (query.containsKey("action")) {
+        //     action = query.get("action");
+        // }
+
+        // mPath = path;
+        // mQuery = query;
+        // mAction = action;
+        // mTitle = title;
+
+        // The glue code is repsonsible for parsing.
+        // Fail immediately if there are problems in the glue code.
+        if (request.getPath() == null) {
+            throw new RuntimeException("Assertion Failure: path == null");
+        }
+        if (request.getQuery() == null) {
+            throw new RuntimeException("Assertion Failure: query == null");
+        }
+        if (request.getAction() == null) {
+            throw new RuntimeException("Assertion Failure: action == null");
+        }
+        if (request.getTitle() == null) {
+            throw new RuntimeException("Assertion Failure: title == null");
+        }
+
+        String action = request.getAction();
+
+        if (mState instanceof ModalContainer) {
+            // Handle transitions out of modal UI states.
+            ModalContainer state = (ModalContainer)mState;
+            if (action.equals("finished")) {
+                System.err.println("finished");
+                if (!state.isFinished()) {
+                    System.err.println("canceling");
+                    state.cancel();
+                    try {
+                        Thread.sleep(250); // HACK
+                    }
+                    catch (InterruptedException ie) {
+                    }
+                }
+                // No "else" because it might have finished while sleeping.
+                if (state.isFinished()) {
+                    System.err.println("finished");
+                    setState(request, mWikiContainer);
+                    return mGotoRedirect;
+                }
+            }
+            return state;  // Don't leave the modal UI state until finished.
+        }
+
+        String path = request.getPath();
+        int slashCount = 0;
+        for (int index = 0; index < path.length(); index++) {
+            if (path.charAt(index) == '/') {
+                slashCount++;
+            }
+        }
+
+        System.err.println("WikiApp.routeRequest: " + path);
+        if (path.equals("fniki/submit")) {
+            System.err.println("BC0");
+            return setState(request, mSubmitting);
+        } else if (path.equals("fniki/changelog")) {
+            return setState(request, mLoadingChangeLog);
+        } else if (path.equals("fniki/getversions")) {
+            return setState(request, mLoadingVersionList);
+        } else if (path.equals("fniki/loadarchive")) {
+            return setState(request, mLoadingArchive);
+        } else if (path.equals("")) {
+            return mDefaultRedirect;
+        } else if (slashCount != 0) {
+            return mQueryError;
+        } else {
+            setState(request, mWikiContainer);
+        }
+
+        return mState;
+    }
+
+    // All requests are serialized! Hmmmm....
+    public synchronized String handle(WikiContext context) throws ChildContainerException {
+        try {
+            ChildContainer childContainer = routeRequest(context);
+            System.err.println("Request routed to: " + childContainer.getClass().getName());
+            return childContainer.handle(context);
+        } catch (ChildContainerException cce) {
+            // Normal, used to do redirection.
+            throw cce;
+        } catch (Exception e) {
+            context.logError("WikiApp.handle -- untrapped!:", e);
+            throw new ServerErrorException("Coding error. Sorry :-(");
+        }
+    }
+
+    ////////////////////////////////////////////////////////////
+    private static class LocalParserDelegate implements FreenetWikiTextParser.ParserDelegate {
+        // Pedantic.  Explictly copy references instead of making this class non-static
+        // so that the code uses well defined interfaces.
+        final WikiContext mContext;
+        final ArchiveManager mArchiveManager;
+
+        LocalParserDelegate(WikiContext context, ArchiveManager archiveManager) {
+            mContext = context;
+            mArchiveManager = archiveManager;
+        }
+
+        public boolean processedMacro(StringBuilder sb, String text) {
+            if (text.equals("LocalChanges")) {
+                try {
+                    FileManifest.Changes changes  = mArchiveManager.getLocalChanges();
+                    if (changes.isUnmodified()) {
+                        sb.append("<br>No local changes.<br>");
+                        return true;
+                    }
+                    appendChangesHtml(changes, containerPrefix(), sb);
+                    return true;
+                } catch (IOException ioe) {
+                    sb.append("{ERROR PROCESSING LOCALCHANGES MACRO}");
+                    return true;
+                }
+            } else if (text.equals("TitleIndex")) {
+                try {
+                    for (String name : mArchiveManager.getStorage().getNames()) {
+                        appendPageLink(containerPrefix(), sb, name, null, true);
+                        sb.append("<br>");
+                    }
+                } catch (IOException ioe) {
+                    sb.append("{ERROR PROCESSING TITLEINDEX MACRO}");
+                    return true;
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        // CHK, SSK, USK freenet links.
+        public void appendLink(StringBuilder sb, String text) {
+            String fproxyPrefix = mContext.getString("fproxy_prefix", null);
+
+            String[] link=split(text, '|');
+            if (fproxyPrefix != null &&
+                isValidFreenetUri(link[0])) {
+                sb.append("<a href=\""+ makeFproxyHref(fproxyPrefix, link[0].trim()) +"\" rel=\"nofollow\">");
+                sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
+                sb.append("</a>");
+                return;
+            }
+            if (isValidLocalLink(link[0])) {
+                // Link to an internal wiki page.
+                sb.append("<a href=\""+ makeHref(mContext.makeLink("/" + link[0].trim())) +"\" rel=\"nofollow\">");
+                sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
+                sb.append("</a>");
+                return;
+            }
+
+            sb.append("<a href=\"" + makeHref(mContext.makeLink("/ExternalLink")) +"\" rel=\"nofollow\">");
+            sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
+            sb.append("</a>");
+        }
+
+        // Only CHK and SSK freenet links.
+        public void appendImage(StringBuilder sb, String text) {
+            boolean allowed = mContext.getInt("allow_images", 0) != 0;
+            if (!allowed) {
+                sb.append("{IMAGES DISABLED. IMAGE WIKITEXT IGNORED}");
+                return;
+            }
+
+            String fproxyPrefix = mContext.getString("fproxy_prefix", null);
+            if (fproxyPrefix == null) {
+                sb.append("{FPROXY PREFIX NOT SET. IMAGE WIKITEXT IGNORED}");
+                return;
+            }
+
+            String[] link=split(text, '|');
+            if (fproxyPrefix != null &&
+                isValidFreenetUri(link[0]) &&
+                !link[0].startsWith("freenet:USK@")) {
+                String alt=escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0]));
+                sb.append("<img src=\"" + makeFproxyHref(fproxyPrefix, link[0].trim())
+                          + "\" alt=\""+alt+"\" title=\""+alt+"\" />");
+                return;
+            }
+            sb.append("{ERROR PROCESSING IMAGE WIKITEXT}");;
+        }
+    }
+
+    // NO trailing slash.
+    private static String containerPrefix() { return "/plugins/fniki.plugin.Fniki"; }
+
+    ////////////////////////////////////////////////////////////
+    // Wiki context implementations.
+    public WikiTextStorage getStorage() throws IOException { return mArchiveManager.getStorage(); }
+
+    public FreenetWikiTextParser.ParserDelegate getParserDelegate() { return mParserDelegate; }
+
+    public String getString(String keyName, String defaultValue) {
+        if (keyName.equals("default_page")) {
+            return "Front_Page";
+        } else if (keyName.equals("fproxy_prefix")) {
+            if (mFproxyPrefix == null) {
+                return defaultValue;
+            }
+            return mFproxyPrefix;
+        } else if (keyName.equals("parent_uri")) {
+            if (mArchiveManager.getParentUri() == null) {
+                // Can be null
+                return defaultValue;
+            }
+            return mArchiveManager.getParentUri();
+        } else if (keyName.equals("container_prefix")) {
+            return containerPrefix();
+        } else if (keyName.equals("form_password") && mFormPassword != null) {
+            return mFormPassword;
+        } else if (keyName.equals("form_encoding")) {
+            if (mUseMultiPartForms) {
+                return "multipart/form-data";
+            }
+            return "application/x-www-form-urlencoded";
+        }
+
+        return defaultValue;
+    }
+
+    public int getInt(String keyName, int defaultValue) {
+        if (keyName.equals("allow_images")) {
+            return mAllowImages ? 1 : 0;
+        }
+        return defaultValue;
+    }
+
+    // DCI: Think this through.
+    public String makeLink(String containerRelativePath) {
+        // Hacks to find bugs
+        if (!containerRelativePath.startsWith("/")) {
+            containerRelativePath = "/" + containerRelativePath;
+            System.err.println("WikiApp.makeLink -- added leading '/': " +
+                               containerRelativePath);
+        }
+        String full = containerPrefix() + containerRelativePath;
+        while (full.indexOf("//") != -1) {
+            System.err.println("WikiApp.makeLink -- fixing  '//': " +
+                               full);
+            full = full.replace("//", "/");
+        }
+        return full;
+    }
+
+    public void raiseRedirect(String toLocation, String msg) throws RedirectException {
+        throw new RedirectException(toLocation, msg);
+    }
+
+    public void raiseNotFound(String msg) throws NotFoundException {
+        throw new NotFoundException(msg);
+    }
+
+    public void raiseAccessDenied(String msg) throws AccessDeniedException {
+        throw new AccessDeniedException(msg);
+    }
+
+    public void raiseServerError(String msg) throws ServerErrorException {
+        throw new ServerErrorException(msg);
+    }
+
+    public void logError(String msg, Throwable t) {
+        if (msg == null) {
+            msg = "null";
+        }
+        if (t == null) {
+            t = new RuntimeException("FAKE EXCEPTION: logError called with t == null!");
+        }
+        System.err.println("Unexpected error: " + msg + " : " + t.toString());
+        t.printStackTrace();
+    }
+
+    // Delegate to the mRequest helper instance set with setRequest().
+    public String getPath() { return mRequest.getPath(); }
+    public Query getQuery() { return mRequest.getQuery(); }
+
+    public String getAction() { return mRequest.getQuery().get("action"); }
+    public String getTitle() { return mRequest.getQuery().get("title"); }
+
+    ////////////////////////////////////////////////////////////
+    public void setRequest(Request request) {
+        if (request == null) {
+            throw new IllegalArgumentException("request == null");
+        }
+        mRequest = request;
+    }
+}
diff --git a/src/fniki/wiki/WikiContext.java b/src/fniki/wiki/WikiContext.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/WikiContext.java
@@ -0,0 +1,51 @@
+/* Interface to represent the state of a jfniki wiki.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+// Add wiki specific functionality.
+public interface WikiContext extends Request {
+    // DCI: are these used?
+    String getAction();
+    String getTitle(); // hmmm
+
+    WikiTextStorage getStorage() throws IOException;
+    FreenetWikiTextParser.ParserDelegate getParserDelegate();
+
+    // Client must deal with url escaping.
+    String makeLink(String containerRelativePath);
+
+    String getString(String keyName, String defaultValue);
+    int getInt(String keyName, int defaultValue);
+
+    // throwable can be null
+    void logError(String msg, Throwable throwable);
+
+    void raiseRedirect(String toLocation, String msg) throws RedirectException; // 302
+    void raiseNotFound(String msg) throws NotFoundException;  // 404
+    void raiseAccessDenied(String msg) throws AccessDeniedException;  // 403
+    void raiseServerError(String msg) throws ServerErrorException;  // 500
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/WikiTextStorage.java b/src/fniki/wiki/WikiTextStorage.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/WikiTextStorage.java
@@ -0,0 +1,40 @@
+/* Interface for the wiki's backing store.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface WikiTextStorage {
+    boolean hasPage(String name) throws IOException;
+    String getPage(String name) throws IOException;
+    void putPage(String name, String text) throws IOException;
+    List<String> getNames() throws IOException;
+    void deletePage(String name) throws IOException;
+
+    boolean hasLocalChange(String name);
+    // Reverting changes on a page that has no changes is allowed.
+    void revertLocalChange(String name);
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/AsyncTaskContainer.java b/src/fniki/wiki/child/AsyncTaskContainer.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/AsyncTaskContainer.java
@@ -0,0 +1,172 @@
+/* A base class for modal UI states which run background tasks.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+
+import ys.wikiparser.Utils;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.ModalContainer;
+import fniki.wiki.RedirectException;
+import fniki.wiki.WikiContext;
+
+public abstract class AsyncTaskContainer implements ChildContainer, ModalContainer {
+    final protected ArchiveManager mArchiveManager;
+
+    protected final int STATE_WAITING = 1;
+    protected final int STATE_WORKING = 2;
+    protected final int STATE_SUCCEEDED = 3;
+    protected final int STATE_FAILED = 4;
+
+    private int mState;
+    private Thread mThread;
+    protected ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
+    protected boolean mUIRunning = false;
+    protected String mExitPage = "/";
+
+
+    // 15 second refresh if the task isn't finished.
+    protected String metaRefresh() {
+        if (isFinished()) {
+            return "";
+        }
+        return "<meta http-equiv=\"refresh\" content=\"15\" />";
+    }
+
+    // DCI: make these return a string? To get rid of no return value warnings
+    protected void sendRedirect(WikiContext context, String toName) throws RedirectException {
+        sendRedirect(context, toName, null);
+    }
+
+    protected void sendRedirect(WikiContext context, String toName, String action) throws RedirectException {
+        String target = toName;
+        if (action != null) {
+            target += "?action=" + action;
+        }
+
+        context.raiseRedirect(context.makeLink("/" + target), "Redirecting...");
+    }
+
+
+
+    // DCI: use a single form? Really ugly.
+    protected void addButtonsHtml(WikiContext context, PrintWriter writer,
+                                  String confirmTitle, String cancelTitle) {
+
+        System.err.println("addButtonsHtml -- context.getPath(): " + context.getPath());
+
+        if (confirmTitle != null) {
+            writer.println(buttonHtml(context.makeLink("/" + context.getPath()), confirmTitle, "confirm"));
+        }
+
+        if (cancelTitle != null) {
+            writer.println(buttonHtml(context.makeLink("/" + context.getPath()), cancelTitle, "finished"));
+        }
+    }
+
+    protected synchronized String getOutput() throws UnsupportedEncodingException {
+        return mBuffer.toString("UTF-8");
+    }
+
+    protected synchronized int getState() { return mState; }
+
+    public AsyncTaskContainer(ArchiveManager archiveManager) {
+        mArchiveManager = archiveManager;
+        mState = STATE_WAITING;
+    }
+
+    public synchronized boolean isFinished() {
+        return mState == STATE_SUCCEEDED ||
+            mState == STATE_FAILED ||
+            mState == STATE_WAITING;
+    }
+
+    public synchronized void cancel() {
+        if (mThread == null) {
+            return;
+        }
+        mThread.interrupt();
+    }
+
+    public void entered(WikiContext context) {
+        mUIRunning = true;
+        mExitPage = context.getTitle();
+    }
+
+    // Subclasses should reset state here.
+    public void exited() {
+        if (!isFinished()) {
+            throw new IllegalStateException("Task didn't finish yet!");
+        }
+        mBuffer = new ByteArrayOutputStream();
+        mExitPage = "/";
+        mState = STATE_WAITING;
+        mUIRunning = false;
+    }
+
+    public synchronized void startTask() {
+        if (mThread != null) {
+            return;
+        }
+        mState = STATE_WORKING;
+        mBuffer = new ByteArrayOutputStream();
+        mThread = new Thread( new Runnable() {
+                public void run() {
+                    invokeWorkerMethod();
+                }
+            });
+        mThread.start();
+    }
+
+    protected void invokeWorkerMethod() {
+        boolean failed = true;
+        try {
+            System.err.println("Task started: " + mState);
+            PrintStream log = new PrintStream(mBuffer, true);
+            mArchiveManager.setDebugOutput(log);
+           failed = !doWork(new PrintStream(mBuffer, true));
+        } catch (Exception e) {
+            e.printStackTrace();
+            failed = true;
+        } finally {
+            synchronized (this) {
+                mState = failed ? STATE_FAILED : STATE_SUCCEEDED;
+                System.err.println("Task finished: " + mState);
+                mThread = null;
+            }
+        }
+    }
+
+    public abstract String handle(WikiContext context) throws ChildContainerException;
+    public abstract boolean doWork(PrintStream out) throws Exception;
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/DefaultRedirect.java b/src/fniki/wiki/child/DefaultRedirect.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/DefaultRedirect.java
@@ -0,0 +1,44 @@
+/* A UI subcomponent which redirects to the default page.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import fniki.wiki.WikiContext;
+
+// DCI: remove this file
+public class DefaultRedirect implements ChildContainer {
+    public DefaultRedirect() {}
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        context.raiseRedirect(context.makeLink("/" + context.getString("default_page", "Front_Page")),
+                              "Redirecting...");
+
+        return "unreachable code";
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/GotoRedirect.java b/src/fniki/wiki/child/GotoRedirect.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/GotoRedirect.java
@@ -0,0 +1,51 @@
+/* A UI subcomponent which redirects to a particular page or loads another wiki version from a uri.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import fniki.wiki.WikiContext;
+
+
+public class GotoRedirect implements ChildContainer {
+    public GotoRedirect() {}
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        String target = context.makeLink("/" + context.getString("default_page", "Front_Page"));
+        if (context.getQuery().get("goto") != null) {
+            target = context.makeLink("/" + context.getQuery().get("goto"));
+        }
+        if (context.getQuery().get("uri") != null) {
+            target = context.makeLink("/fniki/loadarchive?uri=" + context.getQuery().get("uri"));
+        }
+
+        context.raiseRedirect(target, "Redirecting...");
+
+        return "unreachable code";
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/LoadingArchive.java b/src/fniki/wiki/child/LoadingArchive.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/LoadingArchive.java
@@ -0,0 +1,149 @@
+/* A UI subcomponent for loading a wiki version from a WORM Archive in Freenet.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static ys.wikiparser.Utils.*;
+import static fniki.wiki.HtmlUtils.*;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import fniki.wiki.WikiContext;
+
+public class LoadingArchive extends AsyncTaskContainer {
+    private String mUri;
+    public LoadingArchive(ArchiveManager archiveManager) {
+        super(archiveManager);
+    }
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        try {
+            if (context.getQuery().get("uri") != null && mUri == null) {
+                mUri = context.getQuery().get("uri");
+                System.err.println("handle -- set uri: " + mUri);
+            }
+
+            if (context.getAction().equals("confirm")) {
+                if (mUri != null) {
+                    startTask();
+                    sendRedirect(context, context.getPath());
+                    return "Unreachable code";
+                }
+            }
+
+            boolean showBuffer = false;
+            boolean showUri = false;
+            String confirmTitle = null;
+            String cancelTitle = null;
+            String title = null;
+            switch (getState()) {
+            case STATE_WORKING:
+                showBuffer = true;
+                title = "Loading Archive: " + mUri;;
+                cancelTitle = "Cancel";
+                break;
+            case STATE_WAITING:
+                showBuffer = false;
+                showUri = true;
+                title = "Confirm Load";
+                confirmTitle = "Load";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_SUCCEEDED:
+                showBuffer = true;
+                title = "Archive Loaded: " + mUri;
+                cancelTitle = "Done";
+                break;
+            case STATE_FAILED:
+                showBuffer = true;
+                title = "Archive Load Failed";
+                confirmTitle = "Retry";
+                cancelTitle = "Done";
+                break;
+            }
+            StringWriter buffer = new StringWriter();
+            PrintWriter body = new PrintWriter(buffer);
+
+            body.println("<html><head>" + metaRefresh() + "<title>" + escapeHTML(title) + "</title></head><body>");
+
+            if (getState() == STATE_WORKING || getState() == STATE_SUCCEEDED) {
+                title = "Loading Archive:"; // Don't put full uri in header
+            }
+            body.println("<h3>" + escapeHTML(title) + "</h3>");
+            if (showUri) {
+                body.println(escapeHTML(mUri));
+                body.println("<p/>Clicking Load will discard any unsubmitted local changes.</p>");
+            } else if (getState() == STATE_WORKING || getState() == STATE_SUCCEEDED) {
+                body.println(escapeHTML(mUri));
+            }
+
+            if (showBuffer) {
+                body.println("<pre>");
+                body.print(escapeHTML(getOutput()));
+                body.println("</pre>");
+            }
+
+            addButtonsHtml(context, body, confirmTitle, cancelTitle);
+
+            body.println("</body></html>");
+            body.close();
+            return buffer.toString();
+
+        } catch (IOException ioe) {
+            context.logError("Loading Archive", ioe);
+            context.raiseServerError("LoadingArchive.handle coding error. Sorry :-(");
+            return "unreachable code";
+        }
+    }
+
+    public boolean doWork(PrintStream out) throws Exception {
+        if (mUri == null) {
+            out.println("The request uri isn't set");
+            return false;
+        }
+
+        try {
+            out.println("Loading. Please be patient...");
+            mArchiveManager.load(mUri);
+            out.println("Loaded " + mUri);
+            return true;
+        } catch (IOException ioe) {
+            out.println("Load failed from background thread: " + ioe.getMessage());
+            return false;
+        }
+    }
+
+    public void entered(WikiContext context) {
+        System.err.println("DCI: entered called, reset mUri");
+        mUri = null;
+    }
+
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/LoadingChangeLog.java b/src/fniki/wiki/child/LoadingChangeLog.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/LoadingChangeLog.java
@@ -0,0 +1,154 @@
+/* A UI subcomponent to load change history for the current wiki version from Freenet.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static ys.wikiparser.Utils.*;
+
+import wormarc.ExternalRefs;
+import wormarc.FileManifest;
+import wormarc.AuditArchive;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.WikiContext;
+
+public class LoadingChangeLog extends AsyncTaskContainer
+    implements AuditArchive.ChangeLogCallback {
+    private StringBuilder mListHtml = new StringBuilder();
+    private String mPath;
+    private String mContainerPrefix;
+    public LoadingChangeLog(ArchiveManager archiveManager) {
+        super(archiveManager);
+    }
+
+    public synchronized String getListHtml() {
+        return mListHtml.toString();
+    }
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        try {
+            if (context.getAction().equals("confirm")) {
+                mPath = context.getPath();
+                mContainerPrefix = context.getString("container_prefix", null);
+                if (mContainerPrefix == null) {
+                    throw new RuntimeException("Assertion Failure: mContainerPrefix == null");
+                }
+                startTask();
+                sendRedirect(context, context.getPath());
+                return "unreachable code";
+            } // DCI: ripped out code, need to fix links
+
+            boolean showBuffer = false;
+            String confirmTitle = null;
+            String cancelTitle = null;
+            String title = null;
+            switch (getState()) {
+            case STATE_WORKING:
+                showBuffer = true;
+                title = "Loading Change Log";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_WAITING:
+                // Shouldn't hit this state.
+                showBuffer = false;
+                title = "Loading Change Log";
+                confirmTitle = "Load";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_SUCCEEDED:
+                showBuffer = true;
+                title = "Read Full Change Log";
+                confirmTitle = null;
+                cancelTitle = "Done";
+                break;
+            case STATE_FAILED:
+                showBuffer = true;
+                title = "Full Read of Change Log Failed";
+                confirmTitle = "Reload";
+                cancelTitle = "Done";
+                break;
+            }
+
+            StringWriter buffer = new StringWriter();
+            PrintWriter body = new PrintWriter(buffer);
+
+            body.println("<html><head>" + metaRefresh() + "<title>" + escapeHTML(title) + "</title></head><body>");
+            body.println("<h3>" + escapeHTML(title) + "</h3>");
+            if (showBuffer) {
+                body.println(getListHtml());
+                body.println("<hr>");
+                body.println("<pre>");
+                body.print(escapeHTML(getOutput()));
+                body.println("</pre>");
+            }
+            body.println("<hr>");
+
+            addButtonsHtml(context, body, confirmTitle, cancelTitle);
+            body.println("</body></html>");
+            body.close();
+            return buffer.toString();
+        } catch (IOException ioe) {
+            context.logError("Submitting", ioe);
+            context.raiseServerError("LoadingChangeLog.handle coding error. Sorry :-(");
+            return "unreachable code";
+        }
+    }
+
+    public boolean doWork(PrintStream out) throws Exception {
+        synchronized (this) {
+            mListHtml = new StringBuilder();
+        }
+
+        try {
+            out.println("Reading the wiki changelog out of freenet. ");
+            mArchiveManager.readChangeLog(out, this);
+            return true;
+        } catch (IOException ioe) {
+            out.println("Error reading log: " + ioe.getMessage());
+            return false;
+        }
+    }
+
+    public synchronized boolean onChangeEntry(ExternalRefs.Reference oldVer,
+                                              ExternalRefs.Reference newVer,
+                                              FileManifest.Changes fromNewToOld) {
+
+        mListHtml.append("<br>");
+        mListHtml.append("FmsID: ");
+        mListHtml.append(escapeHTML(mArchiveManager.getNym(oldVer.mExternalKey)));
+        mListHtml.append("<br>");
+        mListHtml.append(getVersionLink(mContainerPrefix, "/jfniki/changelog", oldVer.mExternalKey));
+        mListHtml.append("<br>\n");
+        appendChangesHtml(fromNewToOld, mContainerPrefix, mListHtml);
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/LoadingVersionList.java b/src/fniki/wiki/child/LoadingVersionList.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/LoadingVersionList.java
@@ -0,0 +1,182 @@
+/* A UI subcomponent to load a list of other versions of this wiki via FMS.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import java.util.List;
+
+import static ys.wikiparser.Utils.*;
+
+import fmsutil.FMSUtil;
+import wormarc.ExternalRefs;
+import wormarc.FileManifest;
+
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.WikiContext;
+
+public class LoadingVersionList extends AsyncTaskContainer {
+    private StringBuilder mListHtml = new StringBuilder();
+    private String mName = "";
+    private String mContainerPrefix;
+    public LoadingVersionList(ArchiveManager archiveManager) {
+        super(archiveManager);
+    }
+
+    public synchronized String getListHtml() {
+        return mListHtml.toString();
+    }
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        try {
+            if (context.getAction().equals("confirm")) {
+                // Copy stuff we need out because context isn't threadsafe.
+                mName = context.getPath();
+                mContainerPrefix = context.getString("container_prefix", null);
+                if (mContainerPrefix == null) {
+                    throw new RuntimeException("Assertion Failure: mContainerPrefix == null");
+                }
+                startTask();
+                try {
+                    Thread.sleep(1000); // Hack. Give task thread a chance to finish.
+                } catch (InterruptedException ioe) {
+                    /* NOP */
+                }
+                sendRedirect(context, context.getPath());
+                return "unreachable code";
+            }
+
+            boolean showBuffer = false;
+            String confirmTitle = null;
+            String cancelTitle = null;
+            String title = null;
+            switch (getState()) {
+            case STATE_WORKING:
+                showBuffer = true;
+                title = "Loading Wiki Version Info from FMS";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_WAITING:
+                // Shouldn't hit this state.
+                showBuffer = false;
+                title = "Load Wiki Version Info from FMS";
+                confirmTitle = "Load";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_SUCCEEDED:
+                showBuffer = true;
+                title = "Loaded Wiki Version Info from FMS";
+                confirmTitle = null;
+                cancelTitle = "Done";
+                break;
+            case STATE_FAILED:
+                showBuffer = true;
+                title = "Full Read of Wiki Version Info Failed";
+                confirmTitle = "Reload";
+                cancelTitle = "Done";
+                break;
+            }
+
+            StringWriter buffer = new StringWriter();
+            PrintWriter body = new PrintWriter(buffer);
+            body.println("<html><head>\n");
+            body.println(metaRefresh());
+            body.println("<style type=\"text/css\">\n");
+            body.println("TD{font-family: Arial; font-size: 7pt;}\n");
+            body.println("</style>\n");
+            body.println("<title>" + escapeHTML(title) + "</title>\n");
+            body.println("</head><body>\n");
+
+            body.println("<h3>" + escapeHTML(title) + "</h3>");
+            if (showBuffer) {
+                body.println(getListHtml());
+                body.println("<hr>");
+                body.println("<pre>");
+                body.print(escapeHTML(getOutput()));
+                body.println("</pre>");
+            }
+            body.println("<hr>");
+            addButtonsHtml(context, body, confirmTitle, cancelTitle);
+            body.println("</body></html>");
+            body.close();
+            return buffer.toString();
+        } catch (IOException ioe) {
+            context.logError("LoadingVersionList", ioe);
+            return "Error LoadingVersionList";
+        }
+    }
+
+    public static String trustString(int value) {
+        if (value == -1) {
+            return "null";
+        }
+        return Integer.toString(value);
+    }
+
+    public boolean doWork(PrintStream out) throws Exception {
+        synchronized (this) {
+            mListHtml = new StringBuilder();
+        }
+        try {
+            out.println("Reading versions from FMS.");
+            List<FMSUtil.BISSRecord> records = mArchiveManager.getRecentWikiVersions(out);
+
+            synchronized (this) {
+
+                mListHtml.append("<table border=\"1\">\n");
+                mListHtml.append("<tr><td>FMS ID</td><td>Date</td><td>Key</td><td>Msg Trust</td><td>TL Trust</td>" +
+                                 "<td>Peer Msg Trust</td><td>Peer TL Trust</td></tr>\n");
+
+                final String fmt = "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td>" +
+                "<td>%s</td><td>%s</td></tr>\n";
+
+                // DCI: BUG. fix to force finished
+                for (FMSUtil.BISSRecord record : records)  {
+                    mListHtml.append(String.format(fmt,
+                                                   escapeHTML(record.mFmsId),
+                                                   record.mDate,
+                                                   getVersionLink(mContainerPrefix,
+                                                                  "/jfniki/loadarchive", record.mKey),
+                                                   trustString(record.msgTrust()),
+                                                   trustString(record.trustListTrust()),
+                                                   trustString(record.peerMsgTrust()),
+                                                   trustString(record.peerTrustListTrust())));
+                }
+                mListHtml.append("</table>\n");
+            }
+            return true;
+        } catch (IOException ioe) {
+            out.println("Error reading log: " + ioe.getMessage());
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/QueryError.java b/src/fniki/wiki/child/QueryError.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/QueryError.java
@@ -0,0 +1,43 @@
+/* A UI subcomponent to handle unresolvable queries to the WikiApp.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import fniki.wiki.RedirectException;
+import fniki.wiki.WikiContext;
+
+public class QueryError implements ChildContainer {
+    public QueryError() {}
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        // DCI: force redirect to a default location?
+        context.raiseAccessDenied("Couldn't resolve query or post.");
+        return "unreachable code";
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/Submitting.java b/src/fniki/wiki/child/Submitting.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/Submitting.java
@@ -0,0 +1,118 @@
+/* A UI subcomponent to insert local changes to the wiki into Freenet and post an FMS notification.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static ys.wikiparser.Utils.*;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.WikiContext;
+
+public class Submitting extends AsyncTaskContainer {
+    public Submitting(ArchiveManager archiveManager) {
+        super(archiveManager);
+    }
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        try {
+            if (context.getAction().equals("confirm")) {
+                startTask();
+                sendRedirect(context, context.getPath());
+                return "Unreachable code";
+            }
+
+            boolean showBuffer = false;
+            String confirmTitle = null;
+            String cancelTitle = null;
+            String title = null;
+            switch (getState()) {
+            case STATE_WORKING:
+                showBuffer = true;
+                title = "Submitting Changes";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_WAITING:
+                showBuffer = false;
+                title = "Confirm Submit";
+                confirmTitle = "Submit";
+                cancelTitle = "Cancel";
+                break;
+            case STATE_SUCCEEDED:
+                showBuffer = true;
+                title = "Submission Succeeded";
+                cancelTitle = "Done";
+                break;
+            case STATE_FAILED:
+                showBuffer = true;
+                title = "Submission Failed";
+                confirmTitle = "Retry";
+                cancelTitle = "Done";
+                break;
+            }
+
+            StringWriter buffer = new StringWriter();
+            PrintWriter body = new PrintWriter(buffer);
+            body.println("<html><head>" + metaRefresh() + "<title>" + escapeHTML(title) + "</title></head><body>");
+            body.println("<h3>" + escapeHTML(title) + "</h3>");
+            if (showBuffer) {
+                body.println("<pre>");
+                body.print(escapeHTML(getOutput()));
+                body.println("</pre>");
+            }
+            addButtonsHtml(context, body, confirmTitle, cancelTitle);
+            body.println("</body></html>");
+            body.close();
+            return buffer.toString();
+        } catch (IOException ioe) {
+            context.logError("Submitting", ioe);
+            return "Error submitting.";
+        }
+    }
+
+    public boolean doWork(PrintStream out) throws Exception {
+        if (mArchiveManager.getPrivateSSK() == null) {
+            out.println("The private key isn't set.");
+            return false;
+        }
+
+        try {
+            out.println("Inserting. Please be patient...");
+            String requestUri = mArchiveManager.pushToFreenet(out);
+            out.println("Inserted: " + requestUri);
+            return true;
+        } catch (IOException ioe) {
+            out.println("Insert failed from background thread: " + ioe.getMessage());
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/child/WikiContainer.java b/src/fniki/wiki/child/WikiContainer.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/WikiContainer.java
@@ -0,0 +1,247 @@
+/* A UI subcomponent to display and edit wikitext.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static ys.wikiparser.Utils.*;
+
+import fniki.wiki.ChildContainer;
+import fniki.wiki.ChildContainerException;
+import fniki.wiki.Query;
+
+import wormarc.FileManifest;
+import fniki.wiki.FreenetWikiTextParser;
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.WikiApp;
+import fniki.wiki.WikiContext;
+import fniki.wiki.WikiTextStorage;
+
+public class WikiContainer implements ChildContainer {
+    private final static String ENCODING = "UTF-8";
+
+    public WikiContainer() {}
+
+    public String handle(WikiContext context) throws ChildContainerException {
+        try {
+            String action = context.getAction();
+            if (action.equals("finished")) {
+                // Hack: Ignore "finished".
+                // This happens when the user hits the back button and picks
+                // a link from a finished task page. e.g. changelog.
+                action = "view";
+            }
+
+            String title = context.getTitle();
+            Query query = context.getQuery();
+
+            if (action.equals("view")) {
+                return handleView(context, title);
+            } else if (action.equals("edit")) {
+                return handleEdit(context, title);
+            } else if (action.equals("delete")) {
+                return handleDelete(context, title);
+            } else if (action.equals("revert")) {
+                return handleRevert(context, title);
+            } else if (action.equals("save")) {
+                return handleSave(context, query);
+            } else  {
+                context.raiseAccessDenied("Couldn't work out query.");
+            }
+        } catch (IOException ioe) {
+            context.logError("WikiContainer.handle", ioe);
+            context.raiseServerError("Unexpected Error in WikiContainer.handle. Sorry :-(");
+        }
+        return "unreachable code";
+    }
+
+    private String handleView(WikiContext context, String name) throws IOException {
+        return getPageHtml(context, name);
+    }
+
+    private String handleEdit(WikiContext context, String name) throws IOException {
+        return getEditorHtml(context, name);
+    }
+
+    private String handleDelete(WikiContext context, String name) throws IOException {
+        if (context.getStorage().hasPage(name)) {
+            context.getStorage().deletePage(name);
+        }
+        // DCI: apply uniform style! add link to default page!
+        String html =  "<html><head><title>Delete Page</title></head><body>Deleted Page</body></html>";
+        return html;
+    }
+
+    private String handleRevert(WikiContext context, String name) throws ChildContainerException, IOException {
+        context.getStorage().revertLocalChange(name);
+        context.raiseRedirect(context.makeLink("/" + name), "Redirecting...");
+        return "unreachable code";
+    }
+
+    private String handleSave(WikiContext context, Query form) throws ChildContainerException, IOException {
+        // Name is included in the query data.
+        String name = form.get("savepage");
+        String wikiText = form.get("savetext");
+
+        if (name == null || wikiText == null) {
+            context.raiseAccessDenied("Couldn't parse parameters from POST.");
+        }
+
+        System.err.println("Writing: " + name);
+        context.getStorage().putPage(name, wikiText);
+        context.raiseRedirect(context.makeLink("/" + name), "Redirecting...");
+        return "unreachable code";
+    }
+
+    private String titleFromName(String name) {
+        // DCI: html escape
+        return name.replace("_", " "); // DCI: Much more to it?
+    }
+
+    private String getPageHtml(WikiContext context, String name) throws IOException {
+        StringBuilder buffer = new StringBuilder();
+        addHeader(name, buffer);
+        if (context.getStorage().hasPage(name)) {
+            buffer.append(renderXHTML(context, context.getStorage().getPage(name)));
+        } else {
+            buffer.append("Page doesn't exist in the wiki yet.");
+        }
+        addFooter(context, name, buffer);
+        return buffer.toString();
+    }
+
+    private void addHeader(String name, StringBuilder buffer) throws IOException {
+        buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+        buffer.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
+                      "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
+        buffer.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n");
+        buffer.append("<head><title>\n");
+        buffer.append(escapeHTML(titleFromName(name)));
+        buffer.append("</title>\n");
+        buffer.append("<style type=\"text/css\">div.indent{margin-left:20px;} " +
+                      "div.center{text-align:center;} " +
+                      "blockquote{margin-left:20px;background-color:#e0e0e0;} " +
+                      "span.underline{text-decoration:underline;}</style>\n");
+        buffer.append("</head>\n");
+        buffer.append("<body>\n");
+        buffer.append("<h1>\n");
+        buffer.append(escapeHTML(titleFromName(name)));
+        buffer.append("</h1><hr>\n");
+    }
+
+    private String makeLocalLink(WikiContext context, String name, String action, String label) {
+        String href = makeHref(context.makeLink(name), action, name, null, null);
+        return String.format("<a href=\"%s\">%s</a>", href, escapeHTML(label));
+    }
+
+    private void addFooter(WikiContext context, String name, StringBuilder buffer) throws IOException {
+        buffer.append("<hr>\n");
+        buffer.append("Parent Version:<br>");
+        // DCI: css class to make this smaller.
+        String version = context.getString("parent_uri", "None");
+        buffer.append(escapeHTML(version));
+        buffer.append("<hr>\n");
+
+        buffer.append(makeLocalLink(context, name, "edit", "Edit"));
+        buffer.append(" this page.<br>");
+
+        buffer.append(makeLocalLink(context, name, "delete", "Delete"));
+        buffer.append(" this page without confirmation!<br>");
+
+        if (context.getStorage().hasLocalChange(name)) {
+            buffer.append(makeLocalLink(context, name, "revert", "Revert"));
+            buffer.append(" local changes to this page without confirmation!<br>");
+        }
+
+        buffer.append(makeLocalLink(context, "fniki/submit", null, "Submit"));
+        buffer.append(" local changes. <br>");
+
+        buffer.append(makeLocalLink(context, "fniki/changelog", "confirm", "Show"));
+        buffer.append(" change history for this version. <br>");
+
+        buffer.append(makeLocalLink(context, "fniki/getversions", "confirm", "Discover"));
+        buffer.append(" other recent version.<br>");
+
+        buffer.append("</body></html>");
+    }
+
+    private String getEditorHtml(WikiContext context, String name) throws IOException {
+        StringBuilder buffer = new StringBuilder();
+        addHeader(name, buffer);
+
+        String href = makeHref(context.makeLink("/" +name),
+                               "save", null, null, null);
+
+
+        buffer.append("<form method=\"post\" action=\"" +
+                      href +
+                      "\" enctype=\"");
+
+        // IMPORTANT: Only multipart/form-data encoding works in plugins.
+        buffer.append(context.getString("form_encoding", "application/x-www-form-urlencoded"));
+
+        buffer.append("\" accept-charset=\"UTF-8\">\n");
+
+        buffer.append("<input type=hidden name=\"savepage\" value=\"");
+                               buffer.append(name); // DCI: percent escape? Ok for now because of name checks
+        buffer.append("\">\n");
+
+        buffer.append("<textarea wrap=\"virtual\" name=\"savetext\" rows=\"17\" cols=\"120\">\n");
+
+        if (context.getStorage().hasPage(name)) {
+            buffer.append(context.getStorage().getPage(name));
+        } else {
+            buffer.append("Page doesn't exist in the wiki yet.");
+        }
+
+        buffer.append("</textarea>\n");
+        buffer.append("<br><input type=submit value=\"Save\">\n");
+        buffer.append("<input type=hidden name=formPassword value=\"");
+        // IMPORTANT: Required by Freenet Plugin.
+        buffer.append(context.getString("form_password", "FORM_PASSWORD_NOT_SET")); // DCI: % encode?
+        buffer.append("\"/>\n");
+        buffer.append("<input type=reset value=\"Reset\">\n");
+        buffer.append("<br></form>");
+
+        buffer.append("<hr>\n");
+        buffer.append("</body></html>\n");
+
+        return buffer.toString();
+    }
+
+    public String renderXHTML(WikiContext context, String wikiText) {
+        return new FreenetWikiTextParser(wikiText, context.getParserDelegate()).toString();
+    }
+}