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>*sTGs=&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`)OJAT$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 zIJ78yJkurRkGFmB;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<54pvufl)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{~8duPAIBcMOL+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$qbFrr_(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+Zk-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{yQCeYs6$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~uj-)`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{N7yYpER<~ 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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 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 Ӓ} 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(); + } +}