diff --git a/ConnectionManager.iml b/ConnectionManager.iml new file mode 100644 index 0000000..4a5435f --- /dev/null +++ b/ConnectionManager.iml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ConnectionManager.ipr b/ConnectionManager.ipr new file mode 100644 index 0000000..81a34f2 --- /dev/null +++ b/ConnectionManager.ipr @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ConnectionManager.iws b/ConnectionManager.iws new file mode 100644 index 0000000..8cc2468 --- /dev/null +++ b/ConnectionManager.iws @@ -0,0 +1,634 @@ + + + + + + + + + + 5000 + false + + 2000 + + true + true + + 65535 + + true + 1.3 + -341816 + true + -65536 + -256 + false + true + + + true + true + true + -32 + true + false + 0 + 0 + false + -16711936 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.xml b/build/build.xml new file mode 100644 index 0000000..0707823 --- /dev/null +++ b/build/build.xml @@ -0,0 +1,641 @@ + + + + + + + + + + Connection Manager build script. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ensure that you have run ant jar! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Connection Manager ${version} Javadoc]]> +
Connection Manager ${version} Javadoc]]>
+ Copyright © 2003-2006 Jive Software.]]> + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/build/installer/images/service-16x16.png b/build/installer/images/service-16x16.png new file mode 100644 index 0000000..5242fdf --- /dev/null +++ b/build/installer/images/service-16x16.png Binary files differ diff --git a/build/installer/images/service-32x32.png b/build/installer/images/service-32x32.png new file mode 100644 index 0000000..f11e984 --- /dev/null +++ b/build/installer/images/service-32x32.png Binary files differ diff --git a/build/installer/images/service.ico b/build/installer/images/service.ico new file mode 100644 index 0000000..1a00a63 --- /dev/null +++ b/build/installer/images/service.ico Binary files differ diff --git a/build/installer/images/splash.gif b/build/installer/images/splash.gif new file mode 100644 index 0000000..47ad7e2 --- /dev/null +++ b/build/installer/images/splash.gif Binary files differ diff --git a/build/installer/images/splash2.gif b/build/installer/images/splash2.gif new file mode 100644 index 0000000..1219fa3 --- /dev/null +++ b/build/installer/images/splash2.gif Binary files differ diff --git a/build/installer/images/wildfire-16x16.png b/build/installer/images/wildfire-16x16.png new file mode 100644 index 0000000..9e42542 --- /dev/null +++ b/build/installer/images/wildfire-16x16.png Binary files differ diff --git a/build/installer/images/wildfire-32x32.png b/build/installer/images/wildfire-32x32.png new file mode 100644 index 0000000..5437101 --- /dev/null +++ b/build/installer/images/wildfire-32x32.png Binary files differ diff --git a/build/installer/images/wildfire.ico b/build/installer/images/wildfire.ico new file mode 100644 index 0000000..5e4676d --- /dev/null +++ b/build/installer/images/wildfire.ico Binary files differ diff --git a/build/installer/images/wildfire_logo_small.png b/build/installer/images/wildfire_logo_small.png new file mode 100644 index 0000000..c33e322 --- /dev/null +++ b/build/installer/images/wildfire_logo_small.png Binary files differ diff --git a/build/installer/images/wildfire_off-16x16.gif b/build/installer/images/wildfire_off-16x16.gif new file mode 100644 index 0000000..fd9cb6d --- /dev/null +++ b/build/installer/images/wildfire_off-16x16.gif Binary files differ diff --git a/build/installer/images/wildfire_on-16x16.gif b/build/installer/images/wildfire_on-16x16.gif new file mode 100644 index 0000000..d53fd6d --- /dev/null +++ b/build/installer/images/wildfire_on-16x16.gif Binary files differ diff --git a/build/installer/images/wildfired-16x16.png b/build/installer/images/wildfired-16x16.png new file mode 100644 index 0000000..19c6127 --- /dev/null +++ b/build/installer/images/wildfired-16x16.png Binary files differ diff --git a/build/installer/images/wildfired-32x32.png b/build/installer/images/wildfired-32x32.png new file mode 100644 index 0000000..76b8121 --- /dev/null +++ b/build/installer/images/wildfired-32x32.png Binary files differ diff --git a/build/installer/images/wildfired.ico b/build/installer/images/wildfired.ico new file mode 100644 index 0000000..635fa32 --- /dev/null +++ b/build/installer/images/wildfired.ico Binary files differ diff --git a/build/lib/ant-contrib.jar b/build/lib/ant-contrib.jar new file mode 100644 index 0000000..db90b0a --- /dev/null +++ b/build/lib/ant-contrib.jar Binary files differ diff --git a/build/lib/ant-subdirtask.jar b/build/lib/ant-subdirtask.jar new file mode 100644 index 0000000..e5405b7 --- /dev/null +++ b/build/lib/ant-subdirtask.jar Binary files differ diff --git a/build/lib/ant.jar b/build/lib/ant.jar new file mode 100644 index 0000000..395544d --- /dev/null +++ b/build/lib/ant.jar Binary files differ diff --git a/build/lib/commons-el.jar b/build/lib/commons-el.jar new file mode 100644 index 0000000..608ed79 --- /dev/null +++ b/build/lib/commons-el.jar Binary files differ diff --git a/build/lib/dist/bouncycastle.jar b/build/lib/dist/bouncycastle.jar new file mode 100644 index 0000000..4554ce5 --- /dev/null +++ b/build/lib/dist/bouncycastle.jar Binary files differ diff --git a/build/lib/dist/jdic.jar b/build/lib/dist/jdic.jar new file mode 100644 index 0000000..ab9dbb2 --- /dev/null +++ b/build/lib/dist/jdic.jar Binary files differ diff --git a/build/lib/dist/tray.dll b/build/lib/dist/tray.dll new file mode 100644 index 0000000..2438f61 --- /dev/null +++ b/build/lib/dist/tray.dll Binary files differ diff --git a/build/lib/i4jruntime.jar b/build/lib/i4jruntime.jar new file mode 100644 index 0000000..ed00e2b --- /dev/null +++ b/build/lib/i4jruntime.jar Binary files differ diff --git a/build/lib/junit.jar b/build/lib/junit.jar new file mode 100644 index 0000000..674d71e --- /dev/null +++ b/build/lib/junit.jar Binary files differ diff --git a/build/lib/merge/dom4j.jar b/build/lib/merge/dom4j.jar new file mode 100644 index 0000000..c8c4dbb --- /dev/null +++ b/build/lib/merge/dom4j.jar Binary files differ diff --git a/build/lib/merge/jzlib.jar b/build/lib/merge/jzlib.jar new file mode 100644 index 0000000..fd37771 --- /dev/null +++ b/build/lib/merge/jzlib.jar Binary files differ diff --git a/build/lib/merge/xpp3.jar b/build/lib/merge/xpp3.jar new file mode 100644 index 0000000..c9822e6 --- /dev/null +++ b/build/lib/merge/xpp3.jar Binary files differ diff --git a/build/lib/pack200task.jar b/build/lib/pack200task.jar new file mode 100644 index 0000000..80422e4 --- /dev/null +++ b/build/lib/pack200task.jar Binary files differ diff --git a/build/lib/versions.txt b/build/lib/versions.txt new file mode 100644 index 0000000..7267dfa --- /dev/null +++ b/build/lib/versions.txt @@ -0,0 +1,15 @@ +Name | Version +--------------------------------------------- +ant.jar | Jetty 5.1.10 +ant-contrib.jar | 1.0b1 +ant-subdirtask.jar | Revision 1.4 (CVS) +bouncycastle.jar | JDK 1.5, 133 (bcprov-jdk15-133.jar) +commons-el.jar | Jetty 5.1.10 +dom4j.jar | 1.6.1 +!jaxen.jar | 1.1 beta 4 (from DOM4J 1.6.1) +junit.jar | 3.8.1 +jdic.jar | 0.9.1 (for windows only) +jzlib.jar | 1.0.7 +pack200task.jar | August 5, 2004 +xmltask.jar | 1.11 +xpp3.jar | XPP_3 1.1.3.8 \ No newline at end of file diff --git a/build/lib/xmltask.jar b/build/lib/xmltask.jar new file mode 100644 index 0000000..07d38eb --- /dev/null +++ b/build/lib/xmltask.jar Binary files differ diff --git a/documentation/dist/LICENSE.html b/documentation/dist/LICENSE.html new file mode 100644 index 0000000..9b6a521 --- /dev/null +++ b/documentation/dist/LICENSE.html @@ -0,0 +1,357 @@ + + + + + Connection Manager License: GPL + + + + +
+Please see README.html for full licensing terms for Connection Manager.
+
+---------------------------------------------------------------------
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  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 Library 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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 Library General
+Public License instead of this License.
+
+ + + diff --git a/documentation/dist/README.html b/documentation/dist/README.html new file mode 100644 index 0000000..98c02d5 --- /dev/null +++ b/documentation/dist/README.html @@ -0,0 +1,124 @@ + + + + + Connection Manager Readme + + + + +
+Connection Manager Readme +
+ +

+ + + + + + + + +
version:@version@
released:@builddate@
+ +

+Thank you for downloading Connection Manager! +

+Connection Manager concentrates XMPP client connections and forwards their traffic +to an XMPP server. Start off by viewing the documentation +that can be found in the "documentation" directory included with this distribution. +

+Further information can be found on the +Wildfire website. + +

Changelog

+ +View the changelog for a list of changes since the +last release. + +

Support

+ +Free support is provided by the Wildfire community in the +online forums. + +

License Agreements

+ +The Connection Manager source code is governed by the GNU Public License (GPL), which +can be found in the LICENSE.html file in this distribution. +Connection Manager also contains Open Source software from third-parties. +Licensing terms for those components is specifically noted in the relevant source +files.

+ +Connection Manager contains icons and images licensed from INCORS GmbH. All other +images are owned by Jive Software. All icons and images in Connection Manager +are provided under the following license agreement: +

+License Agreement
+
+This is a legal agreement between You, the User of the Connection Manager application
+("The Software"), and Jive Software ("Jive Software"). By downloading the Software,
+you agree to be bound by the terms of this agreement.
+
+All ownership and copyright of the images and icons included in the Software
+distribution remain the property of Jive Software and INCORS GmbH. Jive Software
+grants to you a nonexclusive, non-sublicensable right to use the icons royalty-free
+as part of Connection Manager.
+
+You may not lease, license or sub-license the icons, or a subset of the icons,
+or any modified icons to any third party. You may not incorporate them into your
+own software or design products.
+
+All icon files are provided "As is" without warranties of merchantability and
+fitness for a particular purpose. You agree to hold Jive Software harmless for
+any result that may occur during the course of using the licensed icons.
+
+This License Agreement shall be governed and construed in accordance with the
+laws of Oregon. If any provision of this License Agreement is held to be
+unenforceable, this License Agreement will remain in effect with the provision
+omitted.
+
+ + + diff --git a/documentation/dist/changelog.html b/documentation/dist/changelog.html new file mode 100644 index 0000000..f0287c8 --- /dev/null +++ b/documentation/dist/changelog.html @@ -0,0 +1,84 @@ + + + + + Connection Manager Changelog + + + + +
+Connection Manager Changelog +
+ +

+3.0.0 -- June 29, 2006 +

+ +

+ + + + + \ No newline at end of file diff --git a/documentation/docs/images/launcher.png b/documentation/docs/images/launcher.png new file mode 100644 index 0000000..e8a27bf --- /dev/null +++ b/documentation/docs/images/launcher.png Binary files differ diff --git a/documentation/docs/images/windows_service.png b/documentation/docs/images/windows_service.png new file mode 100644 index 0000000..fb95f88 --- /dev/null +++ b/documentation/docs/images/windows_service.png Binary files differ diff --git a/documentation/docs/index.html b/documentation/docs/index.html new file mode 100644 index 0000000..2661c31 --- /dev/null +++ b/documentation/docs/index.html @@ -0,0 +1,54 @@ + + + + + Connection Manager: Overview - Jive Software + + + + + +

Connection Manager @version@

+ +

+Connection Manager concentrates XMPP client connections and +forwards their traffic to an XMPP server. +

+ +

Documentation:

+ + + +

Developer Documentation:

+ + + +

+An active support community for Connection Manager is available at +http://www.jivesoftware.org/forums/. +

+ + + diff --git a/documentation/docs/install-guide.html b/documentation/docs/install-guide.html new file mode 100644 index 0000000..29d88db --- /dev/null +++ b/documentation/docs/install-guide.html @@ -0,0 +1,170 @@ + + + + Jive Software Connection Manager Readme + + + +

Connection Manager Installation Guide

+ +

Connection Manager lets XMPP servers scale to a greater number +of concurrent users. This document will guide you through installing +Connection Manager as a standalone application. For a full list of +features and more information, please visit the +Wildfire website: http://www.jivesoftware.org/wildfire

+ +

Installation

+ +

Setup Overview

+

To complete the installation of Connection Manager, you'll need to +perform each of the following steps:

+
    +
  1. Setup - Configure properties to connect to XMPP server.
  2. +
  3. DNS Setup - Configure DNS to properly route traffic.
  4. +
+ +

This document also includes information on:

+ + + +

Files in the Distribution

+

The files in your distribution should be as follows (some +sub-directories omitted for brevity):

+
cmanager/
+ |- readme.html
+ |- license.html
+ |- conf/
+ |- bin/
+ |- lib/
+ |- resources/
+     |-security/
+ |- documentation/
+ +

+ +

+

Properties Setup

+

Properties are stored in conf/manager.xml. Only two properties are required to be +configured. The xmpp.domain property specifies the name of the target server +that clients want to connect to. The server and connection managers have to share a +common secret so that the server can let connection managers connect. Set the +xmpp.sharedSecret property with the same shared secret that the server is using. +

+ +

DNS Setup

+

XMPP requires that when clients or servers want to connect to another server a DNS +SRV lookup must be performed to get the real IP address and port to use to connect. +Currently Connection Managers only handle client-to-server traffic while server-to-server +will go directly to the server. You will need to configure the DNS server so that client traffic +is routed to Connection Managers. Depending on your architecture you may want to use a load +balancer in front of a set of connection managers or just configure local DNS servers to +redirect clients to a specific connection manager.
+

+ +
+ +

Running Connection Manager in Windows

+ +

Use the cmanager.bat batch file to start the connection manager. The file is located +in the bin/ directory of your Connection Manager installation. + +

Custom Parameters

+ +

Advanced users may wish to pass in parameters to the Java virtual machine (VM) to customize + the runtime environment of Connection Manager. You can do this by editing the bin/cmanager.bat + or bin/cmanager.sh files and configuring the JVM_SETTINGS variable accordingly. For example, to set + the minimum heap size to 512 MB and max VM heap size to 1024 MB, you'd use: + +

-Xms512m -Xmx1024m
+

+ +

Running Connection Manager in Linux/Unix

+ +You can start Connection Manager using the bin/cmanager script in your +Connection Manager installation: +

+
+ +You must make the ant script executable. From the build directory, type: +

+chmod u+x cmanager.sh + +

+

+

+# ./cmanager
+ + + + diff --git a/documentation/docs/licenses/LICENSE-commons-logging.txt b/documentation/docs/licenses/LICENSE-commons-logging.txt new file mode 100644 index 0000000..9b5a20b --- /dev/null +++ b/documentation/docs/licenses/LICENSE-commons-logging.txt @@ -0,0 +1,60 @@ +/* + * $Header$ + * $Revision: 505 $ + * $Date: 2004-11-22 04:50:40 -0300 (Mon, 22 Nov 2004) $ + * + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ diff --git a/documentation/docs/licenses/LICENSE-dom4j.txt b/documentation/docs/licenses/LICENSE-dom4j.txt new file mode 100644 index 0000000..ec9149a --- /dev/null +++ b/documentation/docs/licenses/LICENSE-dom4j.txt @@ -0,0 +1,45 @@ +/* + $Id: LICENSE-dom4j.txt 505 2004-11-22 07:50:40Z bill $ + + Copyright 2001-2004 (C) MetaStuff, Ltd. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - + http://www.dom4j.org + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + + */ diff --git a/documentation/docs/licenses/LICENSE-jaxen.txt b/documentation/docs/licenses/LICENSE-jaxen.txt new file mode 100644 index 0000000..cb7880d --- /dev/null +++ b/documentation/docs/licenses/LICENSE-jaxen.txt @@ -0,0 +1,45 @@ +/* + $Id: LICENSE-jaxen.txt 505 2004-11-22 07:50:40Z bill $ + + Copyright 2003 (C) The Werken Company. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "jaxen" must not be used to endorse or promote + products derived from this Software without prior written + permission of The Werken Company. For written permission, + please contact bob@werken.com. + + 4. Products derived from this Software may not be called "jaxen" + nor may "jaxen" appear in their names without prior written + permission of The Werken Company. "jaxen" is a registered + trademark of The Werken Company. + + 5. Due credit should be given to The Werken Company. + (http://jaxen.werken.com/). + + THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + + */ diff --git a/documentation/docs/licenses/LICENSE-xpp3.txt b/documentation/docs/licenses/LICENSE-xpp3.txt new file mode 100644 index 0000000..7145f96 --- /dev/null +++ b/documentation/docs/licenses/LICENSE-xpp3.txt @@ -0,0 +1,46 @@ +Indiana University Extreme! Lab Software License + +Version 1.1.1 + +Copyright (c) 2002 Extreme! Lab, Indiana University. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + +3. The end-user documentation included with the redistribution, if any, + must include the following acknowledgment: + + "This product includes software developed by the Indiana University + Extreme! Lab (http://www.extreme.indiana.edu/)." + + Alternately, this acknowledgment may appear in the software itself, + if and wherever such third-party acknowledgments normally appear. + +4. The names "Indiana Univeristy" and "Indiana Univeristy Extreme! Lab" +must not be used to endorse or promote products derived from this +software without prior written permission. For written permission, +please contact http://www.extreme.indiana.edu/. + +5. Products derived from this software may not use "Indiana Univeristy" +name nor may "Indiana Univeristy" appear in their name, without prior +written permission of the Indiana University. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS OR ITS CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/documentation/docs/source-build.html b/documentation/docs/source-build.html new file mode 100644 index 0000000..f82129d --- /dev/null +++ b/documentation/docs/source-build.html @@ -0,0 +1,256 @@ + + + + + Connection Manager Source Instructions + + + + + + +

Connection Manager Source Instructions

+

+ +This document provides detailed information for developers that wish to +compile and make changes to the Connection Manager source code. +Use of the source code is governed by the GPL or the commercial license +you purchased for the codebase from Jive Software. +If Connection Manager +source is embedded into another application, that application must also +be released under the Open Source GPL license (unless you have a commercial OEM license +in place with Jive Software). Please see the license agreement +for further details.. Some of the source code in this package is Open Source +software developed by third parties. Wherever this is the case, it is specifically noted in +the source, and that source is governed by the license given to it by its author. + + +

For additional developer resources, please visit: + +http://www.jivesoftware.org. The Connection Manager build process is based on Ant. Visit the +Ant website +for more information. There is no need to download and install Ant - a version of it is included +in this distribution. +

+This documentation is divided into three sections: +

    +
  1. Source -- get the Connection Manager source code. +
  2. Setup -- how to setup your environment for Connection Manager development. +
  3. Build tasks -- tasks that can be performed using the build program. +
+ +

1. Get the Connection Manager Source

+ +You can get the Connection Manager source code by downloading a source distribution or +by checking out the source code from CVS. Instructions for both options can +be found on the source page. + +

2. Setup Your Environment

+ +Getting your machine ready for development requires a few steps. Wherever +possible, instructions are provided for both Unix/Linux and Windows users. +

+Configure Java for Connection Manager +

+ +

Install the Ant Build Tool

+ +The Connection Manager build process uses Ant, so that tool must be installed +and configured on your computer. First download Ant from: +http://ant.apache.org. Next, follow +the installation instructions. + +

Test the Build Script

+

+ +

Finished!

+

+ +

+ +

3. Build Tasks

+ + The list of build tasks is below. All build commands should be + run from the "build" directory of your Connection Manager distribution. + +

+ + For a list of the commands and a brief description from the command line, type + ant -projecthelp. For more complete help on several commands, + read the documentation below. + +

+ + To execute a build task, type ant [options] targetname where "targetname" is + one of the targets listed below: + +

+

+Each task is documented with a syntax guide and description. Optional paramaters +for each task are enclosed with braces. If you would like to permanently set the +value of a property, add it to build/build.xml file. + + +

Default +

+ + + +

jar +

+ + +

clean +

+ + + diff --git a/documentation/docs/ssl-guide.html b/documentation/docs/ssl-guide.html new file mode 100644 index 0000000..ee5b551 --- /dev/null +++ b/documentation/docs/ssl-guide.html @@ -0,0 +1,224 @@ + + + + Connection Manager SSL Guide + + + + +

Connection Manager SSL Guide

+

Introduction

+

+This document outlines how to customize the SSL support in Connection Manager. +Important note: +because Connection Manager ships with self-signed certificates, it will work out of the box without +installing your own certificate. However, most users will wish to user their own +certificates.

+ +

Connection Manager is a component that lies between clients and an XMPP server. +Therefore, connections with clients may be secured as well as connections with the server. +This document explains how to secure each part of the architecture.

+ +

Connection Manager's SSL support is built using the standard Java security +SSL implementation (javax.net.ssl.SSLServerSocket). In this document, we will +describe how use the standard JDK 1.5 tools to accomplish these tasks. +

+

Background

+

+A server SSL connection uses two sets of certificates to secure the +connection. The first set is called a "keystore". The keystore contains +the keys and certificates for the connection manager. These security credentials +are used to prove to clients that the connection manager is legitimately operating +on behalf of a particular domain hosted by the server. You only need one key +entry and certificate in the keystore for each hosted domain in the server. +Keys are stored in the keystore under aliases. Each alias corresponds +to a domain name (e.g. "example.com"). +

+

+The second set of certificates is called the "truststore" and is used +to verify that a client is legitimately operating on behalf of a +particular user or to verify that a server is legitimately operating +on behalf of a particular domain. By default, the truststore is filled +with certificates provided by Java 1.5 plus root certificates of cacert.org. +

+

+Certificates attempt to guarantee that a particular party is who they +claim to be. Certificates are trusted based on who signed the certificate. +If you only require light security, are deploying for internal use on +trusted networks, etc. you can use "self-signed" certificates. +Self-signed certificates encrypts the communication channel between +client and server. However the client must verify the legitimacy of the +self-signed certificate through some other channel. The most common client +reaction to a self-signed certificate is to ask the user whether +to trust the certificate, or to silently trust the certificate is +legitimate. Unfortunately, blindly accepting self-signed certificates +opens up the system to 'man-in-the-middle' attacks. +

+

+The advantage of a self-signed certificate is you can create them for +free which is great when cost is a major concern, or for testing and evaluation. +In addition, you can safely use a self-signed certificate if you can verify +that the certificate you're using is legitimate. So if a system administrator +creates a self-signed certificate, then personally installs it on a client's +truststore (so that the certificate is trusted) you can be assured that +the SSL connection will only work between the client and the correct server. +

+

+For higher security deployments, you should get your certificate signed +by a certificate authority (CA). Clients truststores will usually contain +the certificates of the major CA's and can verify that a CA has signed a +certificate. This chain of trust allows clients to trust certificate from +servers they've never interacted with before. Certificate signing is similar +to a public notary (with equivalent amounts of verification of identity, +record keeping, and costs). +

+

Sun JDK 1.5 security tools

+

+The Sun JDK (version 1.5.x) ships with all the security tools you need +to configure SSL with Connection Manager. The most important is the +keytool located in the JAVA_HOME/bin directory of the +JDK. Sun JVMs persist keystores and truststores on the filesystem as +encrypted files. The keytool is used to create, read, update, +and delete entries in these files. Connection Manager ships with a self-signed +"dummy" certificate designed for initial evaluation testing. You will need +to adjust the default configuration for most deployments. +

+

+In order to configure SSL on your connection manager you need complete the +following tasks: +

+
    +
  1. Decide on your Wildfire server's public domain. This is the domain that +users will connect to.
  2. +
  3. Create a self-signed SSL server certificate for your server +domain. Note: you may already have one if your Wildfire server +domain matches an existing web domain with SSL. +If so, you can skip to step 4.
  4. +
  5. [Optional] Have a certificate authority (CA) certify the SSL +server certificate. +
      +
    1. Generate a certificate signing request (CSR).
    2. +
    3. Submit your CSR to a CA for signing.
    4. +
    +
  6. +
  7. Import the server certificate into the keystore. Note: if you are +going to use a self-signed certificate +generated in step 2, the certificate is already imported and you can +skip this step.
  8. +
  9. Remove default certificates from the keystore.
  10. +
  11. Import client certificates into the truststore.
  12. +
  13. Adjust the Connection Manager configuration with proper keystore and +truststore settings.
  14. +
+

1. Decide on a Server Domain

+

+The Wildfire server domain should match the host name of the connection manager; +for example, "example.com". Your user accounts will have addresses with +the format "user@example.com" like email addresses. We'll assume +the domain is "example.com" for the rest of the examples. +

+

2. Create a self-signed server certificate

+

+In order to create a self-signed server certificate go to the command +line and change directories to the resources/security +directory of your Wildfire installation. You should see the default +keystore and truststore files. First, you should +change the default keystore +password: +

+

keytool -storepasswd -keystore keystore

+

+keytool will ask for the old password (by default it is changeit) +then the new password. +Now we'll create a certificate using the keytool: +

+

keytool -genkey -keystore keystore -alias example.com

+

+where you should substitute your server's name for example.com. +The keytool will ask for the store password, then several pieces of +information required for the certificate. Enter all the information but remember +to complete with your server's name when asked for your first and last name. +After you have entered all the required information, keytool will ask you to +verify the information and set a key password. +You must use the same key password as the store password. By default +you get this by simply hitting 'enter' when prompted for a key password.

+

If you later change the keystore password remember to change the entries' +password as well using the keytool:

+ +

keytool -keypasswd -alias example.com -keystore keystore +

+

3. Obtain a CA signed certificate

+

+If you decide to get a CA signed certificate, you must first export the +certificate in the +standard CSR format. You can do this with the keytool: +

+

keytool -certreq -keystore keystore -alias example.com -file +certificate_file

+

+Where you should substitute your server's name for example.com +and the name of the +certificate file you wish to produce for certificate_file. +Submit the generated CSR to the CA and follow their instructions to get +it signed. +

+

4. Import server certificates

+

+If you had a CA sign your server certificate, or if you have an +existing SSL certificate, +you must import it using the keytool. +

+

keytool -import -keystore keystore -alias example.com -file +signed_certificate_file

+

+It is important that the alias not already have an associated key or +you'll receive an error. +

+

5. Remove default certificates

+

+After importing your certificate you must remove the default certificates +using the keytool. +

+

keytool -delete -keystore keystore -alias rsa

+

keytool -delete -keystore keystore -alias dsa

+

6. Import client certificates

+

+If you require clients to verify themselves using certificates, obtain +their certificates and import them into the truststore file rather than +the keystore. First, you should change the default truststore +password: +

+

keytool -storepasswd -keystore truststore

+

+keytool will ask for the old password (by default it is changeit) +then the new password. +Now import each certificate using the keytool: +

+

keytool -import -keystore truststore -alias user_name -file +certificate_file

+

7. Configure Connection Manager

+

+Open the conf/manager.xml file in your favorite +editor and add or change the following system properties: +

+ +You will need to restart the connection manager after you have modified any of the above system properties. + + diff --git a/documentation/docs/style.css b/documentation/docs/style.css new file mode 100644 index 0000000..0833d68 --- /dev/null +++ b/documentation/docs/style.css @@ -0,0 +1,117 @@ +BODY { + font-size : 100%; + background-color : #fff; +} +BODY, TD, TH { + font-family : arial, helvetica, sans-serif; + font-size : 10pt; +} +PRE, TT, CODE { + font-family : courier new, monospaced; + font-size : 9pt; +} +A:hover { + text-decoration : none; +} +LI { + padding-bottom : 4px; +} +H1 { + font-size : 1.4em; + font-weight : bold; + width : 100%; + border-bottom : 1px #ccc solid; + padding-bottom : 2px; +} +H2 { + font-size : 1.2em; + font-weight : bold; +} +H3 { + font-size : 1.0em; + font-weight : bold; +} +TT { + font-family : courier new; + font-weight : bold; + color : #060; +} +FIELDSET PRE { + padding : 1em; + margin : 0px; +} +FIELDSET { + margin-left : 2em; + margin-right : 2em; + border : 1px #ccc solid; + -moz-border-radius : 5px; +} +.comment { + color : #666; + font-style : italic; +} + +.subheader { + font-weight : bold; +} +.footer { + font-size : 0.8em; + color : #999; + text-align : center; + width : 100%; + border-top : 1px #ccc solid; + padding-top : 2px; +} +.code { + border : 1px #ccc solid; + padding : 0em 1.0em 0em 1.0em; + margin : 4px 0px 4px 0px; +} +.nav, .nav A { + font-family : verdana; + font-size : 0.85em; + color : #600; + text-decoration : none; + font-weight : bold; +} +.note { + font-family : verdana; + font-size : 0.85em; + color : #600; + text-decoration : none; + font-weight : bold; +} +.nav { + width : 100%; + border-bottom : 1px #ccc solid; + padding : 3px 3px 5px 1px; +} +.nav A:hover { + text-decoration : underline; +}.question { + font-weight: 600; +} +.answer { + font-weight: 300; +} +.toc { + right: 5px; +} +TABLE.dbtable { + border : 1px #ccc solid; + width : 600px; +} +TR, TH { + border-bottom : 1px #ccc solid; +} +TH, TD { + padding-right : 15px; +} +TH { + text-align : left; + white-space : nowrap; + background-color : #eee; +} +.primary-key { + background-color : #ffc; +} diff --git a/documentation/docs/translator-guide.html b/documentation/docs/translator-guide.html new file mode 100644 index 0000000..be3064e --- /dev/null +++ b/documentation/docs/translator-guide.html @@ -0,0 +1,116 @@ + + + + + Connection Manager Translator Guide + + + + + +

Connection Manager Translator Guide

+ + + +

Introduction

+ +

+Connection Manager includes administration and error messages that can be easily translated +into other languages. This document provides instructions for those that wish to make translations. +

+

+Messages are stored in a resource bundle. A resource bundle is a file containing key/value pairs. +Words and phrases are represented using keys. The correct values are retrieved based on +locale settings (English values are used for English locales, French values for French +locales, etc). Key/value pairs in the English resource bundle might look like the following: +

+    skin.yes=Yes
+    skin.no=No
+    skin.topic=Topic
+    skin.message=Message
+
+ +The German resource bundle would contain the same keys, but different values: +
+
+    skin.yes=Ja
+    skin.no=Nein
+    skin.topic=Thema
+    skin.message=Beitrag
+
+ +Making your own translation involves copying the English resource bundle, +renaming the file, then translating its contents.

+ +

Construct a Resource Bundle

+ +

To start, make a copy of the default (English) locale file "cmanager_i18n.properties". +It can be found in the resources\i18n directory of your Connection Manager +installation. Note: the files found in resources\i18n are copies of the +real resource bundles used by the application (the real resource bundles are contained +in the cmanager.jar file). Editing the resource files in the resources\i18n +directory will not affect your running copy of Connection Manager.

+ +

Next, you'll need to rename the file to match the locale that you're making a +translation for. The syntax of the name is "cmanager_i18n_[lang]_[country].properties". +However, the country code should be used in most cases. For example, the German resource +bundle should be named "cmanager_i18n_de.properties" because +"de" is the language code for German. For French, the file would be called +"cmanager_i18n_fr.properties". A list of language codes can +be found at: +http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt. + Some locales require a combination of language and country code. For example + simplified Chinese would have the name "cmanager_i18n_zh_CN.properties" + while traditional Chinese would have the name "cmanager_i18n_zh_TW.properties".

+ +

Translate the Resource Bundle

+ +

Next, use your favorite text editor to translate the English values into +your language. The key names must not be changed.

+ +

When translating from English, you may need to use special characters +for your language (for example, Germans use characters like ä, +ü, or ß). Unfortunately, all resource bundle files must be saved +in ASCII format which doesn't allow for international characters. We +recommend working on your translation in a text editor +that supports all characters in your language. After finishing your translation, +use the "native2ascii" tool (bundled with Java) to convert international +characters to the ASCII format. To use the native2ascii tool:

+ +
+    native2ascii -encoding XXX my_translation.properties cmanager_i18n_YY.properties
+                               ^                         ^
+                               input file                output file
+
+ +

The "-encoding XXX" parameter is optional. If you don't specify it, Java will +use the default encoding value, taken from System property "file.encoding". +If you do specify an encoding (XXX), it must be taken from the first column of +the table of supported encodings in the +Supported Encodings +document. For example, if you created your translation using UTF-8 as your encoding and +you are making a Simplified Chinese translation:

+ +
+    native2ascii -encoding UTF8 my_translation.properties cmanager_i18n_zh_CN.properties
+ 
+ +

Testing Your Translation

+ +

To test your translation, copy the translated resource bundle file (example, +cmanager_i18n_de.properties) to the lib/ directory of your Connection Manager +installation. Make sure Connection Manager is stopped and then edit the conf/manager.xml +file. Set the locale property to match your new resource +bundle such as "de" or "zh_CN". Start Connection Manager and the messages +should now be using your translation. If you still see English text you may have +named your bundle incorrectly or used the wrong value for the locale +property.

+ +

Once your translation is complete and tested, please submit it to the Connection Manager +developers so that others can enjoy it in the next release!

+ +

+ + + diff --git a/src/bin/cmanager.bat b/src/bin/cmanager.bat new file mode 100644 index 0000000..55f1799 --- /dev/null +++ b/src/bin/cmanager.bat @@ -0,0 +1,32 @@ +@echo off + +REM # +REM # $RCSfile$ +REM # $Revision: 1102 $ +REM # $Date: 2005-03-07 22:36:48 -0300 (Mon, 07 Mar 2005) $ +REM # + +REM # JVM_SETTINGS = "-Xms512m -Xmx1024m" +JVM_SETTINGS = "" + +if "%JAVA_HOME%" == "" goto javaerror +if not exist "%JAVA_HOME%\bin\java.exe" goto javaerror +goto run + +:javaerror +echo. +echo Error: JAVA_HOME environment variable not set, Connection Manager not started. +echo. +goto end + +:run +if "%1" == "-debug" goto debug +start "Connection Manager" "%JAVA_HOME%\bin\java" -server %JVM_SETTINGS% -jar ..\lib\startup.jar +goto end + +:debug +start "Connection Manager" "%JAVA_HOME%\bin\java" %JVM_SETTINGS% -Xdebug -Xint -server -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar ..\lib\startup.jar +goto end +:end + + diff --git a/src/bin/cmanager.sh b/src/bin/cmanager.sh new file mode 100644 index 0000000..b8f5572 --- /dev/null +++ b/src/bin/cmanager.sh @@ -0,0 +1,151 @@ +#!/bin/sh + +# +# $RCSfile$ +# $Revision: 1194 $ +# $Date: 2005-03-30 13:39:54 -0300 (Wed, 30 Mar 2005) $ +# + +# tries to determine arguments to launch Connection Manager + +# Set JVM extra Setting +# JVM_SETTINGS="-Xms512m -Xmx1024m" +JVM_SETTINGS="" + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +case "`uname`" in + CYGWIN*) cygwin=true ;; + Darwin*) darwin=true + if [ -z "$JAVA_HOME" ] ; then + JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Home + fi + ;; +esac + +#if cmanager home is not set or is not a directory +if [ -z "$CMANAGER_HOME" -o ! -d "$CMANAGER_HOME" ]; then + + if [ -d /opt/cmanager ] ; then + CMANAGER_HOME="/opt/cmanager" + fi + + if [ -d /usr/local/cmanager ] ; then + CMANAGER_HOME="/usr/local/cmanager" + fi + + if [ -d ${HOME}/opt/cmanager ] ; then + CMANAGER_HOME="${HOME}/opt/cmanager" + fi + + #resolve links - $0 may be a link in cmanager's home + PRG="0" + progname=`basename "$0$"` + + # need this for relative symlinks + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi + done + + #assumes we are in the bin directory + CMANAGER_HOME=`dirname "$PRG"`/.. + + #make it fully qualified + CMANAGER_HOME=`cd "$CMANAGER_HOME" && pwd` +fi +CMANAGER_OPTS="${CMANAGER_OPTS} -DmanagerHome=${CMANAGER_HOME}" + + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$CMANAGER_HOME" ] && + CMANAGER_HOME=`cygpath --unix "$CMANAGER_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +#set the CMANAGER_LIB location +CMANAGER_LIB="${CMANAGER_HOME}/lib" +CMANAGER_OPTS="${CMANAGER_OPTS} -Dcmanager.lib.dir=${CMANAGER_LIB}" + + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD=`which java 2> /dev/null ` + if [ -z "$JAVACMD" ] ; then + JAVACMD=java + fi + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." + echo " We cannot execute $JAVACMD" + exit 1 +fi + +if [ -z "$LOCALCLASSPATH" ] ; then + LOCALCLASSPATH=$CMANAGER_LIB/startup.jar +else + LOCALCLASSPATH=$CMANAGER_LIB/startup.jar:$LOCALCLASSPATH +fi + +# For Cygwin, switch paths to appropriate format before running java +if $cygwin; then + if [ "$OS" = "Windows_NT" ] && cygpath -m .>/dev/null 2>/dev/null ; then + format=mixed + else + format=windows + fi + CMANAGER_HOME=`cygpath --$format "$CMANAGER_HOME"` + CMANAGER_LIB=`cygpath --$format "$CMANAGER_LIB"` + JAVA_HOME=`cygpath --$format "$JAVA_HOME"` + LOCALCLASSPATH=`cygpath --path --$format "$LOCALCLASSPATH"` + if [ -n "$CLASSPATH" ] ; then + CLASSPATH=`cygpath --path --$format "$CLASSPATH"` + fi + CYGHOME=`cygpath --$format "$HOME"` +fi + +# add a second backslash to variables terminated by a backslash under cygwin +if $cygwin; then + case "$CMANAGER_HOME" in + *\\ ) + CMANAGER_HOME="$CMANAGER_HOME\\" + ;; + esac + case "$CYGHOME" in + *\\ ) + CYGHOME="$CYGHOME\\" + ;; + esac + case "$LOCALCLASSPATH" in + *\\ ) + LOCALCLASSPATH="$LOCALCLASSPATH\\" + ;; + esac + case "$CLASSPATH" in + *\\ ) + CLASSPATH="$CLASSPATH\\" + ;; + esac +fi + +cmanager_exec_command="exec \"$JAVACMD\" -server $JVM_SETTINGS $CMANAGER_OPTS -classpath \"$LOCALCLASSPATH\" -jar \"$CMANAGER_LIB\"/startup.jar" +eval $cmanager_exec_command diff --git a/src/bin/extra/cmanagerd b/src/bin/extra/cmanagerd new file mode 100644 index 0000000..674205a --- /dev/null +++ b/src/bin/extra/cmanagerd @@ -0,0 +1,80 @@ +#!/bin/sh + +# cmanagerd +# +# chkconfig: 2345 20 80 +# description: Used to start and stop the Connection Manager service +# +# Script used to start Connection Manager as daemon +# The script has currently been tested on Redhat Fedora Core 3, +# but should theoretically work on most UNIX like systems +# +# before running this script make sure $CMANAGER_HOME/bin/cmanager is +# executable by the user you want to run Connection Manager as +# (chmod +x $CMANAGER_HOME/bin/cmanager) +# +# This script should be copied into /etc/init.d and linked into +# your default runlevel directory. +# You can find your default runlevel directory by typing: +# grep default /etc/inittab +# +# Link to the directory like follows +# cd /etc/rc.d +# ln -s ../init.d/cmanagerd $90cmanagerd +# + +# Set this to tell this script where Connection Manager lives +# If this is not set the script will look for /opt/cmanager, then /usr/local/cmanager +#export CMANAGER_HOME= + +# If there is a different user you would like to run this script as, +# change the following line +export CMANAGER_USER=jive + +# ----------------------------------------------------------------- + +# If a Connection Manager home variable has not been specified, try to determine it +if [ ! $CMANAGER_HOME ]; then + if [ -d "/opt/cmanager" ]; then + CMANAGER_HOME="/opt/cmanager" + elif [ -d "/usr/local/cmanager" ]; then + CMANAGER_HOME="/usr/local/cmanager" + else + echo "Could not find Connection Manager installation under /opt or /usr/local" + echo "Please specify the Connection Manager installation location in environment variable CMANAGER_HOME" + exit 1 + fi +fi + + +function execCommand() { + OLD_PWD=`pwd` + cd $CMANAGER_HOME/bin + CMD="./cmanager $1" + su -c "$CMD" $CMANAGER_USER & + cd $OLD_PWD +} + + +start() { + execCommand "start" +} + +stop() { + execCommand "stop" +} + + +case "$1" in + start) + start + ;; + stop) + stop + ;; + *) + echo "Usage $0 {start|stop}" + exit 1 +esac + +exit 0 diff --git a/src/bin/extra/redhat-postinstall.sh b/src/bin/extra/redhat-postinstall.sh new file mode 100644 index 0000000..1fecbcf --- /dev/null +++ b/src/bin/extra/redhat-postinstall.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# redhat-poinstall.sh +# +# This script sets permissions on the Connection Manager installtion +# and install the init script. +# +# Run this script as root after installation of Connection Manager +# It is expected that you are executing this script from the bin directory + +# If you used an non standard directory name of location +# Please specify it here +# CMANAGER_HOME= + +CMANAGER_USER="jive" +CMANAGER_GROUP="jive" + +if [ ! $CMANAGER_HOME ]; then + if [ -d "/opt/cmanager" ]; then + CMANAGER_HOME="/opt/cmanager" + elif [ -d "/usr/local/cmanager" ]; then + CMANAGER_HOME="/usr/local/cmanager" + fi +fi + +# Grant execution permissions +chmod +x $CMANAGER_HOME/bin/extra/cmanagerd + +# Install the init script +cp $CMANAGER_HOME/bin/extra/cmanagerd /etc/init.d +/sbin/chkconfig --add cmanagerd +/sbin/chkconfig cmanagerd on + +# Create the jive user and group +/usr/sbin/groupadd $CMANAGER_GROUP +/usr/sbin/useradd $CMANAGER_USER -g $CMANAGER_GROUP -s /bin/bash + +# Change the permissions on the installtion directory +/bin/chown -R $CMANAGER_USER:$CMANAGER_GROUP $CMANAGER_HOME diff --git a/src/conf/manager.xml b/src/conf/manager.xml new file mode 100644 index 0000000..28c7fe7 --- /dev/null +++ b/src/conf/manager.xml @@ -0,0 +1,76 @@ + + + + + + + + + 5262 + + + + + + + 5 + + + 5 + + + + + + true + + + true + + true + + true + + + false + + + + + 1800000 + + + + true + 5223 + jks + + + + + + + + false + + + \ No newline at end of file diff --git a/src/i18n/cmanager_i18n_cs_CZ.properties b/src/i18n/cmanager_i18n_cs_CZ.properties new file mode 100644 index 0000000..4d44b1b --- /dev/null +++ b/src/i18n/cmanager_i18n_cs_CZ.properties @@ -0,0 +1,55 @@ +# $RCSfile$ +# $Revision: $ +# $Date: $ + +## +## Connection Manager Resource Bundle - Czech locale (cs_CZ) +## Translation by Tomas Pavlicek +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Zna\u010dka vlo\u017eena u\u017eivatelem {0} - {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Spu\u0161t\u011bn standardn\u00ed (ne\u0161ifrovan\u00fd) socket na portu: {0} +startup.ssl=Spu\u0161t\u011bn SSL (\u0161ifrovan\u00fd) socket na portu: {0} +startup.error=Chyba p\u0159i startu serveru. Pros\u00edm zkontrolujte protokoly pro z\u00edsk\u00e1n\u00ed v\u00edce informac\u00ed. +startup.error.jivehome=Nelze naj\u00edt managerHome. Nastavte vlastnost managerHome nebo upravte \ + v\u00e1\u0161 soubor manager_init.xml tak, aby odpov\u00eddal nasazen\u00ed serveru. + +# Standard server error messages (for server admin) + +admin.error=Intern\u00ed chyba serveru +admin.error.accept=Pot\u00ed\u017ee p\u0159i p\u0159\u00edjmu spojen\u00ed +admin.error.bad-stream=Chybn\u00e1 otev\u00edrac\u00ed zna\u010dka (nejedn\u00e1 se o stream) +admin.error.bad-namespace=Stream nen\u00ed ve spr\u00e1vn\u00e9m jmenn\u00e9m prostoru +admin.error.channel-notfound=Kan\u00e1l {0} nemohl b\u00fdt nalezen +admin.error.close=Nelze zav\u0159\u00edt socket +admin.error.connection=Spojen\u00ed uzav\u0159eno p\u0159ed sestaven\u00edm relace +admin.error.deliver=Nelze doru\u010dit paket +admin.error.min-thread=Nelze nastavit minim\u00e1ln\u00ed po\u010det vl\u00e1ken neplatnou hodnotou. +admin.error.max-thread=Nelze nastavit maxim\u00e1ln\u00ed po\u010det vl\u00e1ken neplatnou hodnotou. +admin.error.packet=P\u0159ijat po\u0161kozen\u00fd paket +admin.error.packet.text=Ve streamu je neo\u010dek\u00e1van\u00fd prost\u00fd text +admin.error.packet.tag=Neo\u010dek\u00e1van\u00e1 zna\u010dka paketu (nejedn\u00e1 se o message,iq,presence) +admin.error.routing=Nelze sm\u011brovat paket +admin.error.socket-setup=Nelze nastavit socket serveru +admin.error.ssl=Nelze nastavit SSL socket +admin.error.stream=Detekov\u00e1na chyba streamu +admin.drop-packet=Zahozen nerozpoznan\u00fd paket +admin.disconnect=Stream zkr\u00e1cen\u011b ukon\u010den (mohl b\u00fdt ukon\u010den norm\u00e1ln\u011b) + +# Setup + +setup.index.unable_locate_dir=Nenalezen platn\u00fd adres\u00e1\u0159 conf. Pros\u00edm pod\u00edvejte se do instala\u010dn\u00ed \ + dokumentace pro spr\u00e1vn\u00fd zp\u016fsob, jak nastavit conf adres\u00e1\u0159. diff --git a/src/i18n/cmanager_i18n_de.properties b/src/i18n/cmanager_i18n_de.properties new file mode 100644 index 0000000..4c8a82e --- /dev/null +++ b/src/i18n/cmanager_i18n_de.properties @@ -0,0 +1,56 @@ +# $RCSfile: cmanager_i18n_de.properties,v $ +# $Revision: 3091 $ +# $Date: 2005-11-16 18:36:03 -0300 (Wed, 16 Nov 2005) $ + +## +## Connection Manager Resource Bundle - German locale (de) +## Translation by Frank Niedermann +## ae=\u00e4 / ue=\u00fc / oe=\u00f6 / Ue=\u00dc / Ae=\u00c4 +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Markerung eingef\u00fcgt von {0} am {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Plain-Instanz (unverschl\u00fcsselt) auf Port {0} gestartet +startup.ssl=SSL Socket (verschl\u00fcsselt) auf Port {0} gestartet +startup.error=Fehler beim Starten des Servers. Bitte die Logdateien nach mehr Informationen \u00fcberpr\u00fcfen. +startup.error.jivehome=Kann managerHome nicht auffinden. Die managerHome-Einstellung setzen oder \ + die manager_init.xml Datei f\u00fcr Bereitstellungen auf Applikationsservern bearbeiten. + +# Standard server error messages (for server admin) + +admin.error=Interner Serverfehler +admin.error.accept=Probleme beim Annehmen der Verbindungen +admin.error.bad-stream=Falsche Anfangskennzeichnung (kein Stream) +admin.error.bad-namespace=Stream nicht in richtigem Namensraum +admin.error.channel-notfound=Kanal {0} konnte nicht gefunden werden +admin.error.close=Socket konnte nicht geschlossen werden +admin.error.connection=Verbindung beendet bevor Sitzung hergestellt wurde +admin.error.deliver=Konnte Paket nicht ausliefern +admin.error.min-thread=Kann die minimale Anzahl der Threads nicht mit einer ung\u00fcltigen Einstellung setzen +admin.error.max-thread=Kann die maximale Anzahl der Threads nicht mit einer ung\u00fcltigen Einstellung setzen +admin.error.packet=Missgebildetes Paket erhalten +admin.error.packet.text=Unerwartet unbearbeiteter Text im Stream +admin.error.packet.tag=Unerwartete Paketkennzeichnung (nicht Nachricht,IQ,Pr\u00e4senz) +admin.error.routing=Kann das Paket nicht weiterleiten/routen +admin.error.socket-setup=Kann kein Server-Socket einrichten +admin.error.ssl=Kann SSL-Socket nicht einrichten +admin.error.stream=Stream-Fehler entdeckt +admin.drop-packet=Unbekanntes Paket verworfen +admin.disconnect=Stream-Abschnitt kurz (k\u00f6nnte normale Trennung sein) + +# Setup + +setup.index.unable_locate_dir=G\u00fcltiges conf-Verzeichnis konnte nicht gefunden werden. F\u00fcr die korrekte \ + Art das conf-Verzeichnis einzustellen bitte auf die Installationsdokumentation beziehen. diff --git a/src/i18n/cmanager_i18n_en.properties b/src/i18n/cmanager_i18n_en.properties new file mode 100644 index 0000000..8f9015a --- /dev/null +++ b/src/i18n/cmanager_i18n_en.properties @@ -0,0 +1,78 @@ +# $RCSfile$ +# $Revision: 3148 $ +# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $ + +## +## Connection Manager Resource Bundle +## +## Additional locales can be specified by creating a new resource file in this +## directory using the following conventions: +## +## cmanager_i18n "_" language "_" country ".properties" +## cmanager_i18n "_" language ".properties" +## +## e.g. +## cmanager_i18n_en.propertis <- English resources +## cmanager_i18n_en_US.properties <- American US resources +## cmanager_i18n_de.properties <- German resources +## cmanager_i18n_ja.properties <- Japanese resources +## +## Please note that the two digit language code should be lower case, and the +## two digit country code should be in uppercase. Often, it is not necessary to +## specify the country code. +## +## A full list of language codes can be found at +## http://www-old.ics.uci.edu/pub/ietf/http/related/iso639.txt +## and a full list of country codes can be found at +## http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html +## +## In property strings that are parameterized, single quotes can be used to +## quote the "{" (curly brace) if necessary. A real single quote is represented by ''. +## +## REVISION HISTORY (by Connection Manager version): +## + + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Marker inserted by {0} at {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Started plain (unencrypted) socket on port: {0} +startup.ssl=Started SSL (encrypted) socket on port: {0} +startup.error=Error starting the server. Please check the log files for more information. +startup.error.jivehome=Could not locate managerHome. Set the managerHome property or edit \ + your manager_init.xml file for app server deployments. + +# Standard server error messages (for server admin) + +admin.error=Internal server error +admin.error.accept=Trouble accepting connection +admin.error.bad-stream=Bad opening tag (not stream) +admin.error.bad-namespace=Stream not in correct namespace +admin.error.channel-notfound=Channel {0} could not be found +admin.error.close=Could not close socket +admin.error.connection=Connection closed before session established +admin.error.deliver=Could not deliver packet +admin.error.min-thread=Cannot set min thread count with invalid value. +admin.error.max-thread=Cannot set max thread count with invalid value. +admin.error.packet=Malformed packet received +admin.error.packet.text=Unexpected raw text in the stream +admin.error.packet.tag=Unexpected packet tag (not message,iq,presence) +admin.error.routing=Could not route packet +admin.error.socket-setup=Could not setup a server socket +admin.error.ssl=Could not setup SSL socket +admin.error.stream=Stream error detected +admin.drop-packet=Dropping unrecognized packet +admin.disconnect=Stream cut short (could be normal disconnect) + +# Setup + +setup.index.unable_locate_dir=Unable to locate valid conf directory. Please refer to the installation \ + documentation for the correct way to set the conf directory. diff --git a/src/i18n/cmanager_i18n_es.properties b/src/i18n/cmanager_i18n_es.properties new file mode 100644 index 0000000..348192b --- /dev/null +++ b/src/i18n/cmanager_i18n_es.properties @@ -0,0 +1,50 @@ +## +## Connection Manager Resource Bundle - Spanish locale (es) +## Translated by Gaston Dombiak +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Marcador insertado por {0} en {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Socket iniciado (sin encriptar) en el puerto: {0} +startup.ssl=Socket SSL (encriptado) iniciado en el puerto: {0} +startup.error=Error iniciando el servidor. Por favor vea los archivos de log para obtener m\u00e1s informaci\u00f3n +startup.error.jivehome=No se pudo encontrar 'managerHome'. Configure la propiedad 'managerHome' o edite su archivo manager_init.xml para distribuiciones con servidor de aplicaciones. + +# Standard server error messages (for server admin) + +admin.error=Error interno en el servidor +admin.error.accept=Problemas la aceptar la conexi\u00f3n +admin.error.bad-stream=Etiqueta de apertura incorrecta (sin flujo de datos) +admin.error.bad-namespace=Flujo de datos en espacio de nombres incorrecto +admin.error.channel-notfound=No se pudo encontrar el canal {0} +admin.error.close=No se pudo cerrar el socket +admin.error.connection=Se cerr\u00f3 la conexi\u00f3n antes de establecer la sesi\u00f3n +admin.error.deliver=No se pudo enviar el paquete +admin.error.min-thread=No se pudo configurar la cantidad m\u00ednima de threads (valor no v\u00e1lido) +admin.error.max-thread=No se pudo configurar la cantidad m\u00e1xima de threads (valor no v\u00e1lido) +admin.error.packet=Se recibi\u00f3 un paquete mal formado +admin.error.packet.text=Texto en bruto inesperado en el flujo de datos +admin.error.packet.tag=Paquete con etiqueta inesperada (no es message,iq,presence) +admin.error.routing=No se pudo rutear el paquete +admin.error.socket-setup=No se pudo establecer un socket de servidor +admin.error.ssl=No se pudo establecer un socket SSL +admin.error.stream=Se detect\u00f3 un error en el flujo de datos +admin.drop-packet=Descartando paquete no reconocido +admin.disconnect=Flujo de datos cortado prematuramente (pudo ser una desconexi\u00f3n normal) + +# Setup + +setup.index.unable_locate_dir=No se pudo encontrar el directorio conf. Por favor vea como configurar \ + el directorio conf en la documentaci\u00f3n de instalaci\u00f3n. diff --git a/src/i18n/cmanager_i18n_fr.properties b/src/i18n/cmanager_i18n_fr.properties new file mode 100644 index 0000000..f5e12a2 --- /dev/null +++ b/src/i18n/cmanager_i18n_fr.properties @@ -0,0 +1,59 @@ +# $RCSfile: cmanager_i18n_fr.properties,v $ +# $Revision: 3091 $ +# $Date: 2005-11-16 18:36:03 -0300 (Wed, 16 Nov 2005) $ + +## +## Connection Manager Resource Bundle - French locale (fr) +## Traduction effectu\u00e9e par Julien DUMETIER +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Marqueur ajout\u00e9 par {0} le {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Socket standard (non crypt\u00e9) d\u00e9marr\u00e9 sur le port : {0} +startup.ssl=Socket SSL (crypt\u00e9) d\u00e9marr\u00e9 sur le port: {0} +startup.error=Erreur au d\u00e9marrage du serveur. Veuillez v\u00e9rifier les journaux pour plus \ + d'information. +startup.error.jivehome=Impossible de localiser managerHome. D\u00e9finissez la propri\u00e9t\u00e9 \ + managerHome ou modifiez \ + votre fichier manager_init.xml pour les d\u00e9ploiements sur serveur d'application. + +# Standard server error messages (for server admin) + +admin.error=Erreur interne du serveur +admin.error.accept=Probl\u00e8me \u00e0 accepter la connexion +admin.error.bad-stream=Mauvais tag d'ouverture (not stream) +admin.error.bad-namespace=Le flux n'est pas dans un espace d nommage correct +admin.error.channel-notfound=Canal {0} n'a pu \u00eatre trouv\u00e9 +admin.error.close=Impossible de fermer le socket +admin.error.connection=Connexion cl\u00f4tur\u00e9e avant l \u00e9tablissement de la session +admin.error.deliver=Impossible de livrer le paquet +admin.error.min-thread=Impossible de param\u00e9trer le nombre minimal de fil avec un valeur invalide. +admin.error.max-thread=Impossible de param\u00e9trer le nombre maximal de fil avec un valeur invalide. +admin.error.packet=Paquet malform\u00e9 re\u00e7u +admin.error.packet.text=Texte brut inattendu dans le flux +admin.error.packet.tag=Tag inattendu dans le paquet (ni message, ni iq, ni presence) +admin.error.routing=Impossible d'orienter ce paquet +admin.error.socket-setup=Impossible de configurer un socket +admin.error.ssl=Impossible de configurer le socket SSL +admin.error.stream=Erreur de flux d\u00e9tect\u00e9e +admin.drop-packet=Rejet de paquet non reconnus +admin.disconnect=Flux coup\u00e9 court (peut-\u00eatre un d\u00e9connexion normale) + +# Setup + +setup.index.unable_locate_dir=Impossible de localiser un r\u00e9pertoire conf valide. Veuillez vous \ + r\u00e9f\u00e9rer \u00e0 la documentation d'installation pour corriger le fa\u00e7on de param\u00e9trer le r\u00e9pertoire \ + conf. diff --git a/src/i18n/cmanager_i18n_nl.properties b/src/i18n/cmanager_i18n_nl.properties new file mode 100644 index 0000000..bc66336 --- /dev/null +++ b/src/i18n/cmanager_i18n_nl.properties @@ -0,0 +1,54 @@ +# $RCSfile: cmanager_i18n_nl.properties,v $ +# $Revision: 3091 $ +# $Date: 2005-11-16 18:36:03 -0300 (Wed, 16 Nov 2005) $ + +## +## Connection Manager Resource Bundle - Dutch locale (NL) +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Markering ingevoegd door {0} om {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} Gestart [{1}] +startup.plain=Gewone (niet versleutelde) socket gestart op poort: {0} +startup.ssl=SSL (versleutelde) socket gestart op poort: {0} +startup.error=Fout tijdens het starten van de server. Bekijk het logboek voor meer informatie. +startup.error.jivehome=Kan managerHome niet vinden. Bepaal de managerHome property of wijzig \ + uw manager_init.xml bestand voor app server installaties. + +# Standard server error messages (for server admin) + +admin.error=Interne serverfout +admin.error.accept=Probleem bij het aanvaarden van een verbinding +admin.error.bad-stream=Foute openingstag (niet "stream") +admin.error.bad-namespace=Stream niet in de geldige namespace +admin.error.channel-notfound=Kanaal {0} kan niet gevonden worden +admin.error.close=Kan socket niet sluiten +admin.error.connection=Verbinding gesloten voor een sessie werd gemaakt +admin.error.deliver=Kan pakket niet leveren +admin.error.min-thread=Kan de min thread teller niet instellen op een ongeldige waarde. +admin.error.max-thread=Kan de max thread teller niet instellen op een ongeldige waarde. +admin.error.packet=Ongeldig gevormd pakket ontvangen +admin.error.packet.text=Onverwachte ruwe tekst in de stream +admin.error.packet.tag=Onverwachte pakket tag (niet message,iq,presence) +admin.error.routing=Kan pakket niet routen +admin.error.socket-setup=Kan geen server socket aanmaken +admin.error.ssl=Kan geen SSL socket aanmaken +admin.error.stream=Stream fout gedetecteerd +admin.drop-packet=Onherkend pakket wordt genegeerd +admin.disconnect=Stream voortijdig afgesloten (kan een normale verbroken verbinding zijn) + +# Setup + +setup.index.unable_locate_dir=Kan geen geldige conf map vinden. Lees de installatiehandleiding om \ + te leren hoe de conf map moet worden bepaald. diff --git a/src/i18n/cmanager_i18n_pl_PL.properties b/src/i18n/cmanager_i18n_pl_PL.properties new file mode 100644 index 0000000..61fd1ed --- /dev/null +++ b/src/i18n/cmanager_i18n_pl_PL.properties @@ -0,0 +1,58 @@ +# $RCSfile$ +# $Revision: 3148 $ +# $Date: 2005-12-01 14:50:45 -0300 (Thu, 01 Dec 2005) $ + +## +## Connection Manager Resource Bundle - Polish locale (pl_PL) +## Translation by Krzysztof Biga and Grzegorz Kapusta +## +## Additional locales can be specified by creating a new resource file in this +## directory using the following conventions: +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Zaznaczenie utworzone przez {0} w {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} [{1}] +startup.plain=Uruchomiono plain (unencrypted) socket na porcie: {0} +startup.ssl=Uruchomiono socket (szyfrowany) SSL na porcie: {0} +startup.error=B\u0142\u0105d podczas uruchamiania serwera. Wi\u0119cej informacji znajduje si\u0119 w plikach log\u00f3w. +startup.error.jivehome=Nie mo\u017cna zlokalizowa\u0107 zmiennej managerHome. Prosz\u0119 ustawi\u0107 zmienn\u0105 managerHome lub skonfigurowa\u0107 \ + plik manager_init.xml aby mie\u0107 mo\u017cliwo\u015b\u0107 deployowania aplikacji na serwerze. + +# Standard server error messages (for server admin) + +admin.error=B\u0142\u0105d wewn\u0119trzny serwera +admin.error.accept=Problemy z akceptacj\u0105 po\u0142\u0105czenia +admin.error.bad-stream=Nieprawid\u0142owy tag otwieraj\u0105cy (not stream) +admin.error.bad-namespace=Strumie\u0144 w nieprawid\u0142owej przestrzeni nazw +admin.error.channel-notfound=Kana\u0142 {0} nie zosta\u0142 odnaleziony +admin.error.close=B\u0142\u0105d przy zamykaniu socketu +admin.error.connection=Zerwano po\u0142\u0105czenie przed ustawieniem sesji +admin.error.deliver=Nie mo\u017cna by\u0142o dostarczy\u0107 pakietu +admin.error.min-thread=Nieprawid\u0142owa warto\u015b\u0107 minimalnej liczby w\u0105tk\u00f3w. +admin.error.max-thread=Nieprawid\u0142owa warto\u015b\u0107 maksymalnej liczby w\u0105tk\u00f3w. +admin.error.packet=Otrzymano uszkodzony pakiet +admin.error.packet.text=Niespodziewane wyst\u0105pienie otwartego tekstu w strumieniu +admin.error.packet.tag=Nieoczekiwany tag pakietu (inny ni\u017c message,iq,presence) +admin.error.routing=B\u0142\u0105d podczas routowania pakietu +admin.error.socket-setup=B\u0142\u0105d podczas inicjalizacji socketu na serwerze +admin.error.ssl=B\u0142\u0105d podczas inicjalizacji socketu SSL +admin.error.stream=Wykryto b\u0142\u0105d strumienia +admin.drop-packet=Porzucono nierozpoznany pakiet +admin.disconnect=Strumie\u0144 uci\u0119ty (could be normal disconnect) + +# Setup + +setup.index.unable_locate_dir=Nie mo\u017cna zlokalizowa\u0107 prawid\u0142owego katalogu conf. Skorzystaj z instrukcji instalacji w celu \ + poprawnego ustawienia katalogu conf. diff --git a/src/i18n/cmanager_i18n_pt_BR.properties b/src/i18n/cmanager_i18n_pt_BR.properties new file mode 100644 index 0000000..550b7f8 --- /dev/null +++ b/src/i18n/cmanager_i18n_pt_BR.properties @@ -0,0 +1,55 @@ +# $RCSfile: cmanager_i18n_pt_BR.properties,v $ +# $Revision: $ +# $Date: $ + +## +## Connection Manager Resource Bundle - Brazilian locale (pt_BR) +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 3.0.0 + +# Connection Manager + +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- Marker inserted by {0} at {1} --- + +# Server startup messages + +startup.name=Connection Manager {0} iniciado [{1}] +startup.starting=Dom\u00ednio do Connection Manager: {0} +startup.plain=Iniciado soquete (n\u00e3o-encriptado) na porta: {0} +startup.ssl=Iniciado soquete SSL (encriptado) na porta: {0} +startup.error=Erro iniciando o servidor. Por favor verifique os arquivos de log para mais informa\u00e7\u00f5es. +startup.error.jivehome=N\u00e3o localizei managerHome. Defina a propriedade managerHome ou edite \ + seu arquivo manager_init.xml para distribui\u00e7\u00e3o do servidor de aplica\u00e7\u00f5es. + +# Standard server error messages (for server admin) + +admin.error=Erro interno do servidor +admin.error.accept=Problemas na aceita\u00e7\u00e3o de conex\u00e3o +admin.error.bad-stream=M\u00e1 tag de abertura (sem fluxo) +admin.error.bad-namespace=N\u00e3o h\u00e1 fluxo no namespace correto +admin.error.channel-notfound=Canal {0} n\u00e3o encontrado +admin.error.close=N\u00e3o \u00e9 poss\u00edvel fechar o soquete +admin.error.connection=Conex\u00e3o fechada antes de estabelecer sess\u00e3o +admin.error.deliver=N\u00e3o foi poss\u00edvel entregar pacote +admin.error.min-thread=N\u00e3o \u00e9 poss\u00edvel definir a contagem m\u00ednima de thread com valor inv\u00e1lido. +admin.error.max-thread=N\u00e3o \u00e9 poss\u00edvel definir a contagem m\u00e1xima de thread com valor inv\u00e1lido. +admin.error.packet=Recebido pacote malformado +admin.error.packet.text=Texto raw inesperado no fluxo +admin.error.packet.tag=Tag de pacote inesperada (n\u00e3o \u00e9 messagem, iq, presen\u00e7a) +admin.error.routing=N\u00e3o \u00e9 poss\u00edvel rotear o pacote +admin.error.socket-setup=N\u00e3o \u00e9 poss\u00edvel definir um soquete de servidor +admin.error.ssl=N\u00e3o \u00e9 poss\u00edvel definir soquete SSL +admin.error.stream=Detectado erro de fluxo +admin.drop-packet=Liberando pacote n\u00e3o reconhecido +admin.disconnect=Fluxo cortado prematuramente (pode ser uma desconex\u00e3o normal) + +# Setup + +setup.index.unable_locate_dir=Incapaz de localizar um diret\u00f3rio conf v\u00e1lido. Por favor verifique a documenta\u00e7\u00e3o \ + da instala\u00e7\u00e3o para configurar de maneira correta o diret\u00f3rio conf. diff --git a/src/i18n/cmanager_i18n_zh_CN.properties b/src/i18n/cmanager_i18n_zh_CN.properties new file mode 100644 index 0000000..5877a09 --- /dev/null +++ b/src/i18n/cmanager_i18n_zh_CN.properties @@ -0,0 +1,53 @@ +# $RCSfile: cmanager_i18n_zh_CN.properties,v $ +# $Revision: 3120 $ +# $Date: 2005-11-28 14:00:51 -0300 (Mon, 28 Nov 2005) $ + +## +## Connection Manager Resource Bundle - Chinese locale (zh_CN) +## +## For a full changelog, refer to the English bundle, cmanager_i18n_en.properties. +## +## Updated for release: 2.6.0 + +# Connection Manager + +# NLS_MESSAGEFORMAT_NONE +short.title = Connection Manager +title = Connection Manager + +# Log messages +log.marker_inserted_by=--- \u7531 {0} \u5728 {1} \u63d2\u5165\u7684\u6807\u8bb0 --- + +# Server startup messages + +startup.name=Connection Manager {0} \u5df2\u542f\u52a8 [{1}] +startup.plain=\u5df2\u5728\u4ee5\u4e0b\u7aef\u53e3\u4e0a\u542f\u52a8\u666e\u901a\uff08\u672a\u52a0\u5bc6\uff09\u5957\u63a5\u5b57\uff1a{0} +startup.ssl=\u5df2\u5728\u4ee5\u4e0b\u7aef\u53e3\u4e0a\u542f\u52a8 SSL\uff08\u5df2\u52a0\u5bc6\uff09\u5957\u63a5\u5b57\uff1a{0} +startup.error=\u542f\u52a8\u670d\u52a1\u5668\u65f6\u51fa\u9519\u3002\u8bf7\u68c0\u67e5\u65e5\u5fd7\u6587\u4ef6\u4ee5\u83b7\u53d6\u66f4\u591a\u4fe1\u606f\u3002 +startup.error.jivehome=\u627e\u4e0d\u5230 managerHome\u3002\u8bf7\u8bbe\u7f6e managerHome \u5c5e\u6027\uff0c\u6216\u7f16\u8f91 manager_init.xml \u6587\u4ef6\u4ee5\u9002\u5e94\u670d\u52a1\u5668\u90e8\u7f72\u3002 + +# Standard server error messages (for server admin) + +admin.error=\u5185\u90e8\u670d\u52a1\u5668\u9519\u8bef +admin.error.accept=\u63a5\u53d7\u8fde\u63a5\u65f6\u9047\u5230\u95ee\u9898 +admin.error.bad-stream=\u9519\u8bef\u7684 opening \u6807\u8bb0\uff08\u4e0d\u662f stream\uff09 +admin.error.bad-namespace=\u6d41\u4e0d\u5728\u6b63\u786e\u7684\u540d\u79f0\u7a7a\u95f4\u4e2d +admin.error.channel-notfound=\u627e\u4e0d\u5230\u901a\u9053 {0} +admin.error.close=\u65e0\u6cd5\u5173\u95ed\u5957\u63a5\u5b57 +admin.error.connection=\u5efa\u7acb\u4f1a\u8bdd\u4e4b\u524d\u8fde\u63a5\u5df2\u5173\u95ed +admin.error.deliver=\u65e0\u6cd5\u4f20\u9012\u6570\u636e\u5305 +admin.error.min-thread=\u65e0\u6cd5\u4ee5\u65e0\u6548\u503c\u8bbe\u7f6e\u6700\u5c0f\u7ebf\u7a0b\u6570\u91cf +admin.error.max-thread=\u65e0\u6cd5\u4ee5\u65e0\u6548\u503c\u8bbe\u7f6e\u6700\u5927\u7ebf\u7a0b\u6570\u91cf +admin.error.packet=\u63a5\u6536\u5230\u683c\u5f0f\u4e0d\u6807\u51c6\u7684\u6570\u636e\u5305 +admin.error.packet.text=\u6d41\u4e2d\u5b58\u5728\u610f\u5916\u666e\u901a\u6587\u672c +admin.error.packet.tag=\u610f\u5916\u6570\u636e\u5305\u6807\u8bb0\uff08\u4e0d\u662f message\u3001iq \u6216 presence\uff09 +admin.error.routing=\u65e0\u6cd5\u8def\u7531\u6570\u636e\u5305 +admin.error.socket-setup=\u65e0\u6cd5\u8bbe\u7f6e\u670d\u52a1\u5668\u5957\u63a5\u5b57 +admin.error.ssl=\u65e0\u6cd5\u8bbe\u7f6e SSL \u5957\u63a5\u5b57 +admin.error.stream=\u68c0\u6d4b\u5230\u6d41\u9519\u8bef +admin.drop-packet=\u4e22\u5f03\u65e0\u6cd5\u8bc6\u522b\u7684\u6570\u636e\u5305 +admin.disconnect=\u6d41\u622a\u65ad\uff08\u65e0\u6cd5\u6b63\u5e38\u65ad\u5f00\u8fde\u63a5\uff09 + +# Setup + +setup.index.unable_locate_dir=\u627e\u4e0d\u5230\u6709\u6548\u7684 conf \u76ee\u5f55\u3002\u8bf7\u53c2\u9605\u5b89\u88c5\u6587\u6863\u4ee5\u83b7\u53d6\u8bbe\u7f6e conf \u76ee\u5f55\u7684\u6b63\u786e\u65b9\u6cd5\u3002 diff --git a/src/java/org/dom4j/io/XMPPPacketReader.java b/src/java/org/dom4j/io/XMPPPacketReader.java new file mode 100644 index 0000000..d0b3d79 --- /dev/null +++ b/src/java/org/dom4j/io/XMPPPacketReader.java @@ -0,0 +1,482 @@ +/* + * Copyright 2001-2004 (C) MetaStuff, Ltd. All Rights Reserved. + * + * This software is open source. + * See the bottom of this file for the licence. + * + * $Id: XMPPPacketReader.java 3190 2005-12-12 15:00:46Z gato $ + */ + +package org.dom4j.io; + +import org.dom4j.*; +import org.jivesoftware.multiplexer.net.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.*; +import java.net.URL; + +/** + *

XMPPPacketReader is a Reader of DOM4J documents that + * uses the fast + * XML Pull Parser 3.x. + * It is very fast for use in SOAP style environments.

+ * + * @author Pelle Braendgaard + * @author James Strachan + * @version $Revision: 3190 $ + */ +public class XMPPPacketReader { + + /** + * DocumentFactory used to create new document objects + */ + private DocumentFactory factory; + + /** + * XmlPullParser used to parse XML + */ + private MXParser xppParser; + + /** + * XmlPullParser used to parse XML + */ + private XmlPullParserFactory xppFactory; + + /** + * DispatchHandler to call when each Element is encountered + */ + private DispatchHandler dispatchHandler; + + /** + * Last time a full Document was read or a heartbeat was received. Hearbeats + * are represented as whitespaces received while a Document is not being parsed. + */ + private long lastActive = System.currentTimeMillis(); + + + public XMPPPacketReader() { + } + + public XMPPPacketReader(DocumentFactory factory) { + this.factory = factory; + } + + + /** + *

Reads a Document from the given File

+ * + * @param file is the File to read from. + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + * @throws java.net.MalformedURLException if a URL could not be made for the given File + */ + public Document read(File file) throws DocumentException, IOException, XmlPullParserException { + String systemID = file.getAbsolutePath(); + return read(new BufferedReader(new FileReader(file)), systemID); + } + + /** + *

Reads a Document from the given URL

+ * + * @param url URL to read from. + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(URL url) throws DocumentException, IOException, XmlPullParserException { + String systemID = url.toExternalForm(); + return read(createReader(url.openStream()), systemID); + } + + /** + *

Reads a Document from the given URL or filename.

+ *

+ *

+ * If the systemID contains a ':' character then it is + * assumed to be a URL otherwise its assumed to be a file name. + * If you want finer grained control over this mechansim then please + * explicitly pass in either a {@link URL} or a {@link File} instance + * instead of a {@link String} to denote the source of the document. + *

+ * + * @param systemID is a URL for a document or a file name. + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + * @throws java.net.MalformedURLException if a URL could not be made for the given File + */ + public Document read(String systemID) throws DocumentException, IOException, XmlPullParserException { + if (systemID.indexOf(':') >= 0) { + // lets assume its a URL + return read(new URL(systemID)); + } + else { + // lets assume that we are given a file name + return read(new File(systemID)); + } + } + + /** + *

Reads a Document from the given stream

+ * + * @param in InputStream to read from. + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(InputStream in) throws DocumentException, IOException, XmlPullParserException { + return read(createReader(in)); + } + + /** + *

Reads a Document from the given Reader

+ * + * @param reader is the reader for the input + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(Reader reader) throws DocumentException, IOException, XmlPullParserException { + getXPPParser().setInput(reader); + return parseDocument(); + } + + /** + *

Reads a Document from the given array of characters

+ * + * @param text is the text to parse + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(char[] text) throws DocumentException, IOException, XmlPullParserException { + getXPPParser().setInput(new CharArrayReader(text)); + return parseDocument(); + } + + /** + *

Reads a Document from the given stream

+ * + * @param in InputStream to read from. + * @param systemID is the URI for the input + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(InputStream in, String systemID) throws DocumentException, IOException, XmlPullParserException { + return read(createReader(in), systemID); + } + + /** + *

Reads a Document from the given Reader

+ * + * @param reader is the reader for the input + * @param systemID is the URI for the input + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(Reader reader, String systemID) throws DocumentException, IOException, XmlPullParserException { + Document document = read(reader); + document.setName(systemID); + return document; + } + + + // Properties + //------------------------------------------------------------------------- + + public MXParser getXPPParser() throws XmlPullParserException { + if (xppParser == null) { + xppParser = (MXParser) getXPPFactory().newPullParser(); + } + return xppParser; + } + + public XmlPullParserFactory getXPPFactory() throws XmlPullParserException { + if (xppFactory == null) { + xppFactory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); + } + xppFactory.setNamespaceAware(true); + return xppFactory; + } + + public void setXPPFactory(XmlPullParserFactory xppFactory) { + this.xppFactory = xppFactory; + } + + /** + * @return the DocumentFactory used to create document objects + */ + public DocumentFactory getDocumentFactory() { + if (factory == null) { + factory = DocumentFactory.getInstance(); + } + return factory; + } + + /** + *

This sets the DocumentFactory used to create new documents. + * This method allows the building of custom DOM4J tree objects to be implemented + * easily using a custom derivation of {@link DocumentFactory}

+ * + * @param factory DocumentFactory used to create DOM4J objects + */ + public void setDocumentFactory(DocumentFactory factory) { + this.factory = factory; + } + + + /** + * Adds the ElementHandler to be called when the + * specified path is encounted. + * + * @param path is the path to be handled + * @param handler is the ElementHandler to be called + * by the event based processor. + */ + public void addHandler(String path, ElementHandler handler) { + getDispatchHandler().addHandler(path, handler); + } + + /** + * Removes the ElementHandler from the event based + * processor, for the specified path. + * + * @param path is the path to remove the ElementHandler for. + */ + public void removeHandler(String path) { + getDispatchHandler().removeHandler(path); + } + + /** + * When multiple ElementHandler instances have been + * registered, this will set a default ElementHandler + * to be called for any path which does NOT have a handler + * registered. + * + * @param handler is the ElementHandler to be called + * by the event based processor. + */ + public void setDefaultHandler(ElementHandler handler) { + getDispatchHandler().setDefaultHandler(handler); + } + + /** + * Returns the last time a full Document was read or a heartbeat was received. Hearbeats + * are represented as whitespaces or \n received while a Document is not being parsed. + * + * @return the time in milliseconds when the last document or heartbeat was received. + */ + public long getLastActive() { + long lastHeartbeat = 0; + try { + lastHeartbeat = getXPPParser().getLastHeartbeat(); + } + catch (XmlPullParserException e) {} + return lastActive > lastHeartbeat ? lastActive : lastHeartbeat; + } + + /* + * DANIELE: Add parse document by string + */ + public Document parseDocument(String xml) throws DocumentException { + /* + // Long way with reuse of DocumentFactory. + DocumentFactory df = getDocumentFactory(); + SAXReader reader = new SAXReader( df ); + Document document = reader.read( new StringReader( xml );*/ + + // Simple way + // TODO Optimize. Do not create a sax reader for each parsing + Document document = DocumentHelper.parseText(xml); + + return document; + } + + // Implementation methods + //------------------------------------------------------------------------- + public Document parseDocument() throws DocumentException, IOException, XmlPullParserException { + DocumentFactory df = getDocumentFactory(); + Document document = df.createDocument(); + Element parent = null; + XmlPullParser pp = getXPPParser(); + int count = 0; + while (true) { + int type = -1; + type = pp.nextToken(); + switch (type) { + case XmlPullParser.PROCESSING_INSTRUCTION: { + String text = pp.getText(); + int loc = text.indexOf(" "); + if (loc >= 0) { + document.addProcessingInstruction(text.substring(0, loc), + text.substring(loc + 1)); + } + else { + document.addProcessingInstruction(text, ""); + } + break; + } + case XmlPullParser.COMMENT: { + if (parent != null) { + parent.addComment(pp.getText()); + } + else { + document.addComment(pp.getText()); + } + break; + } + case XmlPullParser.CDSECT: { + String text = pp.getText(); + if (parent != null) { + parent.addCDATA(text); + } + else { + if (text.trim().length() > 0) { + throw new DocumentException("Cannot have text content outside of the root document"); + } + } + break; + + } + case XmlPullParser.ENTITY_REF: { + String text = pp.getText(); + if (parent != null) { + parent.addText(text); + } + else { + if (text.trim().length() > 0) { + throw new DocumentException("Cannot have an entityref outside of the root document"); + } + } + break; + } + case XmlPullParser.END_DOCUMENT: { + return document; + } + case XmlPullParser.START_TAG: { + QName qname = (pp.getPrefix() == null) ? df.createQName(pp.getName(), pp.getNamespace()) : df.createQName(pp.getName(), pp.getPrefix(), pp.getNamespace()); + Element newElement = null; + // Do not include the namespace if this is the start tag of a new packet + // This avoids including "jabber:client", "jabber:server" or + // "jabber:component:accept" + if ("jabber:connectionmanager".equals(qname.getNamespaceURI())) { + newElement = df.createElement(pp.getName()); + } + else { + newElement = df.createElement(qname); + } + int nsStart = pp.getNamespaceCount(pp.getDepth() - 1); + int nsEnd = pp.getNamespaceCount(pp.getDepth()); + for (int i = nsStart; i < nsEnd; i++) { + if (pp.getNamespacePrefix(i) != null) { + newElement + .addNamespace(pp.getNamespacePrefix(i), pp.getNamespaceUri(i)); + } + } + for (int i = 0; i < pp.getAttributeCount(); i++) { + QName qa = (pp.getAttributePrefix(i) == null) ? df.createQName(pp.getAttributeName(i)) : df.createQName(pp.getAttributeName(i), pp.getAttributePrefix(i), pp.getAttributeNamespace(i)); + newElement.addAttribute(qa, pp.getAttributeValue(i)); + } + if (parent != null) { + parent.add(newElement); + } + else { + document.add(newElement); + } + parent = newElement; + count++; + break; + } + case XmlPullParser.END_TAG: { + if (parent != null) { + parent = parent.getParent(); + } + count--; + if (count < 1) { + // Update the last time a Document was received + lastActive = System.currentTimeMillis(); + return document; + } + break; + } + case XmlPullParser.TEXT: { + String text = pp.getText(); + if (parent != null) { + parent.addText(text); + } + else { + if (text.trim().length() > 0) { + throw new DocumentException("Cannot have text content outside of the root document"); + } + } + break; + } + default: + { + ; + } + } + } + } + + protected DispatchHandler getDispatchHandler() { + if (dispatchHandler == null) { + dispatchHandler = new DispatchHandler(); + } + return dispatchHandler; + } + + protected void setDispatchHandler(DispatchHandler dispatchHandler) { + this.dispatchHandler = dispatchHandler; + } + + /** + * Factory method to create a Reader from the given InputStream. + */ + protected Reader createReader(InputStream in) throws IOException { + return new BufferedReader(new InputStreamReader(in)); + } +} + +/* + * Redistribution and use of this software and associated documentation + * ("Software"), with or without modification, are permitted provided + * that the following conditions are met: + * + * 1. Redistributions of source code must retain copyright + * statements and notices. Redistributions must also contain a + * copy of this document. + * + * 2. Redistributions in binary form must reproduce the + * above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. The name "DOM4J" must not be used to endorse or promote + * products derived from this Software without prior written + * permission of MetaStuff, Ltd. For written permission, + * please contact dom4j-info@metastuff.com. + * + * 4. Products derived from this Software may not be called "DOM4J" + * nor may "DOM4J" appear in their names without prior written + * permission of MetaStuff, Ltd. DOM4J is a registered + * trademark of MetaStuff, Ltd. + * + * 5. Due credit should be given to the DOM4J Project - + * http://www.dom4j.org + * + * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Copyright 2001-2004 (C) MetaStuff, Ltd. All Rights Reserved. + * + * $Id: XMPPPacketReader.java 3190 2005-12-12 15:00:46Z gato $ + */ diff --git a/src/java/org/jivesoftware/multiplexer/ClientSession.java b/src/java/org/jivesoftware/multiplexer/ClientSession.java new file mode 100644 index 0000000..da4c0c5 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ClientSession.java @@ -0,0 +1,335 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; +import org.dom4j.io.XMPPPacketReader; +import org.jivesoftware.multiplexer.net.SocketConnection; +import org.jivesoftware.multiplexer.net.SocketReader; +import org.jivesoftware.multiplexer.spi.ClientFailoverDeliverer; +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.jivesoftware.util.JiveGlobals; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Session that represents a client to server connection. + * + * @author Gaston Dombiak + */ +public class ClientSession extends Session { + + private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams"; + private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash"; + + /** + * Milliseconds a connection has to be idle to be closed. Default is 30 minutes. Sending + * stanzas to the client is not considered as activity. We are only considering the connection + * active when the client sends some data or hearbeats (i.e. whitespaces) to the server. + * The reason for this is that sending data will fail if the connection is closed. And if + * the thread is blocked while sending data (because the socket is closed) then the clean up + * thread will close the socket anyway. + */ + private static long idleTimeout; + + private static StreamIDFactory idFactory = new StreamIDFactory(); + + /** + * Map of existing sessions. A session is added just after the initial stream header + * was processed. Key: stream ID, value: the session. + */ + private static Map sessions = + new ConcurrentHashMap(); + /** + * Socket reader that is processing incoming packets from the client. + */ + private SocketReader socketReader; + + static { + // Set the default read idle timeout. If none was set then assume 30 minutes + idleTimeout = JiveGlobals.getIntProperty("xmpp.client.idle", 30 * 60 * 1000); + } + + public static Session createSession(String serverName, SocketReader socketReader, + XMPPPacketReader reader, SocketConnection connection) + throws XmlPullParserException { + XmlPullParser xpp = reader.getXPPParser(); + + boolean isFlashClient = xpp.getPrefix().equals("flash"); + connection.setFlashClient(isFlashClient); + + // Conduct error checking, the opening tag should be 'stream' + // in the 'etherx' namespace + if (!xpp.getName().equals("stream") && !isFlashClient) { + throw new XmlPullParserException( + LocaleUtils.getLocalizedString("admin.error.bad-stream")); + } + + if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) && + !(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE))) + { + throw new XmlPullParserException(LocaleUtils.getLocalizedString( + "admin.error.bad-namespace")); + } + + // TODO Check if IP address is allowed to connect to the server + + // Default language is English ("en"). + String language = "en"; + // Default to a version of "0.0". Clients written before the XMPP 1.0 spec may + // not report a version in which case "0.0" should be assumed (per rfc3920 + // section 4.4.1). + int majorVersion = 0; + int minorVersion = 0; + for (int i = 0; i < xpp.getAttributeCount(); i++) { + if ("lang".equals(xpp.getAttributeName(i))) { + language = xpp.getAttributeValue(i); + } + if ("version".equals(xpp.getAttributeName(i))) { + try { + int[] version = decodeVersion(xpp.getAttributeValue(i)); + majorVersion = version[0]; + minorVersion = version[1]; + } + catch (Exception e) { + Log.error(e); + } + } + } + + // If the client supports a greater major version than the server, + // set the version to the highest one the server supports. + if (majorVersion > MAJOR_VERSION) { + majorVersion = MAJOR_VERSION; + minorVersion = MINOR_VERSION; + } + else if (majorVersion == MAJOR_VERSION) { + // If the client supports a greater minor version than the + // server, set the version to the highest one that the server + // supports. + if (minorVersion > MINOR_VERSION) { + minorVersion = MINOR_VERSION; + } + } + + // Store language and version information in the connection. + connection.setLanaguage(language); + connection.setXMPPVersion(majorVersion, minorVersion); + + ServerSurrogate serverSurrogate = ConnectionManager.getInstance().getServerSurrogate(); + // Indicate the TLS policy to use for this connection + connection.setTlsPolicy(serverSurrogate.getTlsPolicy()); + + // Indicate the compression policy to use for this connection + connection.setCompressionPolicy(serverSurrogate.getCompressionPolicy()); + + // Set the max number of milliseconds the connection may not receive data from the + // client before closing the connection + connection.setIdleTimeout(idleTimeout); + + // Create a ClientSession for this user. + String streamID = idFactory.createStreamID(); + ClientSession session = new ClientSession(serverName, connection, streamID); + connection.init(session); + session.socketReader = socketReader; + // Set the stream ID that identifies the client when forwarding traffic to a client fails + ((ClientFailoverDeliverer) connection.getPacketDeliverer()).setStreamID(streamID); + // Register that the new session is associated with the specified stream ID + sessions.put(streamID, session); + // Send to the server that a new client session has been created + serverSurrogate.clientSessionCreated(streamID); + + // Build the start packet response + StringBuilder sb = new StringBuilder(200); + sb.append(""); + if (isFlashClient) { + sb.append(""); + connection.deliverRawText(sb.toString()); + + // If this is a "Jabber" connection, the session is now initialized and we can + // return to allow normal packet parsing. + if (majorVersion == 0) { + return session; + } + // Otherwise, this is at least XMPP 1.0 so we need to announce stream features. + + sb = new StringBuilder(490); + sb.append(""); + if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) { + sb.append(""); + if (connection.getTlsPolicy() == Connection.TLSPolicy.required) { + sb.append(""); + } + sb.append(""); + } + // Include available SASL Mechanisms + sb.append(serverSurrogate.getSASLMechanisms(session)); + // Include Stream features + String specificFeatures = session.getAvailableStreamFeatures(); + if (specificFeatures != null) { + sb.append(specificFeatures); + } + sb.append(""); + + connection.deliverRawText(sb.toString()); + return session; + } + + /** + * Closes connections of connected clients since the server or the connection + * manager is being shut down. If the server is the one that is being shut down + * then the connection manager will keep running and will try to establish new + * connections to the server (on demand). + */ + public static void closeAll() { + for (ClientSession session : sessions.values()) { + session.close(true); + } + } + + /** + * Returns the session whose stream ID matches the specified stream ID. + * + * @param streamID the stream ID of the session to look for. + * @return the session whose stream ID matches the specified stream ID. + */ + public static ClientSession getSession(String streamID) { + return sessions.get(streamID); + } + + public ClientSession(String serverName, Connection connection, String streamID) { + super(serverName, connection, streamID); + } + + public String getAvailableStreamFeatures() { + // Offer authenticate and registration only if TLS was not required or if required + // then the connection is already secured + if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) { + return null; + } + + StringBuilder sb = new StringBuilder(200); + + // Include Stream Compression Mechanism + if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled && + !conn.isCompressed()) { + sb.append( + "zlib"); + } + + if (getStatus() != Session.STATUS_AUTHENTICATED) { + ServerSurrogate serverSurrogate = ConnectionManager.getInstance().getServerSurrogate(); + // Advertise that the server supports Non-SASL Authentication + if (serverSurrogate.isNonSASLAuthEnabled()) { + sb.append(""); + } + // Advertise that the server supports In-Band Registration + if (serverSurrogate.isInbandRegEnabled()) { + sb.append(""); + } + } + else { + // If the session has been authenticated then offer resource binding + // and session establishment + sb.append(""); + sb.append(""); + } + return sb.toString(); + } + + /** + * Delivers a stanza sent by the server to the client. + * + * @param stanza the stanza sent by the server. + */ + public void deliver(Element stanza) { + // Until session is not authenticated we need to inspect server traffic + if (status != Session.STATUS_AUTHENTICATED) { + String tag = stanza.getName(); + if ("success".equals(tag)) { + // Session has been authenticated (using SASL). Update status + setStatus(Session.STATUS_AUTHENTICATED); + // Notify the socket reader that sasl authentication has finished + socketReader.clientAuthenticated(true); + } + else if ("failure".equals(tag)) { + // Notify the socket reader that sasl authentication has finished + socketReader.clientAuthenticated(false); + } + else if ("challenge".equals(tag)) { + // Notify the socket reader that client needs to respond to challenge + socketReader.clientChallenged(); + } + } + // Deliver stanza to client + if (conn != null && !conn.isClosed()) { + try { + conn.deliver(stanza); + } + catch (Exception e) { + Log.error(LocaleUtils.getLocalizedString("admin.error"), e); + } + } + } + + public void close() { + close(false); + } + + /** + * Closes the client connection. The systemStopped parameter indicates if the + * client connection is being closed because the server is shutting down or unavailable + * or if it is because the connection manager is being shutdown. + * + * @param systemStopped true when the server is no longer available or the + * connection manager is being shutdown. + */ + public void close(boolean systemStopped) { + if (status != STATUS_CLOSED) { + // Close the connection of the client + if (systemStopped) { + conn.systemShutdown(); + } + else { + conn.close(); + } + // Changhe the status to closed + status = STATUS_CLOSED; + // Remove session from list of sessions + sessions.remove(getStreamID()); + // Tell the server that the client session has been closed + ConnectionManager.getInstance().getServerSurrogate().clientSessionClosed(getStreamID()); + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/Connection.java b/src/java/org/jivesoftware/multiplexer/Connection.java new file mode 100644 index 0000000..6fbface --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/Connection.java @@ -0,0 +1,197 @@ +/** + * $RCSfile: $ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Represents a connection on the server. + * + * @author Iain Shigeoka + */ +public interface Connection { + + /** + * Verifies that the connection is still live. Typically this is done by + * sending a whitespace character between packets. + * + * @return true if the socket remains valid, false otherwise. + */ + public boolean validate(); + + /** + * Returns the InetAddress describing the connection. + * + * @return the InetAddress describing the underlying connection properties. + */ + public InetAddress getInetAddress() throws UnknownHostException; + + /** + * Close this session including associated socket connection. The order of + * events for closing the session is: + *
    + *
  • Set closing flag to prevent redundant shutdowns. + *
  • Call notifyEvent all listeners that the channel is shutting down. + *
  • Close the socket. + *
+ */ + public void close(); + + /** + * Notification message indicating that the server is being shutdown. Implementors + * should send a stream error whose condition is system-shutdown before closing + * the connection. + */ + public void systemShutdown(); + + /** + * Returns true if the connection/session is closed. + * + * @return true if the connection is closed. + */ + public boolean isClosed(); + + /** + * Returns true if this connection is secure. + * + * @return true if the connection is secure (e.g. SSL/TLS) + */ + public boolean isSecure(); + + /** + * Delivers the packet to this connection without checking the recipient. + * The method essentially calls socket.send(packet.getWriteBuffer()). + * + * @param doc the packet to deliver. + */ + public void deliver(Element doc); + + /** + * Delivers raw text to this connection. This is a very low level way for sending + * XML stanzas to the client. This method should not be used unless you have very + * good reasons for not using {@link #deliver(Element)}.

+ * + * This method avoids having to get the writer of this connection and mess directly + * with the writer. Therefore, this method ensures a correct delivery of the stanza + * even if other threads were sending data concurrently. + * + * @param text the XML stanzas represented kept in a String. + */ + public void deliverRawText(String text); + + /** + * Returns true if the connected client is a flash client. Flash clients need + * to receive a special character (i.e. \0) at the end of each xml packet. Flash + * clients may send the character \0 in incoming packets and may start a connection + * using another openning tag such as: "flash:client". + * + * @return true if the connected client is a flash client. + */ + public boolean isFlashClient(); + + /** + * Returns the major version of XMPP being used by this connection + * (major_version.minor_version. In most cases, the version should be + * "1.0". However, older clients using the "Jabber" protocol do not set a + * version. In that case, the version is "0.0". + * + * @return the major XMPP version being used by this connection. + */ + public int getMajorXMPPVersion(); + + /** + * Returns the minor version of XMPP being used by this connection + * (major_version.minor_version. In most cases, the version should be + * "1.0". However, older clients using the "Jabber" protocol do not set a + * version. In that case, the version is "0.0". + * + * @return the minor XMPP version being used by this connection. + */ + public int getMinorXMPPVersion(); + + /** + * Returns the language code that should be used for this connection + * (e.g. "en"). + * + * @return the language code for the connection. + */ + public String getLanguage(); + + /** + * Returns true if the connection is using compression. + * + * @return true if the connection is using compression. + */ + boolean isCompressed(); + + /** + * Returns whether compression is optional or is disabled. + * + * @return whether compression is optional or is disabled. + */ + CompressionPolicy getCompressionPolicy(); + + /** + * Returns whether TLS is mandatory, optional or is disabled. When TLS is mandatory clients + * are required to secure their connections or otherwise their connections will be closed. + * On the other hand, when TLS is disabled clients are not allowed to secure their connections + * using TLS. Their connections will be closed if they try to secure the connection. in this + * last case. + * + * @return whether TLS is mandatory, optional or is disabled. + */ + TLSPolicy getTlsPolicy(); + + /** + * Enumeration of possible compression policies required to interact with the server. + */ + enum CompressionPolicy { + + /** + * compression is optional to interact with the server. + */ + optional, + + /** + * compression is not available. Entities that request a compression negotiation + * will get a stream error and their connections will be closed. + */ + disabled + } + + /** + * Enumeration of possible TLS policies required to interact with the server. + */ + enum TLSPolicy { + + /** + * TLS is required to interact with the server. Entities that do not secure their + * connections using TLS will get a stream error and their connections will be closed. + */ + required, + + /** + * TLS is optional to interact with the server. Entities may or may not secure their + * connections using TLS. + */ + optional, + + /** + * TLS is not available. Entities that request a TLS negotiation will get a stream + * error and their connections will be closed. + */ + disabled + } +} diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionCloseListener.java b/src/java/org/jivesoftware/multiplexer/ConnectionCloseListener.java new file mode 100644 index 0000000..353e547 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ConnectionCloseListener.java @@ -0,0 +1,27 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +/** + * Implement and register with a connection to receive notification + * of the connection closing. + * + * @author Gaston Dombiak + */ +public interface ConnectionCloseListener { + /** + * Called when a connection is closed. + * + * @param handback The handback object associated with the connection listener during Connection.registerCloseListener() + */ + public void onConnectionClose(Object handback); +} diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionManager.java b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java new file mode 100644 index 0000000..31d2883 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java @@ -0,0 +1,590 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Document; +import org.dom4j.io.SAXReader; +import org.jivesoftware.util.*; +import org.jivesoftware.multiplexer.net.SocketAcceptThread; +import org.jivesoftware.multiplexer.net.SSLSocketAcceptThread; +import org.jivesoftware.multiplexer.net.SocketSendingTracker; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Connection managers handle connections of clients that want to connect to a server. Each + * connection manager may have one or more connections to the target server. These connections + * are shared amongst connected clients (i.e. multiplexed) thus reducing the load on the + * server.

+ * + * The only properties that needs to be configured during Connection Managers' setup are + * xmpp.domain and xmpp.sharedSecret. The xmpp.domain property + * defines the name of the target server that clients want to connect to. Clients are + * redirected to a connection manager when trying to open a socket connection to the server. + * This is typically done by configuring some local DNS server with a SRV record for the server + * name that points to the connection manager address. More elaborated solutions may include a + * load balancer in front of several connection managers. Since XMPP connections are state-full + * and long-lived then the load balancer does not have to be configured with "sticky sessions".

+ * + * The server and connection managers have to share a common secret so that the server can + * let connection managers connect to the server and forward packets. Configure the + * xmpp.sharedSecret property with the same shared secret that the server is using.

+ * + * Each connection manager has to have a unique name that uniquely identifies it from other + * connection managers. Use the property xmpp.manager.name to manually set a name. If this + * property is not present then a random name will be created for the manager each time it is + * started. Properties are stored in conf/manager.xml. There are several ways for locating this + * file. + *

    + *
  1. Set the system property managerHome when starting up the server. + *
  2. When running in standalone mode attempt to find it in [home]/conf/manager.xml. + *
  3. Load the path from manager_init.xml which must be in the classpath. + *
+ * + * By default connection managers will open five connections to the server. Configure the + * xmpp.manager.connections property if you want to change the number of connections. + * + * @author Gaston Dombiak + */ +public class ConnectionManager { + + private static ConnectionManager instance; + + /** + * Name of the connection manager. Each manager MUST have a unique name. The name will + * be used when connecting to the server. + */ + protected String name; + /** + * Name of the server to connect. This is the server where users actually want to + * connect. + */ + protected String serverName; + protected Version version; + protected Date startDate; + protected Date stopDate; + + /** + * Location of the home directory. All configuration files should be + * located here. + */ + private File managerHome; + protected ClassLoader loader; + + /** + * True if in setup mode + */ + private boolean setupMode = true; + + private static final String STARTER_CLASSNAME = + "org.jivesoftware.multiplexer.starter.ServerStarter"; + private static final String WRAPPER_CLASSNAME = + "org.tanukisoftware.wrapper.WrapperManager"; + + private ServerSurrogate serverSurrogate; + private SocketAcceptThread socketThread; + private SSLSocketAcceptThread sslSocketThread; + + /** + * Returns a singleton instance of ConnectionManager. + * + * @return an instance. + */ + public static ConnectionManager getInstance() { + return instance; + } + + /** + * Creates a server and starts it. + */ + public ConnectionManager() { + // We may only have one instance of the server running on the JVM + if (instance != null) { + throw new IllegalStateException("A server is already running"); + } + instance = this; + start(); + } + + protected void initialize() throws FileNotFoundException { + locateHome(); + name = JiveGlobals.getXMLProperty("xmpp.manager.name", StringUtils.randomString(5)).toLowerCase(); + serverName = JiveGlobals.getXMLProperty("xmpp.domain"); + + version = new Version(3, 0, 0, Version.ReleaseStatus.Beta, -1); + if (serverName != null) { + setupMode = false; + } + else { + Log.warn(LocaleUtils.getLocalizedString("setup.index.unable_locate_dir")); + System.err.println(LocaleUtils.getLocalizedString("setup.index.unable_locate_dir")); + } + + if (isStandAlone()) { + Runtime.getRuntime().addShutdownHook(new ShutdownHookThread()); + } + + loader = Thread.currentThread().getContextClassLoader(); + } + + /** + * Finish the setup process. Because this method is meant to be called from inside + * the Admin console plugin, it spawns its own thread to do the work so that the + * class loader is correct. + */ + public void finishSetup() { + if (!setupMode) { + return; + } + // Make sure that setup finished correctly. + if ("true".equals(JiveGlobals.getXMLProperty("setup"))) { + // Set the new server domain assigned during the setup process + name = JiveGlobals.getXMLProperty("xmpp.manager.name", StringUtils.randomString(5)) + .toLowerCase(); + serverName = JiveGlobals.getXMLProperty("xmpp.domain").toLowerCase(); + + Thread finishSetup = new Thread() { + public void run() { + try { + // Start modules + startModules(); + } + catch (Exception e) { + e.printStackTrace(); + Log.error(e); + shutdownServer(); + } + } + }; + // Use the correct class loader. + finishSetup.setContextClassLoader(loader); + finishSetup.start(); + // We can now safely indicate that setup has finished + setupMode = false; + } + } + + public void start() { + try { + initialize(); + + // If the server has already been setup then we can start all the server's modules + if (!setupMode) { + // Start modules + startModules(); + } + // Log that the server has been started + List params = new ArrayList(); + params.add(version.getVersionString()); + params.add(JiveGlobals.formatDateTime(new Date())); + String startupBanner = LocaleUtils.getLocalizedString("startup.name", params); + Log.info(startupBanner); + System.out.println(startupBanner); + + startDate = new Date(); + stopDate = null; + } + catch (Exception e) { + e.printStackTrace(); + Log.error(e); + System.out.println(LocaleUtils.getLocalizedString("startup.error")); + shutdownServer(); + } + } + + private void startModules() { + serverSurrogate = new ServerSurrogate(); + serverSurrogate.start(); + String localIPAddress; + // Setup port info + try { + localIPAddress = InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) { + localIPAddress = "Unknown"; + } + // Start process that checks health of socket connections + SocketSendingTracker.getInstance().start(); + // Start the port listener for clients + startClientListeners(localIPAddress); + // Start the port listener for secured clients + startClientSSLListeners(localIPAddress); + } + + private void stopModules() { + stopClientListeners(); + stopClientSSLListeners(); + // Stop process that checks health of socket connections + SocketSendingTracker.getInstance().shutdown(); + // Stop service that forwards packets to the server + if (serverSurrogate != null) { + serverSurrogate.shutdown(false); + } + } + + private void startClientListeners(String localIPAddress) { + // Start clients plain socket unless it's been disabled. + int port = 5222; + ServerPort serverPort = new ServerPort(port, serverName, localIPAddress, + false, null, ServerPort.Type.client); + try { + socketThread = new SocketAcceptThread(serverPort); + //socketThread.setDaemon(true); + socketThread.setPriority(Thread.MAX_PRIORITY); + socketThread.start(); + + List params = new ArrayList(); + params.add(Integer.toString(socketThread.getPort())); + Log.info(LocaleUtils.getLocalizedString("startup.plain", params)); + } + catch (Exception e) { + System.err.println("Error starting XMPP listener on port " + port + ": " + + e.getMessage()); + Log.error(LocaleUtils.getLocalizedString("admin.error.socket-setup"), e); + } + } + + private void stopClientListeners() { + if (socketThread != null) { + socketThread.shutdown(); + socketThread = null; + } + } + + private void startClientSSLListeners(String localIPAddress) { + // Start clients SSL unless it's been disabled. + int port = 5223; + String algorithm = JiveGlobals.getXMLProperty("xmpp.socket.ssl.algorithm"); + if ("".equals(algorithm) || algorithm == null) { + algorithm = "TLS"; + } + ServerPort serverPort = new ServerPort(port, serverName, localIPAddress, + true, algorithm, ServerPort.Type.client); + try { + sslSocketThread = new SSLSocketAcceptThread(serverPort); + //sslSocketThread.setDaemon(true); + sslSocketThread.setPriority(Thread.MAX_PRIORITY); + sslSocketThread.start(); + + List params = new ArrayList(); + params.add(Integer.toString(sslSocketThread.getPort())); + Log.info(LocaleUtils.getLocalizedString("startup.ssl", params)); + } + catch (Exception e) { + System.err.println("Error starting SSL XMPP listener on port " + port + ": " + + e.getMessage()); + Log.error(LocaleUtils.getLocalizedString("admin.error.ssl"), e); + } + } + + private void stopClientSSLListeners() { + if (sslSocketThread != null) { + sslSocketThread.shutdown(); + sslSocketThread = null; + } + } + + /** + * Restarts the server and all it's modules only if the server is restartable. Otherwise do + * nothing. + */ + public void restart() { + if (isStandAlone() && isRestartable()) { + try { + Class wrapperClass = Class.forName(WRAPPER_CLASSNAME); + Method restartMethod = wrapperClass.getMethod("restart", (Class []) null); + restartMethod.invoke(null, (Object []) null); + } + catch (Exception e) { + Log.error("Could not restart container", e); + } + } + } + + /** + * Stops the server only if running in standalone mode. Do nothing if the server is running + * inside of another server. + */ + public void stop() { + // Only do a system exit if we're running standalone + if (isStandAlone()) { + // if we're in a wrapper, we have to tell the wrapper to shut us down + if (isRestartable()) { + try { + Class wrapperClass = Class.forName(WRAPPER_CLASSNAME); + Method stopMethod = wrapperClass.getMethod("stop", Integer.TYPE); + stopMethod.invoke(null, 0); + } + catch (Exception e) { + Log.error("Could not stop container", e); + } + } + else { + shutdownServer(); + stopDate = new Date(); + Thread shutdownThread = new ShutdownThread(); + shutdownThread.setDaemon(true); + shutdownThread.start(); + } + } + else { + // Close listening socket no matter what the condition is in order to be able + // to be restartable inside a container. + shutdownServer(); + stopDate = new Date(); + } + } + + /** + * Makes a best effort attempt to shutdown the server + */ + private void shutdownServer() { + // Stop modules + stopModules(); + // hack to allow safe stopping + Log.info("Connection Manager stopped"); + } + + public boolean isSetupMode() { + return setupMode; + } + + public boolean isRestartable() { + boolean restartable; + try { + restartable = Class.forName(WRAPPER_CLASSNAME) != null; + } + catch (ClassNotFoundException e) { + restartable = false; + } + return restartable; + } + + /** + * Returns if the server is running in standalone mode. We consider that it's running in + * standalone if the "org.jivesoftware.multiplexer.starter.ServerStarter" class is present in the + * system. + * + * @return true if the server is running in standalone mode. + */ + public boolean isStandAlone() { + boolean standalone; + try { + standalone = Class.forName(STARTER_CLASSNAME) != null; + } + catch (ClassNotFoundException e) { + standalone = false; + } + return standalone; + } + + /** + * Returns the service responsible for forwarding stanzas to the server. + * + * @return the service responsible for forwarding stanzas to the server. + */ + public ServerSurrogate getServerSurrogate() { + return serverSurrogate; + } + + /** + * Returns the name of the main server where received packets will be forwarded. + * + * @return the name of the main server where received packets will be forwarded. + */ + public String getServerName() { + return serverName; + } + + /** + * Returns the name that uniquely identifies this connection manager. Use the property + * xmpp.manager.name to manually set a name. If the property is not present then a + * random name will be created for the manager each time it is started. Properties + * are stored in conf/manager.xml. There are several ways for locating this file. + *
    + *
  1. Set the system property managerHome when starting up the server. + *
  2. When running in standalone mode attempt to find it in [home]/conf/manager.xml. + *
  3. Load the path from manager_init.xml which must be in the classpath. + *
+ * + * @return the name that uniquely identifies this connection manager. + */ + public String getName() { + return name; + } + + /** + * Verifies that the given home guess is a real Connection Manager home directory. + * We do the verification by checking for the Connection Manager config file in + * the config dir of jiveHome. + * + * @param homeGuess a guess at the path to the home directory. + * @param jiveConfigName the name of the config file to check. + * @return a file pointing to the home directory or null if the + * home directory guess was wrong. + * @throws java.io.FileNotFoundException if there was a problem with the home + * directory provided + */ + private File verifyHome(String homeGuess, String jiveConfigName) throws FileNotFoundException { + File managerHome = new File(homeGuess); + File configFile = new File(managerHome, jiveConfigName); + if (!configFile.exists()) { + throw new FileNotFoundException(); + } + else { + try { + return new File(managerHome.getCanonicalPath()); + } + catch (Exception ex) { + throw new FileNotFoundException(); + } + } + } + + /** + *

Retrieve the jive home for the container.

+ * + * @throws FileNotFoundException If jiveHome could not be located + */ + private void locateHome() throws FileNotFoundException { + String jiveConfigName = "conf" + File.separator + "manager.xml"; + // First, try to load it managerHome as a system property. + if (managerHome == null) { + String homeProperty = System.getProperty("managerHome"); + try { + if (homeProperty != null) { + managerHome = verifyHome(homeProperty, jiveConfigName); + } + } + catch (FileNotFoundException fe) { + // Ignore. + } + } + + // If we still don't have home, let's assume this is standalone + // and just look for home in a standard sub-dir location and verify + // by looking for the config file + if (managerHome == null) { + try { + managerHome = verifyHome("..", jiveConfigName).getCanonicalFile(); + } + catch (FileNotFoundException fe) { + // Ignore. + } + catch (IOException ie) { + // Ignore. + } + } + + // If home is still null, no outside process has set it and + // we have to attempt to load the value from manager_init.xml, + // which must be in the classpath. + if (managerHome == null) { + InputStream in = null; + try { + in = getClass().getResourceAsStream("/manager_init.xml"); + if (in != null) { + SAXReader reader = new SAXReader(); + Document doc = reader.read(in); + String path = doc.getRootElement().getText(); + try { + if (path != null) { + managerHome = verifyHome(path, jiveConfigName); + } + } + catch (FileNotFoundException fe) { + fe.printStackTrace(); + } + } + } + catch (Exception e) { + System.err.println("Error loading manager_init.xml to find home."); + e.printStackTrace(); + } + finally { + try { + if (in != null) { + in.close(); + } + } + catch (Exception e) { + System.err.println("Could not close open connection"); + e.printStackTrace(); + } + } + } + + if (managerHome == null) { + System.err.println("Could not locate home"); + throw new FileNotFoundException(); + } + else { + // Set the home directory for the config file + JiveGlobals.setHomeDirectory(managerHome.toString()); + // Set the name of the config file + JiveGlobals.setConfigName(jiveConfigName); + } + } + + /** + *

A thread to ensure the server shuts down no matter what.

+ *

Spawned when stop() is called in standalone mode, we wait a few + * seconds then call system exit().

+ * + * @author Iain Shigeoka + */ + private class ShutdownHookThread extends Thread { + + /** + *

Logs the server shutdown.

+ */ + public void run() { + shutdownServer(); + Log.info("Connection Manager halted"); + System.err.println("Connection Manager halted"); + } + } + + /** + *

A thread to ensure the server shuts down no matter what.

+ *

Spawned when stop() is called in standalone mode, we wait a few + * seconds then call system exit().

+ * + * @author Iain Shigeoka + */ + private class ShutdownThread extends Thread { + + /** + *

Shuts down the JVM after a 5 second delay.

+ */ + public void run() { + try { + Thread.sleep(5000); + // No matter what, we make sure it's dead + System.exit(0); + } + catch (InterruptedException e) { + // Ignore. + } + + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java b/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java new file mode 100644 index 0000000..04cd371 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java @@ -0,0 +1,496 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZInputStream; +import org.dom4j.DocumentFactory; +import org.dom4j.Element; +import org.dom4j.io.XMPPPacketReader; +import org.jivesoftware.multiplexer.net.DNSUtil; +import org.jivesoftware.multiplexer.net.MXParser; +import org.jivesoftware.multiplexer.net.SocketAcceptThread; +import org.jivesoftware.multiplexer.net.SocketConnection; +import org.jivesoftware.multiplexer.spi.ServerFailoverDeliverer; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.jivesoftware.util.StringUtils; +import org.xmlpull.v1.XmlPullParser; + +import javax.net.ssl.SSLHandshakeException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.Random; + +/** + * Thread that creates and keeps a connection to the server. This thread is responsable + * for actually forwarding clients traffic to the server. If the connection is no longer + * active then the thread is going to be discarded and a new one is created and added to + * the thread pool that is kept in {@link ServerSurrogate}. + * + * @author Gaston Dombiak + */ +public class ConnectionWorkerThread extends Thread { + + /** + * The utf-8 charset for decoding and encoding Jabber packet streams. + */ + private static String CHARSET = "UTF-8"; + + private static DocumentFactory docFactory = DocumentFactory.getInstance(); + // Sequence and random number generator used for creating unique IQ ID's. + private static int sequence = 0; + private static Random random = new Random(); + private static ConnectionCloseListener connectionListener; + + private String serverName; + private String managerName; + + /** + * JID that identifies this connection to the server. The address is composed by + * the connection manager name and the name of the thread. e.g.: connManager1/thread1 + */ + private String jidAddress; + /** + * Connection to the server. + */ + private SocketConnection connection; + /** + * Store the last received stream features from the server + */ + private Element features; + + static { + connectionListener = new ConnectionCloseListener() { + public void onConnectionClose(Object handback) { + ConnectionWorkerThread thread = (ConnectionWorkerThread) handback; + thread.interrupt(); + } + }; + } + + public ConnectionWorkerThread(ThreadGroup group, Runnable target, String name, long stackSize) { + super(group, target, name, stackSize); + ConnectionManager connectionManager = ConnectionManager.getInstance(); + this.serverName = connectionManager.getServerName(); + this.managerName = connectionManager.getName(); + // Create connection to the server + createConnection(); + // Clean up features variable that is no longer needed + features = null; + } + + /** + * Returns true if there is a connection to the server that is still active. Note + * that sometimes a socket assumes to be opened when in fact the underlying TCP + * socket connection is closed. To detect these cases we rely on heartbeats or + * timing out when writing data hasn't finished for a while. + * + * @return rue if there is a connection to the server that is still active. + */ + public boolean isValid() { + return connection != null && !connection.isClosed(); + } + + /** + * Returns the connection to the server. + * + * @return the connection to the server. + */ + public SocketConnection getConnection() { + return connection; + } + + /** + * Creates a new connection to the server + */ + private boolean createConnection() { + String realHostname = null; + int port = + JiveGlobals.getIntProperty("xmpp.port", SocketAcceptThread.DEFAULT_MULTIPLEX_PORT); + int realPort = port; + Socket socket = new Socket(); + try { + // Get the real hostname to connect to using DNS lookup of the specified hostname + DNSUtil.HostAddress address = DNSUtil.resolveXMPPServerDomain(serverName, port); + realHostname = address.getHost(); + realPort = address.getPort(); + Log.debug("CM - Trying to connect to " + serverName + ":" + port + + "(DNS lookup: " + realHostname + ":" + realPort + ")"); + // Establish a TCP connection to the Receiving Server + socket.connect(new InetSocketAddress(realHostname, realPort), 20000); + Log.debug("CM - Plain connection to " + serverName + ":" + port + " successful"); + } + catch (Exception e) { + Log.error("Error trying to connect to server: " + serverName + + "(DNS lookup: " + realHostname + ":" + realPort + ")", e); + return false; + } + + try { + connection = new SocketConnection(new ServerFailoverDeliverer(), socket, false); + + jidAddress = managerName + "/" + getName(); + + // Send the stream header + StringBuilder openingStream = new StringBuilder(); + openingStream.append(""); + connection.deliverRawText(openingStream.toString()); + + // Set a read timeout (of 5 seconds) so we don't keep waiting forever + int soTimeout = socket.getSoTimeout(); + socket.setSoTimeout(7000); + + XMPPPacketReader reader = new XMPPPacketReader(); + reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(), + CHARSET)); + // Get the answer from the Receiving Server + XmlPullParser xpp = reader.getXPPParser(); + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + + String id = xpp.getAttributeValue("", "id"); + String serverVersion = xpp.getAttributeValue("", "version"); + + // Check if the remote server is XMPP 1.0 compliant + if (serverVersion != null && decodeVersion(serverVersion)[0] >= 1) { + // Get the stream features + features = reader.parseDocument().getRootElement(); + // Check if there was an error + if (features != null && "error".equals(features.getName())) { + Log.debug("CM - Error while opening stream: " + features.asXML()); + // Failed to secure the connection + connection = null; + return false; + } + // Check if TLS is enabled + if (features != null && features.element("starttls") != null) { + // Try to secure the connection since the server supports TLS + if (!secureConnection(reader, openingStream)) { + // Failed to secure the connection + connection = null; + return false; + } + } + /*if (features != null && features.element("mechanisms") != null) { + // Try to authenticate with the server using SASL authentication + // TODO Compression should be done before SASL + if (!doSASLAuthentication(reader, openingStream)) { + // Failed to authenticate with the server + connection = null; + return false; + } + } + else { + // Server didn't offer SASL authentication + connection = null; + return false; + }*/ + if (features != null && features.element("compression") != null) { + // Try to use stream compression since the server supports it + if (!compressConnection(reader, openingStream)) { + // Failed to use stream compression (when enabled locally) + connection = null; + return false; + } + } + if (!doHandshake(id, reader)) { + // Failed to authenticate with the server + connection = null; + return false; + } + // Add connection listener + connection.registerCloseListener(connectionListener, this); + // Set idle time out (server needs to send heartbeats or traffic). Default 5 minutes + connection.setIdleTimeout(5 * 60 * 1000); + // Create reader that will process packets sent from the server. + createSocketReader(reader); + // Restore default timeout + socket.setSoTimeout(soTimeout); + return true; + } + Log.debug("CM - Server does not support XMPP version 1.0 or later"); + } + catch (SSLHandshakeException e) { + Log.warn("Handshake error while connecting to server: " + serverName + + "(DNS lookup: " + realHostname + ":" + realPort + ")", e); + } + catch (Exception e) { + Log.error("Error while connecting to server: " + serverName + "(DNS lookup: " + + realHostname + ":" + realPort + ")", e); + } + // Close the connection + if (connection != null) { + connection.close(); + connection = null; + } + return false; + } + + private boolean secureConnection(XMPPPacketReader reader, StringBuilder openingStream) + throws Exception { + Log.debug("CM - Indicating we want TLS to " + serverName); + connection.deliverRawText(""); + + MXParser xpp = reader.getXPPParser(); + // Wait for the response + Element proceed = reader.parseDocument().getRootElement(); + if (proceed != null && proceed.getName().equals("proceed")) { + Log.debug("CM - Negotiating TLS with " + serverName); + connection.startTLS(true, serverName); + Log.debug("CM - TLS negotiation with " + serverName + " was successful"); + + // TLS negotiation was successful so initiate a new stream + connection.deliverRawText(openingStream.toString()); + + // Reset the parser to use the new secured reader + xpp.setInput( + new InputStreamReader(connection.getTLSStreamHandler().getInputStream(), + CHARSET)); + // Skip new stream element + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + // Get new stream features + features = reader.parseDocument().getRootElement(); + return true; + } else { + Log.debug("CM - Error, was not received"); + } + return false; + } + + private boolean compressConnection(XMPPPacketReader reader, StringBuilder openingStream) + throws Exception { + // Check if we can use stream compression + String policyName = JiveGlobals.getXMLProperty("xmpp.server.compression.policy", + Connection.CompressionPolicy.disabled.toString()); + Connection.CompressionPolicy compressionPolicy = + Connection.CompressionPolicy.valueOf(policyName); + // Check if stream compression is enabled in the Connection Manager + if (Connection.CompressionPolicy.optional == compressionPolicy) { + Element compression = features.element("compression"); + boolean zlibSupported = false; + Iterator it = compression.elementIterator("method"); + while (it.hasNext()) { + Element method = (Element) it.next(); + if ("zlib".equals(method.getTextTrim())) { + zlibSupported = true; + } + } + if (zlibSupported) { + MXParser xpp = reader.getXPPParser(); + // Request Stream Compression + connection.deliverRawText( + "zlib"); + // Check if we are good to start compression + Element answer = reader.parseDocument().getRootElement(); + if ("compressed".equals(answer.getName())) { + // Server confirmed that we can use zlib compression + connection.startCompression(); + Log.debug("CM - Stream compression was successful with " + serverName); + // Stream compression was successful so initiate a new stream + connection.deliverRawText(openingStream.toString()); + // Reset the parser to use stream compression over TLS + ZInputStream in = + new ZInputStream(connection.getTLSStreamHandler().getInputStream()); + in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + xpp.setInput(new InputStreamReader(in, CHARSET)); + // Skip the opening stream sent by the server + for (int eventType = xpp.getEventType(); + eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + // Get new stream features + features = reader.parseDocument().getRootElement(); + return true; + } else { + Log.debug("CM - Stream compression was rejected by " + serverName); + } + } else { + Log.debug( + "CM - Stream compression found but zlib method is not supported by" + + serverName); + } + return false; + } + return true; + } + + private boolean doHandshake(String streamID, XMPPPacketReader reader) throws Exception { + String sharedSecret = JiveGlobals.getXMLProperty("xmpp.sharedSecret"); + if (sharedSecret == null) { + // No shared secret was configued in the connection manager + Log.debug("CM - No shared secret was found. Configure xmpp.sharedSecret property"); + return false; + } + MessageDigest digest; + // Create a message digest instance. + try { + digest = MessageDigest.getInstance("SHA"); + } + catch (NoSuchAlgorithmException e) { + Log.error(LocaleUtils.getLocalizedString("admin.error"), e); + return false; + } + + digest.update(streamID.getBytes()); + String key = StringUtils.encodeHex(digest.digest(sharedSecret.getBytes())); + + Log.debug("OS - Sent handshake to host: " + serverName + " id: " + streamID); + + // Send handshake to server + StringBuilder sb = new StringBuilder(); + sb.append("").append(key).append(""); + connection.deliverRawText(sb.toString()); + + // Wait for the response + Element proceed = reader.parseDocument().getRootElement(); + if (proceed != null && proceed.getName().equals("handshake")) { + Log.debug("OS - Handshake was SUCCESSFUL with host: " + serverName + " id: " + + streamID); + return true; + } + Log.debug("OS - Handshake FAILED with host: " + serverName + " id: " + streamID); + return false; + } + + private int[] decodeVersion(String version) { + int[] answer = new int[]{0, 0}; + String [] versionString = version.split("\\."); + answer[0] = Integer.parseInt(versionString[0]); + answer[1] = Integer.parseInt(versionString[1]); + return answer; + } + + /** + * Creates a reader that will process incoming packets from the server. Incoming + * stanzas will be handled by {@link ServerPacketHandler} through a pool of + * threads. + * + * @param reader the reader to use to retrieve stanzas. + */ + private void createSocketReader(XMPPPacketReader reader) { + ServerPacketReader serverPacketReader = + new ServerPacketReader(reader, connection, jidAddress); + connection.setSocketStatistic(serverPacketReader); + } + + /** + * Sends a notification to the main server that a new client session has been created. + * + * @param streamID the stream ID assigned by the connection manager to the new session. + */ + public void clientSessionCreated(String streamID) { + Element iq = docFactory.createDocument().addElement("iq"); + iq.addAttribute("type", "set"); + iq.addAttribute("to", serverName); + iq.addAttribute("from", jidAddress); + iq.addAttribute("id", String.valueOf(random.nextInt(1000) + "-" + sequence++)); + Element child = iq.addElement("session", "http://jabber.org/protocol/connectionmanager"); + child.addAttribute("id", streamID); + child.addElement("create"); + // Forward the notification to the server + connection.deliver(iq); + } + + /** + * Sends a notification to the main server that a client session has been closed. + * + * @param streamID the stream ID assigned by the connection manager to the closed session. + */ + public void clientSessionClosed(String streamID) { + Element iq = docFactory.createDocument().addElement("iq"); + iq.addAttribute("type", "set"); + iq.addAttribute("to", serverName); + iq.addAttribute("from", jidAddress); + iq.addAttribute("id", String.valueOf(random.nextInt(1000) + "-" + sequence++)); + Element child = iq.addElement("session", "http://jabber.org/protocol/connectionmanager"); + child.addAttribute("id", streamID); + child.addElement("close"); + // Forward the notification to the server + connection.deliver(iq); + } + + /** + * Sends notification to the main server that delivery of a stanza to a client has + * failed. + * + * @param stanza the stanza that was not sent to the client. + * @param streamID the stream ID assigned by the connection manager to the no + * longer available session. + */ + public void deliveryFailed(Element stanza, String streamID) { + Element iq = docFactory.createDocument().addElement("iq"); + iq.addAttribute("type", "set"); + iq.addAttribute("to", serverName); + iq.addAttribute("from", jidAddress); + iq.addAttribute("id", String.valueOf(random.nextInt(1000) + "-" + sequence++)); + Element child = iq.addElement("session", "http://jabber.org/protocol/connectionmanager"); + child.addAttribute("id", streamID); + child.addElement("failed").add(stanza.createCopy()); + // Send notification to the server + connection.deliver(iq); + } + + public void run() { + try { + super.run(); + } + catch(IllegalStateException e) { + // Do not print this exception that was thrown to stop this thread when + // it was detected that the connection was closed before using this thread + } + finally { + // Remove this thread/connection from the list of available connections + ConnectionManager.getInstance().getServerSurrogate().serverConnections.remove(getName()); + // Close the connection + connection.close(); + } + } + + /** + * Indicates the server that the connection manager is being shut down. + */ + void notifySystemShutdown() { + connection.systemShutdown(); + } + + /** + * Delivers clients traffic to the server. The client session that originated + * the traffic is specified by the streamID attribute. Clients traffic is wrapped + * by a route element. + * + * @param stanza the original client stanza that is going to be wrapped. + * @param streamID the stream ID assigned by the connection manager to the client session. + */ + public void deliver(Element stanza, String streamID) { + // Wrap the stanza + Element wrapper = docFactory.createDocument().addElement("route"); + wrapper.addAttribute("to", serverName); + wrapper.addAttribute("from", jidAddress); + wrapper.addAttribute("streamid", streamID); + wrapper.add(stanza.createCopy()); + // Forward the wrapped stanza to the server + connection.deliver(wrapper); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/PacketDeliverer.java b/src/java/org/jivesoftware/multiplexer/PacketDeliverer.java new file mode 100644 index 0000000..50629e1 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/PacketDeliverer.java @@ -0,0 +1,34 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; + +/** + * Delivers packets to locally connected streams. This is the opposite + * of the packet transporter. + * + * @author Iain Shigeoka + */ +public interface PacketDeliverer { + + /** + * Delivers the given packet based on packet recipient and sender. The + * deliverer defers actual routing decisions to other classes. + *

Warning

+ * Be careful to enforce concurrency DbC of concurrent by synchronizing + * any accesses to class resources. + * + * @param doc the packet to route + */ + public void deliver(Element doc); +} diff --git a/src/java/org/jivesoftware/multiplexer/PacketRouter.java b/src/java/org/jivesoftware/multiplexer/PacketRouter.java new file mode 100644 index 0000000..9301e51 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/PacketRouter.java @@ -0,0 +1,32 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; + +/** + * A router that handles incoming packets. Packets will be routed to their + * corresponding handler. A router is much like a forwarded with some logic + * to figute out who is the target for each packet. + * + * @author Gaston Dombiak + */ +public interface PacketRouter { + + /** + * Routes the given packet based on its type. + * + * @param doc The packet to route. + * @param streamID The ID of the client's stream. + */ + void route(Element doc, String streamID); +} diff --git a/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java b/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java new file mode 100644 index 0000000..c532592 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java @@ -0,0 +1,200 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; +import org.jivesoftware.multiplexer.net.SocketConnection; +import org.jivesoftware.util.Log; + +/** + * A ServerPacketHandler is responsible for handling stanzas sent from the server. For each + * server connection there is going to be an instance of this class.

+ * + * Route stanzas are forwarded to clients. IQ stanzas are used when the server wants to + * close a client connection or wants to update the clients connections configurations. + * Stream errors with condition system-shutdown indicate that the server is + * shutting down. The connection manager will close existing client connections but + * will keep running. + * + * @author Gaston Dombiak + */ +class ServerPacketHandler { + + private ConnectionManager connectionManager = ConnectionManager.getInstance(); + + /** + * Connection to the server. + */ + private SocketConnection connection; + /** + * JID that identifies this connection to the server. The address is composed by + * the connection manager name and the name of the thread. e.g.: connManager1/thread1 + */ + private String jidAddress; + + public ServerPacketHandler(SocketConnection connection, String jidAddress) { + this.connection = connection; + this.jidAddress = jidAddress; + } + + /** + * Handles stanza sent from the server. Route stanzas are forwarded to clients. IQ + * stanzas are used when the server wants to close a client connection or wants to + * update the clients connections configurations. Stream errors with condition + * system-shutdown indicate that the server is shutting down. The connection + * manager will close existing client connections but will keep running. + * + * @param stanza stanza sent from the server. + */ + public void handle(Element stanza) { + String tag = stanza.getName(); + if ("route".equals(tag)) { + // Process wrapped packets + processRoute(stanza); + } + else if ("iq".equals(tag)) { + String type = stanza.attributeValue("type"); + if ("set".equals(type)) { + Element wrapper = stanza.element("session"); + if (wrapper != null) { + String streamID = wrapper.attributeValue("id"); + // Check if the server is informing us that we need to close a session + if (wrapper.element("close") != null) { + // Get the session that matches the requested stream ID + ClientSession session = ClientSession.getSession(streamID); + if (session != null) { + session.close(); + } + } else { + Log.warn("Invalid IQ stanza of type SET was received: " + stanza.asXML()); + } + } else { + Element configuration = stanza.element("configuration"); + if (configuration != null) { + obtainClientOptions(stanza, configuration); + } else { + Log.warn("Invalid IQ stanza of type SET was received: " + stanza.asXML()); + } + } + } else if ("result".equals(type)) { + if (Log.isDebugEnabled()) { + Log.debug("IQ stanza of type RESULT was discarded: " + stanza.asXML()); + } + } else { + Log.warn("IQ stanza with invalid type was discarded: " + stanza.asXML()); + } + } else if ("error".equals(tag) && "stream".equals(stanza.getNamespacePrefix())) { + if (stanza.element("system-shutdown") != null) { + // Close connections to the server and client connections. The connection + // manager will still be running and accepting client connections. New + // connections to the server will be created on demand. + connectionManager.getServerSurrogate().closeAll(); + } else { + // Some stream error was sent from the server + Log.warn("Server sent unexpected stream error: " + stanza.asXML()); + } + } else { + Log.warn("Unknown stanza type sent to Connection Manager: " + stanza.asXML()); + } + } + + /** + * Forwards wrapped stanza contained in the route element to the specified + * client. The target client connection is specified in the route element by + * the streamid attribute.

+ * + * Wrapped stanzas that failed to be delivered to the target client are returned to + * the server. + * + * @param route the route element containing the wrapped stanza to send to the target + * client. + */ + private void processRoute(Element route) { + String streamID = route.attributeValue("streamid"); + // Get the wrapped stanza + Element stanza = (Element) route.elementIterator().next(); + // Get the session that matches the requested stream ID + ClientSession session = ClientSession.getSession(streamID); + if (session != null) { + // Deliver the wrapped stanza to the client + session.deliver(stanza); + } else { + // Inform the server that the wrapped stanza was not delivered + String tag = stanza.getName(); + if ("message".equals(tag)) { + connectionManager.getServerSurrogate().deliveryFailed(stanza, streamID); + } + else if ("iq".equals(tag)) { + String type = stanza.attributeValue("type", "get"); + if ("get".equals(type) || "set".equals(type)) { + // Build IQ of type ERROR + Element reply = stanza.createCopy(); + reply.addAttribute("type", "error"); + reply.addAttribute("from", stanza.attributeValue("to")); + reply.addAttribute("to", stanza.attributeValue("from")); + Element error = reply.addElement("error"); + error.addAttribute("type", "wait"); + error.addElement("unexpected-request") + .addAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas"); + // Bounce the failed IQ packet + connectionManager.getServerSurrogate().send(reply, streamID); + } + } + } + } + + /** + * Processes server configuration to use for client connections and store the + * configuration in {@link ServerSurrogate}. + * + * @param stanza stanza sent from the server containing the configuration. + * @param configuration the configuration element contained in the stanza. + */ + private void obtainClientOptions(Element stanza, Element configuration) { + ServerSurrogate serverSurrogate = connectionManager.getServerSurrogate(); + // Check if TLS is avaiable (and if it is required) + Element startTLS = configuration.element("starttls"); + if (startTLS != null) { + if (startTLS.element("required") != null) { + serverSurrogate.setTlsPolicy(Connection.TLSPolicy.required); + } else { + serverSurrogate.setTlsPolicy(Connection.TLSPolicy.optional); + } + } else { + serverSurrogate.setTlsPolicy(Connection.TLSPolicy.disabled); + } + // Check if compression is available + Element compression = configuration.element("compression"); + if (compression != null) { + serverSurrogate.setCompressionPolicy(Connection.CompressionPolicy.optional); + } else { + serverSurrogate.setCompressionPolicy(Connection.CompressionPolicy.disabled); + } + // Cache supported SASL mechanisms for client authentication + Element mechanisms = configuration.element("mechanisms"); + if (mechanisms != null) { + serverSurrogate.setSASLMechanisms(mechanisms); + } + // Check if anonymous login is supported + serverSurrogate.setNonSASLAuthEnabled(configuration.element("auth") != null); + // Check if in-band registration is supported + serverSurrogate.setInbandRegEnabled(configuration.element("register") != null); + + // Send ACK to the server + Element reply = stanza.createCopy(); + reply.addAttribute("type", "result"); + reply.addAttribute("to", connectionManager.getServerName()); + reply.addAttribute("from", jidAddress); + connection.deliver(reply); + } +} + diff --git a/src/java/org/jivesoftware/multiplexer/ServerPacketReader.java b/src/java/org/jivesoftware/multiplexer/ServerPacketReader.java new file mode 100644 index 0000000..165126d --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ServerPacketReader.java @@ -0,0 +1,131 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; +import org.dom4j.io.XMPPPacketReader; +import org.jivesoftware.multiplexer.net.SocketConnection; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.Log; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Reads and processes stanzas sent from the server. Each connection to the server will + * have an instance of this class. Read packets will be processed using a thread pool. + * By default, the thread pool will have 5 processing threads. Configure the property + * xmpp.manager.incoming.threads to change the number of processing threads + * per connection to the server. + * + * @author Gaston Dombiak + */ +class ServerPacketReader implements SocketStatistic { + + private boolean open = true; + private XMPPPacketReader reader = null; + + /** + * Pool of threads that will process incoming stanzas from the server. + */ + private ThreadPoolExecutor threadPool; + /** + * Actual object responsible for handling incoming traffic. + */ + private ServerPacketHandler packetsHandler; + + public ServerPacketReader(XMPPPacketReader reader, SocketConnection connection, + String address) { + this.reader = reader; + packetsHandler = new ServerPacketHandler(connection, address); + init(); + } + + private void init() { + // Create a pool of threads that will process incoming packets. + int maxThreads = JiveGlobals.getIntProperty("xmpp.manager.incoming.threads", 5); + if (maxThreads < 1) { + // Ensure that the max number of threads in the pool is at least 1 + maxThreads = 1; + } + threadPool = + new ThreadPoolExecutor(maxThreads, maxThreads, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue(), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // Create a thread that will read and store DOM Elements. + Thread thread = new Thread("Server Packet Reader") { + public void run() { + while (open) { + Element doc; + try { + doc = reader.parseDocument().getRootElement(); + + if (doc == null) { + // Stop reading the stream since the remote server has sent an end of + // stream element and probably closed the connection. + shutdown(); + } + else { + // Queue task that process incoming stanzas + threadPool.execute(new ProcessStanzaTask(packetsHandler, doc)); + } + } + catch (IOException e) { + Log.debug("Finishing Incoming Server Stanzas Reader.", e); + shutdown(); + } + catch (Exception e) { + Log.error("Finishing Incoming Server Stanzas Reader.", e); + shutdown(); + } + } + } + }; + thread.setDaemon(true); + thread.start(); + } + + public long getLastActive() { + return reader.getLastActive(); + } + + public void shutdown() { + open = false; + threadPool.shutdown(); + } + + /** + * Task that processes incoming stanzas from the server. + */ + private class ProcessStanzaTask implements Runnable { + /** + * Incoming stanza to process. + */ + private Element stanza; + /** + * Actual object responsible for handling incoming traffic. + */ + private ServerPacketHandler packetsHandler; + + public ProcessStanzaTask(ServerPacketHandler packetsHandler, Element stanza) { + this.packetsHandler = packetsHandler; + this.stanza = stanza; + } + + public void run() { + packetsHandler.handle(stanza); + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/ServerPort.java b/src/java/org/jivesoftware/multiplexer/ServerPort.java new file mode 100644 index 0000000..034445d --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ServerPort.java @@ -0,0 +1,138 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Represents a port on which the server will listen for connections. + * Used to aggregate information that the rest of the system needs + * regarding the port while hiding implementation details. + * + * @author Iain Shigeoka + */ +public class ServerPort { + + private int port; + private ArrayList names; + private String address; + private boolean secure; + private String algorithm; + private Type type; + + public ServerPort(int port, String name, String address, + boolean isSecure, String algorithm, Type type) + { + this.port = port; + this.names = new ArrayList(1); + this.names.add(name); + this.address = address; + this.secure = isSecure; + this.algorithm = algorithm; + this.type = type; + } + + /** + * Returns the port number that is being used. + * + * @return the port number this server port is listening on. + */ + public int getPort() { + return port; + } + + /** + * Returns the logical domains for this server port. As multiple + * domains may point to the same server, this helps to define what + * the server considers "local". + * + * @return the server domain name(s) as Strings. + */ + public Iterator getDomainNames() { + return names.iterator(); + } + + /** + * Returns the dot separated IP address for the server. + * + * @return The dot separated IP address for the server + */ + public String getIPAddress() { + return address; + } + + /** + * Determines if the connection is secure. + * + * @return True if the connection is secure + */ + public boolean isSecure() { + return secure; + } + + /** + * Returns the basic protocol/algorithm being used to secure + * the port connections. An example would be "SSL" or "TLS". + * + * @return The protocol used or null if this is not a secure server port + */ + public String getSecurityType() { + return algorithm; + } + + /** + * Returns true if other servers can connect to this port for s2s communication. + * + * @return true if other servers can connect to this port for s2s communication. + */ + public boolean isServerPort() { + return type == Type.server; + } + + /** + * Returns true if clients can connect to this port. + * + * @return true if clients can connect to this port. + */ + public boolean isClientPort() { + return type == Type.client; + } + + /** + * Returns true if external components can connect to this port. + * + * @return true if external components can connect to this port. + */ + public boolean isComponentPort() { + return type == Type.component; + } + + /** + * Returns true if connection managers can connect to this port. + * + * @return true if connection managers can connect to this port. + */ + public boolean isConnectionManagerPort() { + return type == Type.connectionManager; + } + + public static enum Type { + client, + + server, + + component, + + connectionManager + } +} diff --git a/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java b/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java new file mode 100644 index 0000000..1617f95 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java @@ -0,0 +1,386 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.Element; +import org.jivesoftware.multiplexer.task.CloseSessionTask; +import org.jivesoftware.multiplexer.task.DeliveryFailedTask; +import org.jivesoftware.multiplexer.task.NewSessionTask; +import org.jivesoftware.multiplexer.task.RouteTask; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.Log; + +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Surrogate of the main server where the Connection Manager is routing client + * packets. This class is responsible for keeping a pool of working threads to + * processing incoming clients traffic and forward it to the main server. Each working + * thread uses its own connection to the server. By default 5 threads/connections + * are established to the server. Use the system property xmpp.manager.connections + * to modify the default value.

+ * + * ServerSurrogate is also responsible for caching the server configuration such as if + * non-sasl authentication or in-band registration are available.

+ * + * Each connection to the server has its own {@link ServerPacketReader} to read incoming + * traffic from the server. Incoming server traffic is then handled by + * {@link ServerPacketHandler}. + * + * @author Gaston Dombiak + */ +public class ServerSurrogate { + + /** + * TLS policy to use for clients. + */ + private Connection.TLSPolicy tlsPolicy = Connection.TLSPolicy.optional; + + /** + * Compression policy to use for clients. + */ + private Connection.CompressionPolicy compressionPolicy = Connection.CompressionPolicy.disabled; + + /** + * Cache the SASL mechanisms supported by the server for client authentication + */ + private String saslMechanisms; + /** + * Flag indicating if non-sasl authentication is supported by the server. + */ + private boolean nonSASLEnabled; + /** + * Flag indicating if in-band registration is supported by the server. + */ + private boolean inbandRegEnabled; + + /** + * Pool of threads that will send stanzas to the server. The number of threads + * in the pool will match the number of connections to the server. + */ + private ThreadPoolExecutor threadPool; + /** + * Map that holds the list of connections to the server. + * Key: thread name, Value: ConnectionWorkerThread. + */ + Map serverConnections = + new ConcurrentHashMap(0); + + ServerSurrogate() { + } + + void start() { + // Create empty thread pool + createThreadPool(); + // Populate thread pool with threads that will include connections to the server + threadPool.prestartAllCoreThreads(); + // Start thread that will send heartbeats to the server every 30 seconds + // to keep connections to the server open. + Thread hearbeatThread = new Thread() { + public void run() { + while (true) { + try { + Thread.sleep(30000); + for (ConnectionWorkerThread thread : serverConnections.values()) { + thread.getConnection().deliverRawText(" "); + } + } + catch (InterruptedException e) { + // Do nothing + } + catch(Exception e) { + Log.error(e); + } + } + } + }; + hearbeatThread.setDaemon(true); + hearbeatThread.setPriority(Thread.NORM_PRIORITY); + hearbeatThread.start(); + } + + /** + * Closes existing connections to the server. A new thread pool will be created + * but no connections will be created. New connections will be created on demand. + */ + void closeAll() { + shutdown(true); + // Create new thread pool but this time do not populate it + createThreadPool(); + } + + /** + * Closes connections of connected clients and stops forwarding clients traffic to + * the server. If the server is the one that requested to stop forwarding traffic + * then stop doing it now. This means that queued packets will be discarded, otherwise + * stop queuing packet but continue processing queued packets. + * + * @param now true if forwarding packets should be done now. + */ + void shutdown(boolean now) { + // Disconnect connected clients + ClientSession.closeAll(); + // Shutdown the threads that send stanzas to the server + if (now) { + threadPool.shutdownNow(); + } + else { + threadPool.shutdown(); + } + } + + /** + * Notification message indication that a new client session has been created. Send + * a notification to the main server. + * + * @param streamID the stream ID assigned by the connection manager to the new session. + */ + void clientSessionCreated(final String streamID) { + threadPool.execute(new NewSessionTask(streamID)); + } + + /** + * Notification message indication that a client session has been closed. Send + * a notification to the main server. + * + * @param streamID the stream ID assigned by the connection manager to the session. + */ + void clientSessionClosed(final String streamID) { + threadPool.execute(new CloseSessionTask(streamID)); + } + + /** + * Notification message indicating that delivery of a stanza to a client has + * failed. + * + * @param stanza the stanza that was not sent to the client. + * @param streamID the stream ID assigned by the connection manager to the no + * longer available session. + */ + public void deliveryFailed(Element stanza, String streamID) { + threadPool.execute(new DeliveryFailedTask(streamID, stanza)); + } + + /** + * Forwards the specified stanza to the server. The client that is sending the + * stanza is specified by the streamID parameter. + * + * @param stanza the stanza to send to the server. + * @param streamID the stream ID assigned by the connection manager to the session. + */ + public void send(Element stanza, String streamID) { + threadPool.execute(new RouteTask(streamID, stanza)); + } + + /** + * Returns the SASL mechanisms supported by the server for client authentication. + * + * @param session the session connecting to the connection manager. + * @return the SASL mechanisms supported by the server for client authentication. + */ + public String getSASLMechanisms(Session session) { + return saslMechanisms; + } + + /** + * Returns the SASL mechanisms supported by the server for client authentication. + * + * @param mechanisms the SASL mechanisms supported by the server for client authentication. + */ + public void setSASLMechanisms(Element mechanisms) { + saslMechanisms = mechanisms.asXML(); + } + + /** + * Returns whether TLS is mandatory, optional or is disabled. When TLS is mandatory clients + * are required to secure their connections or otherwise their connections will be closed. + * On the other hand, when TLS is disabled clients are not allowed to secure their connections + * using TLS. Their connections will be closed if they try to secure the connection. in this + * last case. + * + * @return whether TLS is mandatory, optional or is disabled. + */ + public Connection.TLSPolicy getTlsPolicy() { + return tlsPolicy; + } + + /** + * Sets whether TLS is mandatory, optional or is disabled. When TLS is mandatory clients + * are required to secure their connections or otherwise their connections will be closed. + * On the other hand, when TLS is disabled clients are not allowed to secure their connections + * using TLS. Their connections will be closed if they try to secure the connection. in this + * last case. + * + * @param tlsPolicy whether TLS is mandatory, optional or is disabled. + */ + public void setTlsPolicy(Connection.TLSPolicy tlsPolicy) { + this.tlsPolicy = tlsPolicy; + } + + /** + * Returns whether compression is optional or is disabled. + * + * @return whether compression is optional or is disabled. + */ + public Connection.CompressionPolicy getCompressionPolicy() { + return compressionPolicy; + } + + /** + * Sets whether compression is enabled or is disabled.

+ * + * Note: Connection managers share the same code from Wildfire so the same compression + * algorithms will be offered. + * // TODO When used with other server we need to store the available algorithms. + * + * @param compressionPolicy whether Compression is enabled or is disabled. + */ + public void setCompressionPolicy(Connection.CompressionPolicy compressionPolicy) { + this.compressionPolicy = compressionPolicy; + } + + /** + * Returns true if non-sasl authentication is supported by the server. + * + * @return true if non-sasl authentication is supported by the server. + */ + public boolean isNonSASLAuthEnabled() { + return nonSASLEnabled; + } + + /** + * Sets if non-sasl authentication is supported by the server. + * + * @param nonSASLEnabled if non-sasl authentication is supported by the server. + */ + public void setNonSASLAuthEnabled(boolean nonSASLEnabled) { + this.nonSASLEnabled = nonSASLEnabled; + } + + /** + * Returns true if in-band registration is supported by the server. + * + * @return true if in-band registration is supported by the server. + */ + public boolean isInbandRegEnabled() { + return inbandRegEnabled; + } + + /** + * Sets if in-band registration is supported by the server. + * + * @param inbandRegEnabled if in-band registration is supported by the server. + */ + public void setInbandRegEnabled(boolean inbandRegEnabled) { + this.inbandRegEnabled = inbandRegEnabled; + } + + /** + * Creates a new thread pool that will not contain any thread. So new connections + * won't be created to the server at this point. + */ + private void createThreadPool() { + int maxConnections = JiveGlobals.getIntProperty("xmpp.manager.connections", 5); + // Create a pool of threads that will process queued packets. + threadPool = new ConnectionWorkerThreadPool(maxConnections, maxConnections, 60, + TimeUnit.SECONDS, new LinkedBlockingQueue(), + new ConnectionsWorkerFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); + } + + /** + * ThreadPoolExecutor that verifies connection status before executing a task. If + * the connection is invalid then the worker thread will be dismissed and the task + * will be injected into the pool again. + */ + private class ConnectionWorkerThreadPool extends ThreadPoolExecutor { + public ConnectionWorkerThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, + handler); + } + + protected void beforeExecute(Thread thread, Runnable task) { + super.beforeExecute(thread, task); + ConnectionWorkerThread workerThread = (ConnectionWorkerThread) thread; + // Check that the worker thread is valid. This means that it has a valid connection + // to the server + if (!workerThread.isValid()) { + // Request other thread to process the task. In fact, a new thread + // will be created by the + execute(task); + // Throw an exception so that this worker is dismissed + throw new IllegalStateException( + "There is no connection to the server or connection is lost."); + } + } + + public void shutdown() { + // Notify the server that the connection manager is being shut down + execute(new Runnable() { + public void run() { + ConnectionWorkerThread thread = (ConnectionWorkerThread) Thread.currentThread(); + thread.notifySystemShutdown(); + } + }); + // Stop the workers and shutdown + super.shutdown(); + } + } + + /** + * Factory of threads where is thread will create and keep its own connection + * to the server. If creating new connections to the server failes 2 consecutive + * times then existing client connections will be closed. + */ + private class ConnectionsWorkerFactory implements ThreadFactory { + final ThreadGroup group; + final AtomicInteger threadNumber = new AtomicInteger(1); + final AtomicInteger failedAttempts = new AtomicInteger(0); + + ConnectionsWorkerFactory() { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + } + + public Thread newThread(Runnable r) { + // Create new worker thread that will include a connection to the server + ConnectionWorkerThread t = new ConnectionWorkerThread(group, r, + "Connection Worker - " + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + // Return null if failed to create worker thread + if (!t.isValid()) { + int attempts = failedAttempts.incrementAndGet(); + if (attempts == 2 && serverConnections.size() == 0) { + // Server seems to be unavailable so close existing client connections + closeAll(); + // Clean up the counter of failed attemps to create new connections + failedAttempts.set(0); + } + return null; + } + // Clean up the counter of failed attemps to create new connections + failedAttempts.set(0); + // Update number of available connections to the server + serverConnections.put(t.getName(), t); + return t; + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/Session.java b/src/java/org/jivesoftware/multiplexer/Session.java new file mode 100644 index 0000000..27769db --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/Session.java @@ -0,0 +1,148 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import java.util.Date; + +/** + * The session represents a connection between the server and a client (c2s) or + * another server (s2s) as well as a connection with a component. Authentication and + * user accounts are associated with c2s connections while s2s has an optional authentication + * association but no single user user.

+ * + * Obtain object managers from the session in order to access server resources. + * + * @author Gaston Dombiak + */ +public abstract class Session { + + /** + * Version of the XMPP spec supported as MAJOR_VERSION.MINOR_VERSION (e.g. 1.0). + */ + public static final int MAJOR_VERSION = 1; + public static final int MINOR_VERSION = 0; + + /** + * The utf-8 charset for decoding and encoding Jabber packet streams. + */ + protected static String CHARSET = "UTF-8"; + + public static final int STATUS_CLOSED = -1; + public static final int STATUS_CONNECTED = 1; + public static final int STATUS_STREAMING = 2; + public static final int STATUS_AUTHENTICATED = 3; + + /** + * The stream id for this session (random and unique). + */ + private String streamID; + + /** + * The current session status. + */ + protected int status = STATUS_CONNECTED; + + /** + * The connection that this session represents. + */ + protected Connection conn; + + private String serverName; + + private Date startDate = new Date(); + + /** + * Creates a session with an underlying connection and permission protection. + * + * @param connection The connection we are proxying + */ + public Session(String serverName, Connection connection, String streamID) { + conn = connection; + this.streamID = streamID; + this.serverName = serverName; + } + + /** + * Obtain the current status of this session. + * + * @return The status code for this session + */ + public int getStatus() { + return status; + } + + /** + * Set the new status of this session. Setting a status may trigger + * certain events to occur (setting a closed status will close this + * session). + * + * @param status The new status code for this session + */ + public void setStatus(int status) { + this.status = status; + } + + /** + * Obtain the stream ID associated with this sesison. Stream ID's are generated by the server + * and should be unique and random. + * + * @return This session's assigned stream ID + */ + public String getStreamID() { + return streamID; + } + + /** + * Obtain the name of the server this session belongs to. + * + * @return the server name. + */ + public String getServerName() { + return serverName; + } + + /** + * Obtain the date the session was created. + * + * @return the session's creation date. + */ + public Date getCreationDate() { + return startDate; + } + + /** + * Returns a text with the available stream features. Each subclass may return different + * values depending whether the session has been authenticated or not. + * + * @return a text with the available stream features or null to add nothing. + */ + public abstract String getAvailableStreamFeatures(); + + /** + * Indicate the server that the session has been closed. Do nothing if the session + * was the one that originated the close action. + */ + public abstract void close(); + + public String toString() { + return super.toString() + " status: " + status + " id: " + streamID; + } + + protected static int[] decodeVersion(String version) { + int[] answer = new int[] {0 , 0}; + String [] versionString = version.split("\\."); + answer[0] = Integer.parseInt(versionString[0]); + answer[1] = Integer.parseInt(versionString[1]); + return answer; + } + +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/SocketStatistic.java b/src/java/org/jivesoftware/multiplexer/SocketStatistic.java new file mode 100644 index 0000000..326aebb --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/SocketStatistic.java @@ -0,0 +1,29 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +/** + * Interface that keeps statistics of sockets. Currently the only supported statistic + * is the last time a socket received a stanza or a heartbeat. + * + * @author Gaston Dombiak + */ +public interface SocketStatistic { + + /** + * Returns the last time a stanza was read or a heartbeat was received. Hearbeats + * are represented as whitespaces received while a Document is not being parsed. + * + * @return the time in milliseconds when the last stanza or heartbeat was received. + */ + public long getLastActive(); +} diff --git a/src/java/org/jivesoftware/multiplexer/StreamError.java b/src/java/org/jivesoftware/multiplexer/StreamError.java new file mode 100644 index 0000000..6b126c0 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/StreamError.java @@ -0,0 +1,485 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import org.dom4j.*; +import org.dom4j.io.XMLWriter; +import org.dom4j.io.OutputFormat; + +import java.util.Iterator; +import java.io.StringWriter; + +/** + * A stream error. Stream errors have a condition and they + * can optionally include explanation text. + * + * @author Matt Tucker + */ +public class StreamError { + + private static final String ERROR_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-streams"; + + private static DocumentFactory docFactory = DocumentFactory.getInstance(); + + private Element element; + + /** + * Construcs a new StreamError with the specified condition. + * + * @param condition the error condition. + */ + public StreamError(Condition condition) { + this.element = docFactory.createElement(docFactory.createQName("error", "stream", + "http://etherx.jabber.org/streams")); + setCondition(condition); + } + + /** + * Constructs a new StreamError with the specified condition and error text. + * + * @param condition the error condition. + * @param text the text description of the error. + */ + public StreamError(Condition condition, String text) { + this.element = docFactory.createElement(docFactory.createQName("error", "stream", + "http://etherx.jabber.org/streams")); + setCondition(condition); + setText(text, null); + } + + /** + * Constructs a new StreamError with the specified condition and error text. + * + * @param condition the error condition. + * @param text the text description of the error. + * @param language the language code of the error description (e.g. "en"). + */ + public StreamError(Condition condition, String text, String language) { + this.element = docFactory.createElement(docFactory.createQName("error", "stream", + "http://etherx.jabber.org/streams")); + setCondition(condition); + setText(text, language); + } + + /** + * Constructs a new StreamError using an existing Element. This is useful + * for parsing incoming error Elements into StreamError objects. + * + * @param element the stream error Element. + */ + public StreamError(Element element) { + this.element = element; + } + + /** + * Returns the error condition. + * + * @return the error condition. + * @see Condition + */ + public Condition getCondition() { + for (Iterator i=element.elementIterator(); i.hasNext(); ) { + Element el = (Element)i.next(); + if (el.getNamespaceURI().equals(ERROR_NAMESPACE) && + !el.getName().equals("text")) + { + return Condition.fromXMPP(el.getName()); + } + } + return null; + } + + /** + * Sets the error condition. + * + * @param condition the error condition. + * @see Condition + */ + public void setCondition(Condition condition) { + if (condition == null) { + throw new NullPointerException("Condition cannot be null"); + } + Element conditionElement = null; + for (Iterator i=element.elementIterator(); i.hasNext(); ) { + Element el = (Element)i.next(); + if (el.getNamespaceURI().equals(ERROR_NAMESPACE) && + !el.getName().equals("text")) + { + conditionElement = el; + } + } + if (conditionElement != null) { + element.remove(conditionElement); + } + + conditionElement = docFactory.createElement(condition.toXMPP(), ERROR_NAMESPACE); + element.add(conditionElement); + } + + /** + * Returns a text description of the error, or null if there + * is no text description. + * + * @return the text description of the error. + */ + public String getText() { + return element.elementText("text"); + } + + /** + * Sets the text description of the error. + * + * @param text the text description of the error. + */ + public void setText(String text) { + setText(text, null); + } + + /** + * Sets the text description of the error. Optionally, a language code + * can be specified to indicate the language of the description. + * + * @param text the text description of the error. + * @param language the language code of the description, or null to specify + * no language code. + */ + public void setText(String text, String language) { + Element textElement = element.element("text"); + // If text is null, clear the text. + if (text == null) { + if (textElement != null) { + element.remove(textElement); + } + return; + } + + if (textElement == null) { + textElement = docFactory.createElement("text", ERROR_NAMESPACE); + if (language != null) { + textElement.addAttribute(QName.get("lang", "xml", + "http://www.w3.org/XML/1998/namespace"), language); + } + element.add(textElement); + } + textElement.setText(text); + } + + /** + * Returns the text description's language code, or null if there + * is no language code associated with the description text. + * + * @return the language code of the text description, if it exists. + */ + public String getTextLanguage() { + Element textElement = element.element("text"); + if (textElement != null) { + return textElement.attributeValue(QName.get("lang", "xml", + "http://www.w3.org/XML/1998/namespace")); + } + return null; + } + + /** + * Returns the DOM4J Element that backs the error. The element is the definitive + * representation of the error and can be manipulated directly to change + * error contents. + * + * @return the DOM4J Element. + */ + public Element getElement() { + return element; + } + + /** + * Returns the textual XML representation of this stream error. + * + * @return the textual XML representation of this stream error. + */ + public String toXML() { + return element.asXML(); + } + + public String toString() { + StringWriter out = new StringWriter(); + XMLWriter writer = new XMLWriter(out, OutputFormat.createPrettyPrint()); + try { + writer.write(element); + } + catch (Exception e) { } + return out.toString(); + } + + /** + * Type-safe enumeration for the error condition.

+ * + * Implementation note: XMPP error conditions use "-" characters in + * their names such as "bad-request". Because "-" characters are not valid + * identifier parts in Java, they have been converted to "_" characters in + * the enumeration names, such as bad_request. The {@link #toXMPP()} and + * {@link #fromXMPP(String)} methods can be used to convert between the + * enumertation values and XMPP error code strings. + */ + public enum Condition { + + /** + * The entity has sent XML that cannot be processed; this error MAY be used + * instead of the more specific XML-related errors, such as <bad-namespace-prefix/>, + * <invalid-xml/>, <restricted-xml/>, <unsupported-encoding/>, and + * <xml-not-well-formed/>, although the more specific errors are preferred. + */ + bad_format("bad-format"), + + /** + * The entity has sent a namespace prefix that is unsupported, or has sent no + * namespace prefix on an element that requires such a prefix. + */ + bad_namespace_prefix("bad-namespace-prefix"), + + /** + * The server is closing the active stream for this entity because a new stream + * has been initiated that conflicts with the existing stream. + */ + conflict("conflict"), + + /** + * The entity has not generated any traffic over the stream for some period of + * time (configurable according to a local service policy). + */ + connection_timeout("connection-timeout"), + + /** + * The value of the 'to' attribute provided by the initiating entity in the + * stream header corresponds to a hostname that is no longer hosted by the server. + */ + host_gone("host-gone"), + + /** + * The value of the 'to' attribute provided by the initiating entity in the + * stream header does not correspond to a hostname that is hosted by the server. + */ + host_unknown("host-unknown"), + + /** + * A stanza sent between two servers lacks a 'to' or 'from' attribute + * (or the attribute has no value). + */ + improper_addressing("improper-addressing"), + + /** + * The server has experienced a misconfiguration or an otherwise-undefined + * internal error that prevents it from servicing the stream. + */ + internal_server_error("internal-server-error"), + + /** + * The JID or hostname provided in a 'from' address does not match an authorized + * JID or validated domain negotiated between servers via SASL or dialback, or + * between a client and a server via authentication and resource binding. + */ + invalid_from("invalid-from"), + + /** + * The stream ID or dialback ID is invalid or does not match an ID previously provided. + */ + invalid_id("invalid-id"), + + /** + * the streams namespace name is something other than "http://etherx.jabber.org/streams" + * or the dialback namespace name is something other than "jabber:server:dialback". + */ + invalid_namespace("invalid-namespace"), + + /** + * The entity has sent invalid XML over the stream to a server that performs validation. + */ + invalid_xml("invalid-xml"), + + /** + * The entity has attempted to send data before the stream has been authenticated, + * or otherwise is not authorized to perform an action related to stream + * negotiation; the receiving entity MUST NOT process the offending stanza before + * sending the stream error. + */ + not_authorized("not-authorized"), + + /** + * The entity has violated some local service policy; the server MAY choose to + * specify the policy in the element or an application-specific condition + * element. + */ + policy_violation("policy-violation"), + + /** + * The server is unable to properly connect to a remote entity that is required for + * authentication or authorization. + */ + remote_connection_failed("remote-connection-failed"), + + /** + * The server lacks the system resources necessary to service the stream. + */ + resource_constraint("resource-constraint"), + + /** + * The entity has attempted to send restricted XML features such as a comment, + * processing instruction, DTD, entity reference, or unescaped character. + */ + restricted_xml("restricted-xml"), + + /** + * The server will not provide service to the initiating entity but is redirecting + * traffic to another host; the server SHOULD specify the alternate hostname or IP + * address (which MUST be a valid domain identifier) as the XML character data of the + * <see-other-host/> element. + */ + see_other_host("see-other-host"), + + /** + * The server is being shut down and all active streams are being closed. + */ + system_shutdown("system-shutdown"), + + /** + * The error condition is not one of those defined by the other conditions in this + * list; this error condition SHOULD be used only in conjunction with an + * application-specific condition. + */ + undefined_condition("undefined-condition"), + + /** + * The initiating entity has encoded the stream in an encoding that is not + * supported by the server. + */ + unsupported_encoding("unsupported-encoding"), + + /** + * The initiating entity has sent a first-level child of the stream that is + * not supported by the server. + */ + unsupported_stanza_type("unsupported-stanza-type"), + + /** + * the value of the 'version' attribute provided by the initiating entity in the + * stream header specifies a version of XMPP that is not supported by the server; + * the server MAY specify the version(s) it supports in the <text/> element. + */ + unsupported_version("unsupported-version"), + + /** + * The initiating entity has sent XML that is not well-formed. + */ + xml_not_well_formed("xml-not-well-formed"); + + /** + * Converts a String value into its Condition representation. + * + * @param condition the String value. + * @return the condition corresponding to the String. + */ + public static Condition fromXMPP(String condition) { + if (condition == null) { + throw new NullPointerException(); + } + condition = condition.toLowerCase(); + if (bad_format.toXMPP().equals(condition)) { + return bad_format; + } + else if (bad_namespace_prefix.toXMPP().equals(condition)) { + return bad_namespace_prefix; + } + else if (conflict.toXMPP().equals(condition)) { + return conflict; + } + else if (connection_timeout.toXMPP().equals(condition)) { + return connection_timeout; + } + else if (host_gone.toXMPP().equals(condition)) { + return host_gone; + } + else if (host_unknown.toXMPP().equals(condition)) { + return host_unknown; + } + else if (improper_addressing.toXMPP().equals(condition)) { + return improper_addressing; + } + else if (internal_server_error.toXMPP().equals(condition)) { + return internal_server_error; + } + else if (invalid_from.toXMPP().equals(condition)) { + return invalid_from; + } + else if (invalid_id.toXMPP().equals(condition)) { + return invalid_id; + } + else if (invalid_namespace.toXMPP().equals(condition)) { + return invalid_namespace; + } + else if (invalid_xml.toXMPP().equals(condition)) { + return invalid_xml; + } + else if (not_authorized.toXMPP().equals(condition)) { + return not_authorized; + } + else if (policy_violation.toXMPP().equals(condition)) { + return policy_violation; + } + else if (remote_connection_failed.toXMPP().equals(condition)) { + return remote_connection_failed; + } + else if (resource_constraint.toXMPP().equals(condition)) { + return resource_constraint; + } + else if (restricted_xml.toXMPP().equals(condition)) { + return restricted_xml; + } + else if (see_other_host.toXMPP().equals(condition)) { + return see_other_host; + } + else if (system_shutdown.toXMPP().equals(condition)) { + return system_shutdown; + } + else if (undefined_condition.toXMPP().equals(condition)) { + return undefined_condition; + } + else if (unsupported_encoding.toXMPP().equals(condition)) { + return unsupported_encoding; + } + else if (unsupported_stanza_type.toXMPP().equals(condition)) { + return unsupported_stanza_type; + } + else if (unsupported_version.toXMPP().equals(condition)) { + return unsupported_version; + } + else if (xml_not_well_formed.toXMPP().equals(condition)) { + return xml_not_well_formed; + } + else { + throw new IllegalArgumentException("Condition invalid:" + condition); + } + } + + private String value; + + private Condition(String value) { + this.value = value; + } + + /** + * Returns the error code as a valid XMPP error code string. + * + * @return the XMPP error code value. + */ + public String toXMPP() { + return value; + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/StreamIDFactory.java b/src/java/org/jivesoftware/multiplexer/StreamIDFactory.java new file mode 100644 index 0000000..f466771 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/StreamIDFactory.java @@ -0,0 +1,37 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer; + +import java.util.Random; + +/** + * A basic stream ID factory that produces id's using java.util.Random + * and a simple hex representation of a random int prefixed by the connection + * manager name.

+ * + * Each connection manager has to provide unique stream IDs. + * + * @author Gaston Dombiak + */ +class StreamIDFactory { + /** + * The random number to use, someone with Java can predict stream IDs if they can guess the current seed * + */ + Random random = new Random(); + + String managerName = ConnectionManager.getInstance().getName(); + + public String createStreamID() { + return managerName + Integer.toHexString(random.nextInt()); + } + +} diff --git a/src/java/org/jivesoftware/multiplexer/net/BlockingAcceptingMode.java b/src/java/org/jivesoftware/multiplexer/net/BlockingAcceptingMode.java new file mode 100644 index 0000000..d9008b3 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/BlockingAcceptingMode.java @@ -0,0 +1,65 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.jivesoftware.multiplexer.ServerPort; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * Accepts new socket connections and uses a thread for each new connection. + * + * @author Gaston Dombiak + */ +class BlockingAcceptingMode extends SocketAcceptingMode { + + protected BlockingAcceptingMode(ServerPort serverPort, + InetAddress bindInterface) throws IOException { + super(serverPort); + serverSocket = new ServerSocket(serverPort.getPort(), -1, bindInterface); + } + + /** + * About as simple as it gets. The thread spins around an accept + * call getting sockets and creating new reading threads for each new connection. + */ + public void run() { + while (notTerminated) { + try { + Socket sock = serverSocket.accept(); + if (sock != null) { + Log.debug("Connect " + sock.toString()); + SocketReader reader = + SocketReaderFactory.createSocketReader(sock, false, serverPort, true); + Thread thread = new Thread(reader, reader.getName()); + thread.setDaemon(true); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + } + } + catch (IOException ie) { + if (notTerminated) { + Log.error(LocaleUtils.getLocalizedString("admin.error.accept"), + ie); + } + } + catch (Throwable e) { + Log.error(LocaleUtils.getLocalizedString("admin.error.accept"), e); + } + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/BlockingReadingMode.java b/src/java/org/jivesoftware/multiplexer/net/BlockingReadingMode.java new file mode 100644 index 0000000..3a8d2ed --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/BlockingReadingMode.java @@ -0,0 +1,296 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZInputStream; +import org.dom4j.Element; +import org.jivesoftware.multiplexer.Connection; +import org.jivesoftware.multiplexer.Session; +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.SocketException; +import java.nio.channels.AsynchronousCloseException; + +/** + * Process incoming packets using a blocking model. Once a session has been created + * an endless loop is used to process incoming packets. Packets are processed + * sequentially. + * + * @author Gaston Dombiak + */ +class BlockingReadingMode extends SocketReadingMode { + + private Status saslStatus = Status.waitingServer; + + public BlockingReadingMode(Socket socket, SocketReader socketReader) { + super(socket, socketReader); + } + + /** + * A dedicated thread loop for reading the stream and sending incoming + * packets to the appropriate router. + */ + public void run() { + try { + socketReader.reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(), + CHARSET)); + + // Read in the opening tag and prepare for packet stream + try { + socketReader.createSession(); + } + catch (IOException e) { + Log.debug("Error creating session", e); + throw e; + } + + // Read the packet stream until it ends + if (socketReader.session != null) { + readStream(); + } + + } + catch (EOFException eof) { + // Normal disconnect + } + catch (SocketException se) { + // The socket was closed. The server may close the connection for several + // reasons (e.g. user requested to remove his account). Do nothing here. + } + catch (AsynchronousCloseException ace) { + // The socket was closed. + } + catch (XmlPullParserException ie) { + // It is normal for clients to abruptly cut a connection + // rather than closing the stream document. Since this is + // normal behavior, we won't log it as an error. + // Log.error(LocaleUtils.getLocalizedString("admin.disconnect"),ie); + } + catch (Exception e) { + Log.warn(LocaleUtils.getLocalizedString("admin.error.stream") + ". Connection: " + + socketReader.connection, e); + } + finally { + if (socketReader.session != null) { + if (Log.isDebugEnabled()) { + Log.debug("Logging off " + socketReader.connection); + } + try { + socketReader.session.close(); + } + catch (Exception e) { + Log.warn(LocaleUtils.getLocalizedString("admin.error.connection") + + "\n" + socket.toString()); + } + } + else { + // Close and release the created connection + socketReader.connection.close(); + Log.error(LocaleUtils.getLocalizedString("admin.error.connection") + + "\n" + socket.toString()); + } + socketReader.shutdown(); + } + } + + /** + * Read the incoming stream until it ends. + */ + private void readStream() throws Exception { + socketReader.open = true; + while (socketReader.open) { + Element doc = socketReader.reader.parseDocument().getRootElement(); + if (doc == null) { + // Stop reading the stream since the client has sent an end of + // stream element and probably closed the connection. + return; + } + String tag = doc.getName(); + if ("starttls".equals(tag)) { + // Negotiate TLS + if (negotiateTLS()) { + tlsNegotiated(); + } + else { + socketReader.open = false; + } + } + else if ("auth".equals(tag)) { + // User is trying to authenticate using SASL + if (authenticateClient(doc)) { + // SASL authentication was successful so open a new stream and offer + // resource binding and session establishment (to client sessions only) + saslSuccessful(); + } + } + else if ("compress".equals(tag)) + { + // Client is trying to initiate compression + if (compressClient(doc)) { + // Compression was successful so open a new stream and offer + // resource binding and session establishment (to client sessions only) + compressionSuccessful(); + } + } + else { + socketReader.process(doc); + } + } + } + + protected void tlsNegotiated() throws XmlPullParserException, IOException { + XmlPullParser xpp = socketReader.reader.getXPPParser(); + // Reset the parser to use the new reader + xpp.setInput(new InputStreamReader( + socketReader.connection.getTLSStreamHandler().getInputStream(), CHARSET)); + // Skip new stream element + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + super.tlsNegotiated(); + } + + protected boolean authenticateClient(Element doc) throws Exception { + // Ensure that connection was secured if TLS was required + if (socketReader.connection.getTlsPolicy() == Connection.TLSPolicy.required && + !socketReader.connection.isSecure()) { + socketReader.closeNeverSecuredConnection(); + return false; + } + + boolean isComplete = false; + boolean success = false; + while (!isComplete && !socketReader.connection.isClosed()) { + // Forward stanza to the server + socketReader.process(doc); + // Wait 5 minutes to get a response from the server + synchronized (this) { + wait(5 * 60 * 1000); + } + // Raise an error if no response from the server was received + if (saslStatus == Status.waitingServer) { + throw new Exception("No answer was received from the server"); + } + + // If client was challenged then wait for client answer + if (saslStatus == Status.needResponse) { + doc = socketReader.reader.parseDocument().getRootElement(); + if (doc == null) { + // Nothing was read because the connection was closed or dropped + isComplete = true; + } + } + else { + success = socketReader.session.getStatus() == Session.STATUS_AUTHENTICATED; + isComplete = true; + } + } + return success; + } + + /** + * Notification message indicating that a client needs to response to a SASL + * challenge. + */ + void clientChallenged() { + // Set that client needs to send response + saslStatus = Status.needResponse; + synchronized (this) { + notify(); + } + } + + /** + * Notification message indicating that sasl authentication has finished. The + * success parameter indicates whether authentication was successful or not. + * + * @param success true when authentication was successful. + */ + void clientAuthenticated(boolean success) { + // Set result of authentication process + saslStatus = success ? Status.authenticated : Status.failed; + synchronized (this) { + notify(); + } + } + + protected void saslSuccessful() throws XmlPullParserException, IOException { + MXParser xpp = socketReader.reader.getXPPParser(); + // Reset the parser since a new stream header has been sent from the client + xpp.resetInput(); + + // Skip the opening stream sent by the client + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + super.saslSuccessful(); + } + + protected boolean compressClient(Element doc) throws XmlPullParserException, IOException { + boolean answer = super.compressClient(doc); + if (answer) { + XmlPullParser xpp = socketReader.reader.getXPPParser(); + // Reset the parser since a new stream header has been sent from the client + if (socketReader.connection.getTLSStreamHandler() == null) { + ZInputStream in = new ZInputStream(socket.getInputStream()); + in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + xpp.setInput(new InputStreamReader(in, CHARSET)); + } + else { + ZInputStream in = new ZInputStream( + socketReader.connection.getTLSStreamHandler().getInputStream()); + in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + xpp.setInput(new InputStreamReader(in, CHARSET)); + } + } + return answer; + } + + protected void compressionSuccessful() throws XmlPullParserException, IOException { + XmlPullParser xpp = socketReader.reader.getXPPParser(); + // Skip the opening stream sent by the client + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + super.compressionSuccessful(); + } + + public enum Status { + /** + * Server needs to process sasl stanza and send its answer to the client. + */ + waitingServer, + /** + * Entity needs to respond last challenge. Session is still negotiating + * SASL authentication. + */ + needResponse, + /** + * SASL negotiation has failed. The entity may retry a few times before the connection + * is closed. + */ + failed, + /** + * SASL negotiation has been successful. + */ + authenticated + } + +} diff --git a/src/java/org/jivesoftware/multiplexer/net/ClientSocketReader.java b/src/java/org/jivesoftware/multiplexer/net/ClientSocketReader.java new file mode 100644 index 0000000..7364a22 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/ClientSocketReader.java @@ -0,0 +1,68 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.multiplexer.ClientSession; +import org.jivesoftware.multiplexer.PacketRouter; +import org.jivesoftware.util.JiveGlobals; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.Socket; + +/** + * A SocketReader specialized for client connections. This reader will be used when the open + * stream contains a jabber:client namespace. Received packet will have their FROM attribute + * overriden to avoid spoofing.

+ * + * By default the hostname specified in the stream header sent by clients will not be validated. + * When validated the TO attribute of the stream header has to match the server name or a valid + * subdomain. If the value of the 'to' attribute is not valid then a host-unknown error + * will be returned. To enable the validation set the system property + * xmpp.client.validate.host to true.

+ * + * Stanzas that do not have a FROM attribute will be wrapped before forwarding them to the + * server. The wrapping element will include the stream ID that uniquely identifies the client + * in the server. The server will then be able to use the proper client session for processing + * the stanza. + * + * @author Gaston Dombiak + */ +public class ClientSocketReader extends SocketReader { + + public ClientSocketReader(PacketRouter router, String serverName, + Socket socket, SocketConnection connection, boolean useBlockingMode) { + super(router, serverName, socket, connection, useBlockingMode); + } + + boolean createSession(String namespace) throws XmlPullParserException, + IOException { + if ("jabber:client".equals(namespace)) { + // The connected client is a regular client so create a ClientSession + session = ClientSession.createSession(serverName, this, reader, connection); + return true; + } + return false; + } + + String getNamespace() { + return "jabber:client"; + } + + String getName() { + return "Client SR - " + hashCode(); + } + + boolean validateHost() { + return JiveGlobals.getBooleanProperty("xmpp.client.validate.host",false); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/DNSUtil.java b/src/java/org/jivesoftware/multiplexer/net/DNSUtil.java new file mode 100644 index 0000000..df2cde3 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/DNSUtil.java @@ -0,0 +1,124 @@ +/** + * $RCSfile: $ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.Log; + +import javax.naming.directory.Attributes; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.DirContext; +import java.util.Hashtable; + +/** + * Utilty class to perform DNS lookups for XMPP services. + * + * @author Matt Tucker + */ +public class DNSUtil { + + private static DirContext context; + + static { + try { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + context = new InitialDirContext(env); + } + catch (Exception e) { + Log.error(e); + } + } + + /** + * Returns the host name and port that the specified XMPP server can be + * reached at for server-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-server._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the specified default port.

+ * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @param defaultPort default port to return if the DNS look up fails. + * @return a HostAddress, which encompasses the hostname and port that the XMPP + * server can be reached at for the specified domain. + */ + public static HostAddress resolveXMPPServerDomain(String domain, int defaultPort) { + if (context == null) { + return new HostAddress(domain, defaultPort); + } + String host = domain; + int port = defaultPort; + try { + Attributes dnsLookup = context.getAttributes("_xmpp-server._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e) { + // Attempt lookup with older "jabber" name. + try { + Attributes dnsLookup = context.getAttributes("_jabber._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e2) { } + } + // Host entries in DNS should end with a ".". + if (host.endsWith(".")) { + host = host.substring(0, host.length()-1); + } + return new HostAddress(host, port); + } + + /** + * Encapsulates a hostname and port. + */ + public static class HostAddress { + + private String host; + private int port; + + private HostAddress(String host, int port) { + this.host = host; + this.port = port; + } + + /** + * Returns the hostname. + * + * @return the hostname. + */ + public String getHost() { + return host; + } + + /** + * Returns the port. + * + * @return the port. + */ + public int getPort() { + return port; + } + + public String toString() { + return host + ":" + port; + } + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/net/MXParser.java b/src/java/org/jivesoftware/multiplexer/net/MXParser.java new file mode 100644 index 0000000..17dbed2 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/MXParser.java @@ -0,0 +1,356 @@ +/** + * $RCSfile: $ + * $Revision: 3135 $ + * $Date: 2005-12-01 02:03:04 -0300 (Thu, 01 Dec 2005) $ + * + * Copyright (C) 2005 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +import java.io.IOException; +import java.io.Reader; + +/** + * MXParser that returns an IGNORABLE_WHITESPACE event when a whitespace character or a + * line feed is received. This parser is useful when not validating documents.

+ * + * This class was copied from Wildfire. + * + * @author Gaston Dombiak + */ +public class MXParser extends org.xmlpull.mxp1.MXParser { + + /** + * Last time a heartbeat was received. Hearbeats are represented as whitespaces + * or \n characters received when an XmlPullParser.END_TAG was parsed. Note that we + * can falsely detect heartbeats when parsing XHTML content but that is fine. + */ + private long lastHeartbeat = 0; + + protected int nextImpl() + throws XmlPullParserException, IOException + { + text = null; + pcEnd = pcStart = 0; + usePC = false; + bufStart = posEnd; + if(pastEndTag) { + pastEndTag = false; + --depth; + namespaceEnd = elNamespaceCount[ depth ]; // less namespaces available + } + if(emptyElementTag) { + emptyElementTag = false; + pastEndTag = true; + return eventType = END_TAG; + } + + // [1] document ::= prolog element Misc* + if(depth > 0) { + + if(seenStartTag) { + seenStartTag = false; + return eventType = parseStartTag(); + } + if(seenEndTag) { + seenEndTag = false; + return eventType = parseEndTag(); + } + + // ASSUMPTION: we are _on_ first character of content or markup!!!! + // [43] content ::= CharData? ((element | Reference | CDSect | PI | Comment) CharData?)* + char ch; + if(seenMarkup) { // we have read ahead ... + seenMarkup = false; + ch = '<'; + } else if(seenAmpersand) { + seenAmpersand = false; + ch = '&'; + } else { + ch = more(); + } + posStart = pos - 1; // VERY IMPORTANT: this is correct start of event!!! + + // when true there is some potential event TEXT to return - keep gathering + boolean hadCharData = false; + + // when true TEXT data is not continous (like ) and requires PC merging + boolean needsMerging = false; + + MAIN_LOOP: + while(true) { + // work on MARKUP + if(ch == '<') { + if(hadCharData) { + //posEnd = pos - 1; + if(tokenize) { + seenMarkup = true; + return eventType = TEXT; + } + } + ch = more(); + if(ch == '/') { + if(!tokenize && hadCharData) { + seenEndTag = true; + //posEnd = pos - 2; + return eventType = TEXT; + } + return eventType = parseEndTag(); + } else if(ch == '!') { + ch = more(); + if(ch == '-') { + // note: if(tokenize == false) posStart/End is NOT changed!!!! + parseComment(); + if(tokenize) return eventType = COMMENT; + if( !usePC && hadCharData ) { + needsMerging = true; + } else { + posStart = pos; //completely ignore comment + } + } else if(ch == '[') { + //posEnd = pos - 3; + // must remeber previous posStart/End as it merges with content of CDATA + //int oldStart = posStart + bufAbsoluteStart; + //int oldEnd = posEnd + bufAbsoluteStart; + parseCDSect(hadCharData); + if(tokenize) return eventType = CDSECT; + final int cdStart = posStart; + final int cdEnd = posEnd; + final int cdLen = cdEnd - cdStart; + + + if(cdLen > 0) { // was there anything inside CDATA section? + hadCharData = true; + if(!usePC) { + needsMerging = true; + } + } + + // posStart = oldStart; + // posEnd = oldEnd; + // if(cdLen > 0) { // was there anything inside CDATA section? + // if(hadCharData) { + // // do merging if there was anything in CDSect!!!! + // // if(!usePC) { + // // // posEnd is correct already!!! + // // if(posEnd > posStart) { + // // joinPC(); + // // } else { + // // usePC = true; + // // pcStart = pcEnd = 0; + // // } + // // } + // // if(pcEnd + cdLen >= pc.length) ensurePC(pcEnd + cdLen); + // // // copy [cdStart..cdEnd) into PC + // // System.arraycopy(buf, cdStart, pc, pcEnd, cdLen); + // // pcEnd += cdLen; + // if(!usePC) { + // needsMerging = true; + // posStart = cdStart; + // posEnd = cdEnd; + // } + // } else { + // if(!usePC) { + // needsMerging = true; + // posStart = cdStart; + // posEnd = cdEnd; + // hadCharData = true; + // } + // } + // //hadCharData = true; + // } else { + // if( !usePC && hadCharData ) { + // needsMerging = true; + // } + // } + } else { + throw new XmlPullParserException( + "unexpected character in markup "+printable(ch), this, null); + } + } else if(ch == '?') { + parsePI(); + if(tokenize) return eventType = PROCESSING_INSTRUCTION; + if( !usePC && hadCharData ) { + needsMerging = true; + } else { + posStart = pos; //completely ignore PI + } + + } else if( isNameStartChar(ch) ) { + if(!tokenize && hadCharData) { + seenStartTag = true; + //posEnd = pos - 2; + return eventType = TEXT; + } + return eventType = parseStartTag(); + } else { + throw new XmlPullParserException( + "unexpected character in markup "+printable(ch), this, null); + } + // do content comapctation if it makes sense!!!! + + } else if(ch == '&') { + // work on ENTITTY + //posEnd = pos - 1; + if(tokenize && hadCharData) { + seenAmpersand = true; + return eventType = TEXT; + } + final int oldStart = posStart + bufAbsoluteStart; + final int oldEnd = posEnd + bufAbsoluteStart; + final char[] resolvedEntity = parseEntityRef(); + if(tokenize) return eventType = ENTITY_REF; + // check if replacement text can be resolved !!! + if(resolvedEntity == null) { + if(entityRefName == null) { + entityRefName = newString(buf, posStart, posEnd - posStart); + } + throw new XmlPullParserException( + "could not resolve entity named '"+printable(entityRefName)+"'", + this, null); + } + //int entStart = posStart; + //int entEnd = posEnd; + posStart = oldStart - bufAbsoluteStart; + posEnd = oldEnd - bufAbsoluteStart; + if(!usePC) { + if(hadCharData) { + joinPC(); // posEnd is already set correctly!!! + needsMerging = false; + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + // write into PC replacement text - do merge for replacement text!!!! + for (int i = 0; i < resolvedEntity.length; i++) + { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = resolvedEntity[ i ]; + + } + hadCharData = true; + //assert needsMerging == false; + } else { + + if(needsMerging) { + //assert usePC == false; + joinPC(); // posEnd is already set correctly!!! + //posStart = pos - 1; + needsMerging = false; + } + + + //no MARKUP not ENTITIES so work on character data ... + + + + // [14] CharData ::= [^<&]* - ([^<&]* ']]>' [^<&]*) + + + hadCharData = true; + + boolean normalizedCR = false; + final boolean normalizeInput = tokenize == false || roundtripSupported == false; + // use loop locality here!!!! + boolean seenBracket = false; + boolean seenBracketBracket = false; + do { + + // check that ]]> does not show in + if (eventType == XmlPullParser.END_TAG && + (ch == ' ' || ch == '\n' || ch == '\t')) { + // ** ADDED CODE (INCLUDING IF STATEMENT) + lastHeartbeat = System.currentTimeMillis();; + } + if(ch == ']') { + if(seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else if(seenBracketBracket && ch == '>') { + throw new XmlPullParserException( + "characters ]]> are not allowed in content", this, null); + } else { + if(seenBracket) { + seenBracketBracket = seenBracket = false; + } + // assert seenTwoBrackets == seenBracket == false; + } + if(normalizeInput) { + // deal with normalization issues ... + if(ch == '\r') { + normalizedCR = true; + posEnd = pos -1; + // posEnd is alreadys set + if(!usePC) { + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + // if(!usePC) { joinPC(); } else { if(pcEnd >= pc.length) ensurePC(); } + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + + ch = more(); + } while(ch != '<' && ch != '&'); + posEnd = pos - 1; + continue MAIN_LOOP; // skip ch = more() from below - we are alreayd ahead ... + } + ch = more(); + } // endless while(true) + } else { + if(seenRoot) { + return parseEpilog(); + } else { + return parseProlog(); + } + } + } + + /** + * Returns the last time a heartbeat was received. Hearbeats are represented as whitespaces + * or \n characters received when an XmlPullParser.END_TAG was parsed. Note that we + * can falsely detect heartbeats when parsing XHTML content but that is fine. + * + * @return the time in milliseconds when a heartbeat was received. + */ + public long getLastHeartbeat() { + return lastHeartbeat; + } + + public void resetInput() { + Reader oldReader = reader; + String oldEncoding = inputEncoding; + reset(); + reader = oldReader; + inputEncoding = oldEncoding; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/NonBlockingAcceptingMode.java b/src/java/org/jivesoftware/multiplexer/net/NonBlockingAcceptingMode.java new file mode 100644 index 0000000..866078e --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/NonBlockingAcceptingMode.java @@ -0,0 +1,160 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.jivesoftware.multiplexer.ServerPort; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +/** + * Accepts new socket connections using a non-blocking model. A single selector is + * used for all connected clients and also for accepting new connections. + * + * @author Daniele Piras + */ +class NonBlockingAcceptingMode extends SocketAcceptingMode { + + // Time (in ms) to sleep from a reading-cycle to another + private static final long CYCLE_TIME = 10; + + // Selector to collect messages from client connections. + private Selector selector; + + protected NonBlockingAcceptingMode(ServerPort serverPort, + InetAddress bindInterface) throws IOException { + super(serverPort); + + // Chaning server to use NIO + // Open selector... + selector = Selector.open(); + // Create a new ServerSocketChannel + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // Retrieve socket and bind socket with specified address + this.serverSocket = serverSocketChannel.socket(); + this.serverSocket.bind(new InetSocketAddress(bindInterface, serverPort.getPort())); + // Configure Blocking to unblocking + serverSocketChannel.configureBlocking(false); + // Registering connection with selector. + SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + AcceptConnection acceptConnection = new AcceptConnection(); + sk.attach(acceptConnection); + } + + /** + * DANIELE: + * This thread use the selector NIO features to retrieve client connections + * and messages. + */ + public void run() { + while (notTerminated && !Thread.interrupted()) { + try { + selector.select(); + Set selected = selector.selectedKeys(); + Iterator it = selected.iterator(); + while (it.hasNext()) { + SelectionKey key = (SelectionKey) it.next(); + it.remove(); + SelectorAction action = (SelectorAction) key.attachment(); + if (action == null) { + continue; + } + if (key.isAcceptable()) { + action.connect(key); + } + else if (key.isReadable()) { + action.read(key); + } + } + Thread.sleep(CYCLE_TIME); + } + catch (IOException ie) { + if (notTerminated) { + Log.error(LocaleUtils.getLocalizedString("admin.error.accept"), + ie); + } + } + catch (Exception e) { + Log.error(LocaleUtils.getLocalizedString("admin.error.accept"), e); + } + } + } + + /* + * InnerClass that is use when a new client arrive. + * It's use the reactor pattern to register an abstract action + * to the selector. + */ + class AcceptConnection implements SelectorAction { + + + public void read(SelectionKey key) throws IOException { + } + + /* + * A client arrive... + */ + public void connect(SelectionKey key) throws IOException { + // Retrieve the server socket channel... + ServerSocketChannel sChannel = (ServerSocketChannel) key.channel(); + // Accept the connection + SocketChannel socketChannel = sChannel.accept(); + // Retrieve socket for incoming connection + Socket sock = socketChannel.socket(); + socketChannel.configureBlocking(false); + // Registering READING operation into the selector + SelectionKey sockKey = socketChannel.register(selector, SelectionKey.OP_READ); + if (sock != null) { + System.out.println("Connect " + sock.toString()); + Log.debug("Connect " + sock.toString()); + try { + SocketReader reader = + SocketReaderFactory.createSocketReader(sock, false, serverPort, false); + SelectorAction action = new ReadAction(reader); + sockKey.attach(action); + } + catch (Exception e) { + // There is an exception... + Log.error(LocaleUtils.getLocalizedString("admin.error.accept"), e); + } + } + } + } + + class ReadAction implements SelectorAction { + + SocketReader reader; + + public ReadAction(SocketReader reader) { + this.reader = reader; + } + + public void read(SelectionKey key) throws IOException { + // Socket reader (using non-blocking mode) will read the stream and process, in + // another thread, any number of stanzas found in the stream. + reader.run(); + } + + public void connect(SelectionKey key) throws IOException { + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java b/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java new file mode 100644 index 0000000..732975c --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java @@ -0,0 +1,156 @@ +/** + * $RCSfile$ + * $Revision: 1217 $ + * $Date: 2005-04-11 18:11:06 -0300 (Mon, 11 Apr 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.security.KeyStore; + +/** + * Configuration of Wildfire's SSL settings.

+ * + * This class was copied from Wildfire. Properties are now stored in XML. + * + * @author Gaston Dombiak + */ +public class SSLConfig { + + private static SSLJiveServerSocketFactory sslFactory; + private static KeyStore keyStore; + private static String keypass; + private static KeyStore trustStore; + private static String trustpass; + private static String keyStoreLocation; + private static String trustStoreLocation; + + private SSLConfig() { + } + + static { + String algorithm = JiveGlobals.getXMLProperty("xmpp.socket.ssl.algorithm", "TLS"); + String storeType = JiveGlobals.getXMLProperty("xmpp.socket.ssl.storeType", "jks"); + + // Get the keystore location. The default location is security/keystore + keyStoreLocation = JiveGlobals.getXMLProperty("xmpp.socket.ssl.keystore", + "resources" + File.separator + "security" + File.separator + "keystore"); + keyStoreLocation = JiveGlobals.getHomeDirectory() + File.separator + keyStoreLocation; + + // Get the keystore password. The default password is "changeit". + keypass = JiveGlobals.getXMLProperty("xmpp.socket.ssl.keypass", "changeit"); + keypass = keypass.trim(); + + // Get the truststore location; default at security/truststore + trustStoreLocation = JiveGlobals.getXMLProperty("xmpp.socket.ssl.truststore", + "resources" + File.separator + "security" + File.separator + "truststore"); + trustStoreLocation = JiveGlobals.getHomeDirectory() + File.separator + trustStoreLocation; + + // Get the truststore passwprd; default is "changeit". + trustpass = JiveGlobals.getXMLProperty("xmpp.socket.ssl.trustpass", "changeit"); + trustpass = trustpass.trim(); + + try { + keyStore = KeyStore.getInstance(storeType); + keyStore.load(new FileInputStream(keyStoreLocation), keypass.toCharArray()); + + trustStore = KeyStore.getInstance(storeType); + trustStore.load(new FileInputStream(trustStoreLocation), trustpass.toCharArray()); + + sslFactory = (SSLJiveServerSocketFactory)SSLJiveServerSocketFactory.getInstance( + algorithm, keyStore, trustStore); + } + catch (Exception e) { + Log.error("SSLConfig startup problem.\n" + + " storeType: [" + storeType + "]\n" + + " keyStoreLocation: [" + keyStoreLocation + "]\n" + + " keypass: [" + keypass + "]\n" + + " trustStoreLocation: [" + trustStoreLocation+ "]\n" + + " trustpass: [" + trustpass + "]", e); + keyStore = null; + trustStore = null; + sslFactory = null; + } + } + + public static String getKeyPassword() { + return keypass; + } + + public static String getTrustPassword() { + return trustpass; + } + + public static String[] getDefaultCipherSuites() { + String[] suites; + if (sslFactory == null) { + suites = new String[]{}; + } + else { + suites = sslFactory.getDefaultCipherSuites(); + } + return suites; + } + + public static String[] getSpportedCipherSuites() { + String[] suites; + if (sslFactory == null) { + suites = new String[]{}; + } + else { + suites = sslFactory.getSupportedCipherSuites(); + } + return suites; + } + + public static KeyStore getKeyStore() throws IOException { + if (keyStore == null) { + throw new IOException(); + } + return keyStore; + } + + public static KeyStore getTrustStore() throws IOException { + if (trustStore == null) { + throw new IOException(); + } + return trustStore; + } + + public static void saveStores() throws IOException { + try { + keyStore.store(new FileOutputStream(keyStoreLocation), keypass.toCharArray()); + trustStore.store(new FileOutputStream(trustStoreLocation), trustpass.toCharArray()); + } + catch (IOException e) { + throw e; + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + public static ServerSocket createServerSocket(int port, InetAddress ifAddress) throws + IOException { + if (sslFactory == null) { + throw new IOException(); + } + else { + return sslFactory.createServerSocket(port, -1, ifAddress); + } + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLJiveKeyManagerFactory.java b/src/java/org/jivesoftware/multiplexer/net/SSLJiveKeyManagerFactory.java new file mode 100644 index 0000000..5a284ee --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SSLJiveKeyManagerFactory.java @@ -0,0 +1,110 @@ +/** + * $RCSfile$ + * $Revision: 2774 $ + * $Date: 2005-09-05 01:53:16 -0300 (Mon, 05 Sep 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; + +import org.jivesoftware.util.Log; + +/** + * A custom KeyManagerFactory that creates a key manager list using the + * default key manager or a standard keystore as specified in manager.xml. + * The default keystore provided with the Jive distribution uses the Sun Java + * Keystore (JKS) and that takes a single password which must apply to both the + * keystore and the key itself. Users may specify another keystore type and keystore + * location. Alternatively, don't set a keystore type to use the JVM defaults and + * configure your JVMs security files (see your JVM documentation) to plug in + * any KeyManagerFactory provider. + * + * @author Iain Shigeoka + */ +public class SSLJiveKeyManagerFactory { + + /** + * Creates a KeyManager list which is null if the storeType is null, or + * is a standard KeyManager that uses a KeyStore of type storeType, + * located at 'keystore' location under home, and uses 'keypass' as + * the password for the keystore password and key password. The default + * Jive keystore contains a self-signed X509 certificate pair under the + * alias '127.0.0.1' in a Java KeyStore (JKS) with initial password 'changeit'. + * This is sufficient for local host testing but should be using standard + * key management tools for any significant testing or deployment. See + * the Jive XMPP server security documentation for more information. + * + * @param storeType The type of keystore (e.g. "JKS") to use or null to indicate no keystore should be used + * @param keystore The relative location of the keystore under home + * @param keypass The password for the keystore and key + * @return An array of relevant KeyManagers (may be null indicating a default KeyManager should be created) + * @throws NoSuchAlgorithmException If the keystore type doesn't exist (not provided or configured with your JVM) + * @throws KeyStoreException If the keystore is corrupt + * @throws IOException If the keystore could not be located or loaded + * @throws CertificateException If there were no certificates to be loaded or they are invalid + * @throws UnrecoverableKeyException If they keystore coud not be opened (typically the password is bad) + */ + public static KeyManager[] getKeyManagers(String storeType, String keystore, String keypass) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException { + KeyManager[] keyManagers; + if (keystore == null) { + keyManagers = null; + } + else { + if (keypass == null) { + keypass = ""; + } + KeyStore keyStore = KeyStore.getInstance(storeType); + keyStore.load(new FileInputStream(keystore), keypass.toCharArray()); + + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyFactory.init(keyStore, keypass.toCharArray()); + keyManagers = keyFactory.getKeyManagers(); + } + return keyManagers; + } + public static KeyManager[] getKeyManagers(KeyStore keystore, String keypass) { + KeyManager[] keyManagers; + try { + if (keystore == null) { + keyManagers = null; + } else { + KeyManagerFactory keyFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (keypass == null) { + keypass = SSLConfig.getKeyPassword(); + } + + keyFactory.init(keystore, keypass.toCharArray()); + keyManagers = keyFactory.getKeyManagers(); + } + } catch (KeyStoreException e) { + keyManagers = null; + Log.error("SSLJiveKeyManagerFactory startup problem.\n" + + " the keystore is corrupt", e); + } catch (NoSuchAlgorithmException e) { + keyManagers = null; + Log.error("SSLJiveKeyManagerFactory startup problem.\n" + + " the keystore type doesn't exist (not provided or configured with your JVM)", e); + } catch (UnrecoverableKeyException e) { + keyManagers = null; + Log.error("SSLJiveKeyManagerFactory startup problem.\n" + + " the keystore could not be opened (typically the password is bad)", e); + } + return keyManagers; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLJiveServerSocketFactory.java b/src/java/org/jivesoftware/multiplexer/net/SSLJiveServerSocketFactory.java new file mode 100644 index 0000000..c6065a1 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SSLJiveServerSocketFactory.java @@ -0,0 +1,85 @@ +/** + * $RCSfile$ + * $Revision: 1217 $ + * $Date: 2005-04-11 18:11:06 -0300 (Mon, 11 Apr 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.Log; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.security.KeyStore; + +/** + * Securue socket factory wrapper allowing simple setup of all security + * SSL related parameters.

+ * + * This class was copied from Wildfire. + * + * @author Gaston Dombiak + */ +public class SSLJiveServerSocketFactory extends SSLServerSocketFactory { + + public static SSLServerSocketFactory getInstance(String algorithm, + KeyStore keystore, + KeyStore truststore) throws + IOException { + + try { + SSLContext sslcontext = SSLContext.getInstance(algorithm); + SSLServerSocketFactory factory; + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyFactory.init(keystore, SSLConfig.getKeyPassword().toCharArray()); + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init(truststore); + + sslcontext.init(keyFactory.getKeyManagers(), + trustFactory.getTrustManagers(), + new java.security.SecureRandom()); + factory = sslcontext.getServerSocketFactory(); + return new SSLJiveServerSocketFactory(factory); + } + catch (Exception e) { + Log.error(e); + throw new IOException(e.getMessage()); + } + } + + private SSLServerSocketFactory factory; + + private SSLJiveServerSocketFactory(SSLServerSocketFactory factory) { + this.factory = factory; + } + + public ServerSocket createServerSocket(int i) throws IOException { + return factory.createServerSocket(i); + } + + public ServerSocket createServerSocket(int i, int i1) throws IOException { + return factory.createServerSocket(i, i1); + } + + public ServerSocket createServerSocket(int i, int i1, InetAddress inetAddress) throws IOException { + return factory.createServerSocket(i, i1, inetAddress); + } + + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLJiveTrustManagerFactory.java b/src/java/org/jivesoftware/multiplexer/net/SSLJiveTrustManagerFactory.java new file mode 100644 index 0000000..2ba712a --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SSLJiveTrustManagerFactory.java @@ -0,0 +1,107 @@ +/** + * $RCSfile$ + * $Revision: 2774 $ + * $Date: 2005-09-05 01:53:16 -0300 (Mon, 05 Sep 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.jivesoftware.util.Log; + +/** + * A custom TrustManagerFactory that creates a trust manager list using the + * default trust manager or a standard keystore as specified in manager.xml. + * There is no default trust keystore provided with the Jive distribution as most + * clients will not need to be authenticated with the server. + *

+ * The Java Keystore (JKS) takes a single password which must apply to both the + * keystore and the key itself. Users may specify another keystore type and keystore + * location. Alternatively, don't set a keystore type to use the JVM defaults and + * configure your JVMs security files (see your JVM documentation) to plug in + * any TrustManagerFactory provider. + * + * @author Iain Shigeoka + */ +public class SSLJiveTrustManagerFactory { + + /** + * Creates a TrustManager list which is null if the storeType is null, or + * is a standard TrustManager that uses a KeyStore of type storeType, + * located at 'keystore' location under home, and uses 'keypass' as + * the password for the keystore password and key password (note that + * trust managers typically don't need a key password as public keys + * are stored in the clear and can be obtained without a key password). + * The default Jive distribution doesn't ship with a trust keystore + * as it is not needed (the server does not require client authentication). + * + * @param storeType The type of keystore (e.g. "JKS") to use or null to indicate no keystore should be used + * @param truststore The relative location of the keystore under home + * @param trustpass The password for the keystore and key + * @return An array of relevant KeyManagers (may be null indicating a default KeyManager should be created) + * @throws NoSuchAlgorithmException If the keystore type doesn't exist (not provided or configured with your JVM) + * @throws KeyStoreException If the keystore is corrupt + * @throws IOException If the keystore could not be located or loaded + * @throws CertificateException If there were no certificates to be loaded or they are invalid + */ + public static TrustManager[] getTrustManagers(String storeType, String truststore, String trustpass) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException { + TrustManager[] trustManagers; + if (truststore == null) { + trustManagers = null; + } + else { + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (trustpass == null) { + trustpass = ""; + } + KeyStore keyStore = KeyStore.getInstance(storeType); + keyStore.load(new FileInputStream(truststore), trustpass.toCharArray()); + trustFactory.init(keyStore); + trustManagers = trustFactory.getTrustManagers(); + } + return trustManagers; + } + + public static TrustManager[] getTrustManagers(KeyStore truststore, + String trustpass) { + TrustManager[] trustManagers; + try { + if (truststore == null) { + trustManagers = null; + } else { + TrustManagerFactory trustFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (trustpass == null) { + trustpass = SSLConfig.getTrustPassword(); + } + + trustFactory.init(truststore); + + trustManagers = trustFactory.getTrustManagers(); + } + } catch (KeyStoreException e) { + trustManagers = null; + Log.error("SSLJiveTrustManagerFactory startup problem.\n" + + " the keystore is corrupt", e); + } catch (NoSuchAlgorithmException e) { + trustManagers = null; + Log.error("SSLJiveTrustManagerFactory startup problem.\n" + + " the keystore type doesn't exist (not provided or configured with your JVM)", e); + } + return trustManagers; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLSocketAcceptThread.java b/src/java/org/jivesoftware/multiplexer/net/SSLSocketAcceptThread.java new file mode 100644 index 0000000..d87e78c --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SSLSocketAcceptThread.java @@ -0,0 +1,185 @@ +/** + * $RCSfile$ + * $Revision: 1583 $ + * $Date: 2005-07-03 17:55:39 -0300 (Sun, 03 Jul 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; +import org.jivesoftware.multiplexer.ServerPort; + +import javax.net.ssl.SSLException; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * Implements a network front end with a dedicated thread reading + * each incoming socket. The old SSL method always uses a blocking model. + * + * @author Gaston Dombiak + */ +public class SSLSocketAcceptThread extends Thread { + + /** + * The default Jabber socket + */ + public static final int DEFAULT_PORT = 5223; + + /** + * Holds information about the port on which the server will listen for connections. + */ + private ServerPort serverPort; + + /** + * True while this thread should continue running. + */ + private boolean notTerminated = true; + + /** + * The accept socket we're running + */ + private ServerSocket serverSocket; + + /** + * The number of SSL related exceptions occuring rapidly that should signal a need + * to shutdown the SSL port. + */ + private static final int MAX_SSL_EXCEPTIONS = 10; + + /** + * Creates an instance using the default port, TLS transport security, and + * JVM defaults for all security settings. + * + * @throws IOException if there was trouble initializing the SSL configuration. + */ + public SSLSocketAcceptThread(ServerPort serverPort) + throws IOException { + super("Secure Socket Listener"); + this.serverPort = serverPort; + int port = serverPort.getPort(); + // Listen on a specific network interface if it has been set. + String interfaceName = JiveGlobals.getXMLProperty("network.interface"); + InetAddress bindInterface = null; + if (interfaceName != null) { + try { + if (interfaceName.trim().length() > 0) { + bindInterface = InetAddress.getByName(interfaceName); + } + } + catch (UnknownHostException e) { + Log.error(LocaleUtils.getLocalizedString("admin.error"), e); + } + } + serverSocket = SSLConfig.createServerSocket(port, bindInterface); + } + + /** + * Retrieve the port this server socket is bound to. + * + * @return the port the socket is bound to. + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + + /** + * Returns information about the port on which the server is listening for connections. + * + * @return information about the port on which the server is listening for connections. + */ + public ServerPort getServerPort() { + return serverPort; + } + + /** + * Unblock the thread and force it to terminate. + */ + public void shutdown() { + notTerminated = false; + try { + ServerSocket sSock = serverSocket; + serverSocket = null; + if (sSock != null) { + sSock.close(); + } + } + catch (IOException e) { + // we don't care, no matter what, the socket should be dead + } + } + + /** + * About as simple as it gets. The thread spins around an accept + * call getting sockets and handing them to the SocketManager. + * We need to detect run away failures since an SSL configuration + * problem can cause the loop to spin, constantly rethrowing SSLExceptions + * (e.g. if a certificate is in the keystore that can't be verified). + */ + public void run() { + long lastExceptionTime = 0; + int exceptionCounter = 0; + while (notTerminated) { + try { + Socket sock = serverSocket.accept(); + Log.debug("SSL Connect " + sock.toString()); + SocketReader reader = + SocketReaderFactory.createSocketReader(sock, true, serverPort, true); + // Create a new reading thread for each new connected client + Thread thread = new Thread(reader, reader.getName()); + thread.setDaemon(true); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + } + catch (SSLException se) { + long exceptionTime = System.currentTimeMillis(); + if (exceptionTime - lastExceptionTime > 1000) { + // if the time between SSL exceptions is too long + // reset the counter + exceptionCounter = 1; + } + else { + // If this exception occured within a second of the last one + // we need to count it + exceptionCounter++; + } + lastExceptionTime = exceptionTime; + Log.error(LocaleUtils.getLocalizedString("admin.error.ssl"), se); + // and if the number of consecutive exceptions exceeds the limit + // we should assume there's an SSL problem or DOS attack and shutdown + if (exceptionCounter > MAX_SSL_EXCEPTIONS) { + String msg = "Shutting down SSL port - " + + "suspected configuration problem"; + Log.error(msg); + Log.info(msg); + shutdown(); + } + } + catch (Throwable e) { + if (notTerminated) { + Log.error(LocaleUtils.getLocalizedString("admin.error.ssl"), e); + } + } + } + try { + ServerSocket sSock = serverSocket; + serverSocket = null; + if (sSock != null) { + sSock.close(); + } + } + catch (IOException e) { + // we don't care, no matter what, the socket should be dead + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SelectorAction.java b/src/java/org/jivesoftware/multiplexer/net/SelectorAction.java new file mode 100644 index 0000000..7c827e2 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SelectorAction.java @@ -0,0 +1,24 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import java.io.IOException; +import java.nio.channels.SelectionKey; + +/** + * @author Daniele Piras + */ +interface SelectorAction +{ + public abstract void read( SelectionKey key ) throws IOException; + public abstract void connect( SelectionKey key ) throws IOException; +} diff --git a/src/java/org/jivesoftware/multiplexer/net/ServerTrustManager.java b/src/java/org/jivesoftware/multiplexer/net/ServerTrustManager.java new file mode 100644 index 0000000..fa757ec --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/ServerTrustManager.java @@ -0,0 +1,224 @@ +/** + * Copyright (C) 2004 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.Log; + +import javax.net.ssl.X509TrustManager; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; + +/** + * ServerTrustManager is a Trust Manager that is only used for s2s connections. This TrustManager + * is used both when the server connects to another server as well as when receiving a connection + * from another server. In both cases, it is possible to indicate if self-signed certificates + * are going to be accepted. In case of accepting a self-signed certificate a warning is logged. + * Future version of the server might include a small workflow so admins can review self-signed + * certificates or certificates of unknown issuers and manually accept them. + * + * @author Gaston Dombiak + */ +class ServerTrustManager implements X509TrustManager { + + /** + * KeyStore that holds the trusted CA + */ + private KeyStore trustStore; + /** + * Holds the domain of the remote server we are trying to connect + */ + private String server; + + public ServerTrustManager(String server, KeyStore trustTrust) { + super(); + this.server = server; + this.trustStore = trustTrust; + } + + public void checkClientTrusted(X509Certificate[] x509Certificates, + String string) { + // Do not validate the certificate at this point. The certificate is going to be used + // when the remote server requests to do EXTERNAL SASL + } + + /** + * Given the partial or complete certificate chain provided by the peer, build a certificate + * path to a trusted root and return if it can be validated and is trusted for server SSL + * authentication based on the authentication type. The authentication type is the key + * exchange algorithm portion of the cipher suites represented as a String, such as "RSA", + * "DHE_DSS". Note: for some exportable cipher suites, the key exchange algorithm is + * determined at run time during the handshake. For instance, for + * TLS_RSA_EXPORT_WITH_RC4_40_MD5, the authType should be RSA_EXPORT when an ephemeral + * RSA key is used for the key exchange, and RSA when the key from the server certificate + * is used. Checking is case-sensitive.

+ * + * By default certificates are going to be verified. This includes verifying the certificate + * chain, the root certificate and the certificates validity. However, it is possible to + * disable certificates validation as a whole or each specific validation. + * + * @param x509Certificates an ordered array of peer X.509 certificates with the peer's own + * certificate listed first and followed by any certificate authorities. + * @param string the key exchange algorithm used. + * @throws CertificateException if the certificate chain is not trusted by this TrustManager. + */ + public void checkServerTrusted(X509Certificate[] x509Certificates, String string) + throws CertificateException { + + // Flag that indicates if certificates of the remote server should be validated. Disabling + // certificate validation is not recommended for production environments. + boolean verify = JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify", true); + if (verify) { + int nSize = x509Certificates.length; + + List peerIdentities = TLSStreamHandler.getPeerIdentities(x509Certificates[0]); + + if (JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify.chain", true)) { + // Working down the chain, for every certificate in the chain, + // verify that the subject of the certificate is the issuer of the + // next certificate in the chain. + Principal principalLast = null; + for (int i = nSize -1; i >= 0 ; i--) { + X509Certificate x509certificate = x509Certificates[i]; + Principal principalIssuer = x509certificate.getIssuerDN(); + Principal principalSubject = x509certificate.getSubjectDN(); + if (principalLast != null) { + if (principalIssuer.equals(principalLast)) { + try { + PublicKey publickey = + x509Certificates[i + 1].getPublicKey(); + x509Certificates[i].verify(publickey); + } + catch (GeneralSecurityException generalsecurityexception) { + throw new CertificateException( + "signature verification failed of " + peerIdentities); + } + } + else { + throw new CertificateException( + "subject/issuer verification failed of " + peerIdentities); + } + } + principalLast = principalSubject; + } + } + + if (JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify.root", true)) { + // Verify that the the last certificate in the chain was issued + // by a third-party that the client trusts. + boolean trusted = false; + try { + trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null; + if (!trusted && nSize == 1 && JiveGlobals + .getBooleanProperty("xmpp.server.certificate.accept-selfsigned", false)) + { + Log.warn("Accepting self-signed certificate of remote server: " + + peerIdentities); + trusted = true; + } + } + catch (KeyStoreException e) { + Log.error(e); + } + if (!trusted) { + throw new CertificateException("root certificate not trusted of " + peerIdentities); + } + } + + // Verify that the first certificate in the chain corresponds to + // the server we desire to authenticate. + // Check if the certificate uses a wildcard indicating that subdomains are valid + if (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) { + // Remove the wildcard + String peerIdentity = peerIdentities.get(0).replace("*.", ""); + // Check if the requested subdomain matches the certified domain + if (!server.endsWith(peerIdentity)) { + throw new CertificateException("target verification failed of " + peerIdentities); + } + } + else if (!peerIdentities.contains(server)) { + throw new CertificateException("target verification failed of " + peerIdentities); + } + + if (JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify.validity", true)) { + // For every certificate in the chain, verify that the certificate + // is valid at the current time. + Date date = new Date(); + for (int i = 0; i < nSize; i++) { + try { + x509Certificates[i].checkValidity(date); + } + catch (GeneralSecurityException generalsecurityexception) { + throw new CertificateException("invalid date of " + peerIdentities); + } + } + } + } + } + + private boolean isChainTrusted(X509Certificate[] chain) { + boolean trusted = false; + try { + // Start with the root and see if it is in the Keystore. + // The root is at the end of the chain. + for (int i = chain.length - 1; i >= 0; i--) { + if (trustStore.getCertificateAlias(chain[i]) != null) { + trusted = true; + break; + } + } + } + catch (Exception e) { + Log.error(e); + trusted = false; + } + return trusted; + } + + public X509Certificate[] getAcceptedIssuers() { + if (JiveGlobals.getBooleanProperty("xmpp.server.certificate.accept-selfsigned", false)) { + // Answer an empty list since we accept any issuer + return new X509Certificate[0]; + } + else { + X509Certificate[] X509Certs = null; + try { + // See how many certificates are in the keystore. + int numberOfEntry = trustStore.size(); + // If there are any certificates in the keystore. + if (numberOfEntry > 0) { + // Create an array of X509Certificates + X509Certs = new X509Certificate[numberOfEntry]; + + // Get all of the certificate alias out of the keystore. + Enumeration aliases = trustStore.aliases(); + + // Retrieve all of the certificates out of the keystore + // via the alias name. + int i = 0; + while (aliases.hasMoreElements()) { + X509Certs[i] = + (X509Certificate) trustStore. + getCertificate((String) aliases.nextElement()); + i++; + } + + } + } + catch (Exception e) { + Log.error(e); + X509Certs = null; + } + return X509Certs; + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketAcceptThread.java b/src/java/org/jivesoftware/multiplexer/net/SocketAcceptThread.java new file mode 100644 index 0000000..61106e1 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketAcceptThread.java @@ -0,0 +1,109 @@ +/** + * $RCSfile$ + * $Revision: 1583 $ + * $Date: 2005-07-03 17:55:39 -0300 (Sun, 03 Jul 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.multiplexer.ServerPort; + +import java.io.IOException; +import java.net.InetAddress; + +/** + * Implements a network front end with a dedicated thread reading + * each incoming socket. Blocking and non-blocking modes are supported. + * By default blocking mode is used. Use the xmpp.socket.blocking + * system property to change the blocking mode. Restart the server after making + * changes to the system property. + * + * @author Gaston Dombiak + */ +public class SocketAcceptThread extends Thread { + + /** + * The default XMPP port for clients. + */ + public static final int DEFAULT_PORT = 5222; + + /** + * The default XMPP port for server2server communication. + */ + public static final int DEFAULT_SERVER_PORT = 5269; + + /** + * The default XMPP port for connection multiplex. + */ + public static final int DEFAULT_MULTIPLEX_PORT = 5262; + + /** + * Holds information about the port on which the server will listen for connections. + */ + private ServerPort serverPort; + + private SocketAcceptingMode acceptingMode; + + public SocketAcceptThread(ServerPort serverPort) + throws IOException { + super("Socket Listener at port " + serverPort.getPort()); + this.serverPort = serverPort; + // Listen on a specific network interface if it has been set. + String interfaceName = JiveGlobals.getXMLProperty("network.interface"); + InetAddress bindInterface = null; + if (interfaceName != null) { + if (interfaceName.trim().length() > 0) { + bindInterface = InetAddress.getByName(interfaceName); + } + } + // Set the blocking reading mode to use + boolean useBlockingMode = JiveGlobals.getBooleanProperty("xmpp.socket.blocking", true); + if (useBlockingMode) { + acceptingMode = new BlockingAcceptingMode(serverPort, bindInterface); + } + else { + acceptingMode = new NonBlockingAcceptingMode(serverPort, bindInterface); + } + } + + /** + * Retrieve the port this server socket is bound to. + * + * @return the port the socket is bound to. + */ + public int getPort() { + return serverPort.getPort(); + } + + /** + * Returns information about the port on which the server is listening for connections. + * + * @return information about the port on which the server is listening for connections. + */ + public ServerPort getServerPort() { + return serverPort; + } + + /** + * Unblock the thread and force it to terminate. + */ + public void shutdown() { + acceptingMode.shutdown(); + } + + /** + * About as simple as it gets. The thread spins around an accept + * call getting sockets and handing them to the SocketManager. + */ + public void run() { + acceptingMode.run(); + // We stopped accepting new connections so close the listener + shutdown(); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketAcceptingMode.java b/src/java/org/jivesoftware/multiplexer/net/SocketAcceptingMode.java new file mode 100644 index 0000000..8c0dbaf --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketAcceptingMode.java @@ -0,0 +1,60 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.multiplexer.ServerPort; + +import java.io.IOException; +import java.net.ServerSocket; + +/** + * Abstract class for {@link BlockingAcceptingMode} and {@link NonBlockingAcceptingMode}. + * + * @author Gaston Dombiak + */ +abstract class SocketAcceptingMode { + + /** + * True while this thread should continue running. + */ + protected boolean notTerminated = true; + + /** + * Holds information about the port on which the server will listen for connections. + */ + protected ServerPort serverPort; + + /** + * socket that listens for connections. + */ + protected ServerSocket serverSocket; + + protected SocketAcceptingMode(ServerPort serverPort) { + this.serverPort = serverPort; + } + + public abstract void run(); + + public void shutdown() { + notTerminated = false; + try { + ServerSocket sSock = serverSocket; + serverSocket = null; + if (sSock != null) { + sSock.close(); + } + } + catch (IOException e) { + // we don't care, no matter what, the socket should be dead + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketConnection.java b/src/java/org/jivesoftware/multiplexer/net/SocketConnection.java new file mode 100644 index 0000000..189dafb --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketConnection.java @@ -0,0 +1,631 @@ +/** + * $RCSfile$ + * $Revision: 3187 $ + * $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZOutputStream; +import org.dom4j.Element; +import org.jivesoftware.multiplexer.*; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.LocaleUtils; +import org.jivesoftware.util.Log; + +import javax.net.ssl.SSLSession; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.channels.Channels; +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An object to track the state of a XMPP client-server session. Currently this class + * contains the socket channel connecting the client and server.

+ * + * This class was copied from Wildfire. PacketInterceptors were removed. Session concept was + * removed. ConnectionCloseListeners were removed. + * + * @author Gaston Dombiak + */ +public class SocketConnection implements Connection { + + /** + * The utf-8 charset for decoding and encoding XMPP packet streams. + */ + public static final String CHARSET = "UTF-8"; + + private static Map instances = + new ConcurrentHashMap(); + + /** + * Milliseconds a connection has to be idle to be closed. Timeout is disabled by default. It's + * up to the connection's owner to configure the timeout value. Sending stanzas to the client + * is not considered as activity. We are only considering the connection active when the + * client sends some data or hearbeats (i.e. whitespaces) to the server. + * The reason for this is that sending data will fail if the connection is closed. And if + * the thread is blocked while sending data (because the socket is closed) then the clean up + * thread will close the socket anyway. + */ + private long idleTimeout = -1; + + final private Map listeners = + new HashMap(); + + private Socket socket; + private SocketStatistic socketStatistic; + + private Writer writer; + private AtomicBoolean writing = new AtomicBoolean(false); + + /** + * Deliverer to use when the connection is closed or was closed when delivering + * a packet. + */ + private PacketDeliverer backupDeliverer; + + private Session session; + private boolean secure; + private boolean compressed; + private org.jivesoftware.util.XMLWriter xmlSerializer; + private boolean flashClient = false; + private int majorVersion = 1; + private int minorVersion = 0; + private String language = null; + private TLSStreamHandler tlsStreamHandler; + + private long writeStarted = -1; + + /** + * TLS policy currently in use for this connection. + */ + private TLSPolicy tlsPolicy = TLSPolicy.optional; + + /** + * Compression policy currently in use for this connection. + */ + private CompressionPolicy compressionPolicy = CompressionPolicy.disabled; + + public static Collection getInstances() { + return instances.keySet(); + } + + /** + * Create a new session using the supplied socket. + * + * @param backupDeliverer the packet deliverer this connection will use when socket is closed. + * @param socket the socket to represent. + * @param isSecure true if this is a secure connection. + * @throws NullPointerException if the socket is null. + */ + public SocketConnection(PacketDeliverer backupDeliverer, Socket socket, boolean isSecure) + throws IOException { + if (socket == null) { + throw new NullPointerException("Socket channel must be non-null"); + } + + this.secure = isSecure; + this.socket = socket; + // DANIELE: Modify socket to use channel + if (socket.getChannel() != null) { + writer = Channels.newWriter(socket.getChannel(), CHARSET); + } + else { + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET)); + } + this.backupDeliverer = backupDeliverer; + xmlSerializer = new XMLSocketWriter(writer, this); + + instances.put(this, ""); + } + + /** + * Returns the stream handler responsible for securing the plain connection and providing + * the corresponding input and output streams. + * + * @return the stream handler responsible for securing the plain connection and providing + * the corresponding input and output streams. + */ + public TLSStreamHandler getTLSStreamHandler() { + return tlsStreamHandler; + } + + /** + * Secures the plain connection by negotiating TLS with the client. When connecting + * to a remote server then clientMode will be true and + * remoteServer is the server name of the remote server. Otherwise clientMode + * will be false and remoteServer null. + * + * @param clientMode boolean indicating if this entity is a client or a server. + * @param remoteServer server name of the remote server we are connecting to or null + * when not in client mode. + * @throws IOException if an error occured while securing the connection. + */ + public void startTLS(boolean clientMode, String remoteServer) throws IOException { + if (!secure) { + secure = true; + // Prepare for TLS + tlsStreamHandler = new TLSStreamHandler(socket, clientMode, remoteServer, false); + if (!clientMode) { + // Indicate the client that the server is ready to negotiate TLS + deliverRawText(""); + } + // Start handshake + tlsStreamHandler.start(); + // Use new wrapped writers + writer = new BufferedWriter(new OutputStreamWriter(tlsStreamHandler.getOutputStream(), CHARSET)); + xmlSerializer = new XMLSocketWriter(writer, this); + } + } + + /** + * Start using compression for this connection. Compression will only be available after TLS + * has been negotiated. This means that a connection can never be using compression before + * TLS. However, it is possible to use compression without TLS. + * + * @throws IOException if an error occured while starting compression. + */ + public void startCompression() throws IOException { + compressed = true; + + if (tlsStreamHandler == null) { + ZOutputStream out = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_COMPRESSION); + out.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + writer = new BufferedWriter(new OutputStreamWriter(out, CHARSET)); + xmlSerializer = new XMLSocketWriter(writer, this); + } + else { + ZOutputStream out = new ZOutputStream(tlsStreamHandler.getOutputStream(), JZlib.Z_BEST_COMPRESSION); + out.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + writer = new BufferedWriter(new OutputStreamWriter(out, CHARSET)); + xmlSerializer = new XMLSocketWriter(writer, this); + } + } + + public boolean validate() { + if (isClosed()) { + return false; + } + boolean allowedToWrite = false; + try { + requestWriting(); + allowedToWrite = true; + // Register that we started sending data on the connection + writeStarted(); + writer.write(" "); + writer.flush(); + } + catch (Exception e) { + Log.warn("Closing no longer valid connection" + "\n" + this.toString(), e); + close(); + } + finally { + // Register that we finished sending data on the connection + writeFinished(); + if (allowedToWrite) { + releaseWriting(); + } + } + return !isClosed(); + } + + public void init(Session owner) { + session = owner; + } + + public Object registerCloseListener(ConnectionCloseListener listener, Object handbackMessage) { + Object status = null; + if (isClosed()) { + listener.onConnectionClose(handbackMessage); + } + else { + status = listeners.put(listener, handbackMessage); + } + return status; + } + + public Object removeCloseListener(ConnectionCloseListener listener) { + return listeners.remove(listener); + } + + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + /** + * Returns the port that the connection uses. + * + * @return the port that the connection uses. + */ + public int getPort() { + return socket.getPort(); + } + + public boolean isClosed() { + if (session == null) { + return socket.isClosed(); + } + return session.getStatus() == Session.STATUS_CLOSED; + } + + public boolean isSecure() { + return secure; + } + + public boolean isCompressed() { + return compressed; + } + + public TLSPolicy getTlsPolicy() { + return tlsPolicy; + } + + /** + * Sets whether TLS is mandatory, optional or is disabled. When TLS is mandatory clients + * are required to secure their connections or otherwise their connections will be closed. + * On the other hand, when TLS is disabled clients are not allowed to secure their connections + * using TLS. Their connections will be closed if they try to secure the connection. in this + * last case. + * + * @param tlsPolicy whether TLS is mandatory, optional or is disabled. + */ + public void setTlsPolicy(TLSPolicy tlsPolicy) { + this.tlsPolicy = tlsPolicy; + } + + public CompressionPolicy getCompressionPolicy() { + return compressionPolicy; + } + + /** + * Sets whether compression is enabled or is disabled. + * + * @param compressionPolicy whether Compression is enabled or is disabled. + */ + public void setCompressionPolicy(CompressionPolicy compressionPolicy) { + this.compressionPolicy = compressionPolicy; + } + + public long getIdleTimeout() { + return idleTimeout; + } + + /** + * Sets the number of milliseconds a connection has to be idle to be closed. Sending + * stanzas to the client is not considered as activity. We are only considering the + * connection active when the client sends some data or hearbeats (i.e. whitespaces) + * to the server. + * + * @param timeout the number of milliseconds a connection has to be idle to be closed. + */ + public void setIdleTimeout(long timeout) { + this.idleTimeout = timeout; + } + + public int getMajorXMPPVersion() { + return majorVersion; + } + + public int getMinorXMPPVersion() { + return minorVersion; + } + + /** + * Sets the XMPP version information. In most cases, the version should be "1.0". + * However, older clients using the "Jabber" protocol do not set a version. In that + * case, the version is "0.0". + * + * @param majorVersion the major version. + * @param minorVersion the minor version. + */ + public void setXMPPVersion(int majorVersion, int minorVersion) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + } + + public String getLanguage() { + return language; + } + + /** + * Sets the language code that should be used for this connection (e.g. "en"). + * + * @param language the language code. + */ + public void setLanaguage(String language) { + this.language = language; + } + + public boolean isFlashClient() { + return flashClient; + } + + /** + * Sets whether the connected client is a flash client. Flash clients need to + * receive a special character (i.e. \0) at the end of each xml packet. Flash + * clients may send the character \0 in incoming packets and may start a + * connection using another openning tag such as: "flash:client". + * + * @param flashClient true if the if the connection is a flash client. + */ + public void setFlashClient(boolean flashClient) { + this.flashClient = flashClient; + } + + public SSLSession getSSLSession() { + if (tlsStreamHandler != null) { + return tlsStreamHandler.getSSLSession(); + } + return null; + } + + /** + * Returns the packet deliverer to use when delivering a packet over the socket fails. The + * packet deliverer will retry to send the packet using some other connection, will store + * the packet offline for later retrieval or will just drop it. + * + * @return the packet deliverer to use when delivering a packet over the socket fails. + */ + public PacketDeliverer getPacketDeliverer() { + return backupDeliverer; + } + + public void close() { + boolean wasClosed = false; + synchronized (this) { + if (!isClosed()) { + try { + boolean allowedToWrite = false; + try { + requestWriting(); + allowedToWrite = true; + // Register that we started sending data on the connection + writeStarted(); + writer.write(""); + if (flashClient) { + writer.write('\0'); + } + writer.flush(); + } + catch (IOException e) { + // Do nothing + } + finally { + // Register that we finished sending data on the connection + writeFinished(); + if (allowedToWrite) { + releaseWriting(); + } + } + } + catch (Exception e) { + Log.error(LocaleUtils.getLocalizedString("admin.error.close") + + "\n" + this.toString(), e); + } + closeConnection(); + wasClosed = true; + } + } + if (wasClosed) { + notifyCloseListeners(); + } + } + + public void systemShutdown() { + deliverRawText(""); + close(); + } + + void writeStarted() { + writeStarted = System.currentTimeMillis(); + } + + void writeFinished() { + writeStarted = -1; + } + + /** + * Returns true if the socket was closed due to a bad health. The socket is considered to + * be in a bad state if a thread has been writing for a while and the write operation has + * not finished in a long time or when the client has not sent a heartbeat for a long time. + * In any of both cases the socket will be closed. + * + * @return true if the socket was closed due to a bad health.s + */ + boolean checkHealth() { + // Check that the sending operation is still active + long writeTimestamp = writeStarted; + if (writeTimestamp > -1 && System.currentTimeMillis() - writeTimestamp > + JiveGlobals.getIntProperty("xmpp.session.sending-limit", 60000)) { + // Close the socket + if (Log.isDebugEnabled()) { + Log.debug("Closing connection: " + this + " that started sending data at: " + + new Date(writeTimestamp)); + } + forceClose(); + return true; + } + else { + // Check if the connection has been idle. A connection is considered idle if the client + // has not been receiving data for a period. Sending data to the client is not + // considered as activity. + if (idleTimeout > -1 && socketStatistic != null && + System.currentTimeMillis() - socketStatistic.getLastActive() > idleTimeout) { + // Close the socket + if (Log.isDebugEnabled()) { + Log.debug("Closing connection that has been idle: " + this); + } + forceClose(); + return true; + } + } + return false; + } + + private void release() { + writeStarted = -1; + instances.remove(this); + } + + /** + * Forces the connection to be closed immediately no matter if closing the socket takes + * a long time. This method should only be called from {@link SocketSendingTracker} when + * sending data over the socket has taken a long time and we need to close the socket, discard + * the connection and its session. + */ + private void forceClose() { + closeConnection(); + // Notify the close listeners so that the SessionManager can send unavailable + // presences if required. + notifyCloseListeners(); + } + + private void closeConnection() { + release(); + try { + if (tlsStreamHandler == null) { + socket.close(); + } + else { + // Close the channels since we are using TLS (i.e. NIO). If the channels implement + // the InterruptibleChannel interface then any other thread that was blocked in + // an I/O operation will be interrupted and an exception thrown + tlsStreamHandler.close(); + } + } + catch (Exception e) { + Log.error(LocaleUtils.getLocalizedString("admin.error.close") + + "\n" + this.toString(), e); + } + } + + public void deliver(Element doc) { + if (isClosed()) { + backupDeliverer.deliver(doc); + } + else { + boolean errorDelivering = false; + boolean allowedToWrite = false; + try { + requestWriting(); + allowedToWrite = true; + xmlSerializer.write(doc); + if (flashClient) { + writer.write('\0'); + } + xmlSerializer.flush(); + } + catch (Exception e) { + Log.debug("Error delivering packet" + "\n" + this.toString(), e); + errorDelivering = true; + } + finally { + if (allowedToWrite) { + releaseWriting(); + } + } + if (errorDelivering) { + close(); + // Retry sending the packet again. Most probably if the packet is a + // Message it will be stored offline + backupDeliverer.deliver(doc); + } + } + } + + public void deliverRawText(String text) { + if (!isClosed()) { + boolean errorDelivering = false; + boolean allowedToWrite = false; + try { + requestWriting(); + allowedToWrite = true; + // Register that we started sending data on the connection + writeStarted(); + writer.write(text); + if (flashClient) { + writer.write('\0'); + } + writer.flush(); + } + catch (Exception e) { + Log.debug("Error delivering raw text" + "\n" + this.toString(), e); + errorDelivering = true; + } + finally { + // Register that we finished sending data on the connection + writeFinished(); + if (allowedToWrite) { + releaseWriting(); + } + } + if (errorDelivering) { + close(); + } + } + } + + /** + * Notifies all close listeners that the connection has been closed. + * Used by subclasses to properly finish closing the connection. + */ + private void notifyCloseListeners() { + synchronized (listeners) { + for (ConnectionCloseListener listener : listeners.keySet()) { + try { + listener.onConnectionClose(listeners.get(listener)); + } + catch (Exception e) { + Log.error("Error notifying listener: " + listener, e); + } + } + } + } + + private void requestWriting() throws Exception { + for (;;) { + if (writing.compareAndSet(false, true)) { + // We are now in writing mode and only we can write to the socket + return; + } + else { + // Check health of the socket + if (checkHealth()) { + // Connection was closed then stop + throw new Exception("Probable dead connection was closed"); + } + else { + Thread.sleep(1); + } + } + } + } + + private void releaseWriting() { + writing.compareAndSet(true, false); + } + + public String toString() { + return super.toString() + " socket: " + socket; + } + + public void setSocketStatistic(SocketStatistic socketStatistic) { + this.socketStatistic = socketStatistic; + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketReader.java b/src/java/org/jivesoftware/multiplexer/net/SocketReader.java new file mode 100644 index 0000000..5c5bae6 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketReader.java @@ -0,0 +1,295 @@ +/** + * $RCSfile$ + * $Revision: 3187 $ + * $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.dom4j.Element; +import org.dom4j.io.XMPPPacketReader; +import org.jivesoftware.multiplexer.*; +import org.jivesoftware.util.Log; +import org.jivesoftware.util.StringUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.net.Socket; + +/** + * A SocketReader creates the appropriate {@link Session} based on the defined namespace in the + * stream element and will then keep reading and routing the received packets.

+ * + * This class was copied from Wildfire. PacketInterceptors were removed. Session concept was + * removed. + * + * @author Gaston Dombiak + */ +public abstract class SocketReader implements Runnable, SocketStatistic { + + /** + * The utf-8 charset for decoding and encoding Jabber packet streams. + */ + private static String CHARSET = "UTF-8"; + /** + * Reuse the same factory for all the connections. + */ + private static XmlPullParserFactory factory = null; + + /** + * Session associated with the socket reader. + */ + protected Session session; + /** + * Reference to the physical connection. + */ + protected SocketConnection connection; + /** + * Server name for which we are attending clients. + */ + protected String serverName; + + /** + * Router used to route incoming packets to the correct channels. + */ + private PacketRouter router; + /** + * Specifies whether the socket is using blocking or non-blocking connections. + */ + private SocketReadingMode readingMode; + XMPPPacketReader reader = null; + protected boolean open; + + static { + try { + factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); + } + catch (XmlPullParserException e) { + Log.error("Error creating a parser factory", e); + } + } + + /** + * Creates a dedicated reader for a socket. + * + * @param router the router for sending packets that were read. + * @param serverName the name of the server this socket is working for. + * @param socket the socket to read from. + * @param connection the connection being read. + * @param useBlockingMode true means that the server will use a thread per connection. + */ + public SocketReader(PacketRouter router, String serverName, + Socket socket, SocketConnection connection, boolean useBlockingMode) { + this.serverName = serverName; + this.router = router; + this.connection = connection; + + connection.setSocketStatistic(this); + + // Reader is associated with a new XMPPPacketReader + reader = new XMPPPacketReader(); + reader.setXPPFactory(factory); + + // Set the blocking reading mode to use + if (useBlockingMode) { + readingMode = new BlockingReadingMode(socket, this); + } + else { + //TODO readingMode = new NonBlockingReadingMode(socket, this); + } + } + + /** + * A dedicated thread loop for reading the stream and sending incoming + * packets to the appropriate router. + */ + public void run() { + readingMode.run(); + } + + /** + * Notification message indicating that a client needs to response to a SASL + * challenge. + */ + public void clientChallenged() { + readingMode.clientChallenged(); + } + + /** + * Notification message indicating that sasl authentication has finished. The + * success parameter indicates whether authentication was successful or not. + * + * @param success true when authentication was successful. + */ + public void clientAuthenticated(boolean success) { + readingMode.clientAuthenticated(success); + } + + protected void process(Element doc) throws Exception { + if (doc == null) { + return; + } + + // Ensure that connection was secured if TLS was required + if (connection.getTlsPolicy() == Connection.TLSPolicy.required && + !connection.isSecure()) { + closeNeverSecuredConnection(); + return; + } + router.route(doc, session.getStreamID()); + } + + public long getLastActive() { + return reader.getLastActive(); + } + + /** + * Returns a name that identifies the type of reader and the unique instance. + * + * @return a name that identifies the type of reader and the unique instance. + */ + abstract String getName(); + + /** + * Close the connection since TLS was mandatory and the entity never negotiated TLS. Before + * closing the connection a stream error will be sent to the entity. + */ + void closeNeverSecuredConnection() { + // Set the not_authorized error + StreamError error = new StreamError(StreamError.Condition.not_authorized); + // Deliver stanza + connection.deliverRawText(error.toXML()); + // Close the underlying connection + connection.close(); + // Log a warning so that admins can track this case from the server side + Log.warn("TLS was required by the server and connection was never secured. " + + "Closing connection : " + connection); + } + + /** + * Uses the XPP to grab the opening stream tag and create an active session + * object. The session to create will depend on the sent namespace. In all + * cases, the method obtains the opening stream tag, checks for errors, and + * either creates a session or returns an error and kills the connection. + * If the connection remains open, the XPP will be set to be ready for the + * first packet. A call to next() should result in an START_TAG state with + * the first packet in the stream. + */ + protected void createSession() throws XmlPullParserException, IOException { + XmlPullParser xpp = reader.getXPPParser(); + for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { + eventType = xpp.next(); + } + + // Check that the TO attribute of the stream header matches the server name or a valid + // subdomain. If the value of the 'to' attribute is not valid then return a host-unknown + // error and close the underlying connection. + String host = reader.getXPPParser().getAttributeValue("", "to"); + if (validateHost() && isHostUnknown(host)) { + StringBuilder sb = new StringBuilder(250); + sb.append(""); + // Append stream header + sb.append(""); + // Set the host_unknown error + StreamError error = new StreamError(StreamError.Condition.host_unknown); + sb.append(error.toXML()); + // Deliver stanza + connection.deliverRawText(sb.toString()); + // Close the underlying connection + connection.close(); + // Log a warning so that admins can track this cases from the server side + Log.warn("Closing session due to incorrect hostname in stream header. Host: " + host + + ". Connection: " + connection); + } + + // Create the correct session based on the sent namespace. At this point the server + // may offer the client to secure the connection. If the client decides to secure + // the connection then a stanza should be received + else if (!createSession(xpp.getNamespace(null))) { + // No session was created because of an invalid namespace prefix so answer a stream + // error and close the underlying connection + StringBuilder sb = new StringBuilder(250); + sb.append(""); + // Append stream header + sb.append(""); + // Include the bad-namespace-prefix in the response + StreamError error = new StreamError(StreamError.Condition.bad_namespace_prefix); + sb.append(error.toXML()); + connection.deliverRawText(sb.toString()); + // Close the underlying connection + connection.close(); + // Log a warning so that admins can track this cases from the server side + Log.warn("Closing session due to bad_namespace_prefix in stream header. Prefix: " + + xpp.getNamespace(null) + ". Connection: " + connection); + } + } + + private boolean isHostUnknown(String host) { + if (host == null) { + // Answer false since when using server dialback the stream header will not + // have a TO attribute + return false; + } + if (serverName.equals(host)) { + // requested host matched the server name + return false; + } + return true; + } + + /** + * Returns the stream namespace. (E.g. jabber:client, jabber:server, etc.). + * + * @return the stream namespace. + */ + abstract String getNamespace(); + + /** + * Returns true if the value of the 'to' attribute in the stream header should be + * validated. If the value of the 'to' attribute is not valid then a host-unknown error + * will be returned and the underlying connection will be closed. + * + * @return true if the value of the 'to' attribute in the initial stream header should be + * validated. + */ + abstract boolean validateHost(); + + /** + * Notification message indicating that the SocketReader is shutting down. The thread will + * stop reading and processing new requests. Subclasses may want to redefine this message + * for releasing any resource they might need. + */ + protected void shutdown() { + } + + /** + * Creates the appropriate {@link Session} subclass based on the specified namespace. + * + * @param namespace the namespace sent in the stream element. eg. jabber:client. + * @return the created session or null. + * @throws XmlPullParserException + * @throws IOException + */ + abstract boolean createSession(String namespace) throws XmlPullParserException, IOException; +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketReaderFactory.java b/src/java/org/jivesoftware/multiplexer/net/SocketReaderFactory.java new file mode 100644 index 0000000..d836506 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketReaderFactory.java @@ -0,0 +1,48 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.multiplexer.PacketRouter; +import org.jivesoftware.multiplexer.ServerPort; +import org.jivesoftware.multiplexer.spi.ClientFailoverDeliverer; +import org.jivesoftware.multiplexer.spi.ServerRouter; +import org.jivesoftware.util.Log; + +import java.io.IOException; +import java.net.Socket; + +/** + * Factory of {@link SocketReader}. Currently only socket readers for clients are + * supported. + * + * @author Gaston Dombiak + */ +class SocketReaderFactory { + + private static PacketRouter router = new ServerRouter(); + private static String serverName = ConnectionManager.getInstance().getServerName(); + + static SocketReader createSocketReader(Socket sock, boolean isSecure, ServerPort serverPort, + boolean useBlockingMode) throws IOException { + if (serverPort.isClientPort()) { + SocketConnection conn = + new SocketConnection(new ClientFailoverDeliverer(), sock, isSecure); + return new ClientSocketReader(router, serverName, sock, conn, useBlockingMode); + } else { + Log.warn("Invalid socket reader was requested. Only clients are allowed to connect,"); + return null; + } + } + + +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketReadingMode.java b/src/java/org/jivesoftware/multiplexer/net/SocketReadingMode.java new file mode 100644 index 0000000..f8eac6c --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketReadingMode.java @@ -0,0 +1,243 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.dom4j.Element; +import org.jivesoftware.multiplexer.*; +import org.jivesoftware.util.Log; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.Socket; + +/** + * Abstract class for {@link BlockingReadingMode} and {@link NonBlockingReadingMode}. + * + * @author Gaston Dombiak + */ +abstract class SocketReadingMode { + + /** + * The utf-8 charset for decoding and encoding Jabber packet streams. + */ + protected static String CHARSET = "UTF-8"; + + protected SocketReader socketReader; + protected Socket socket; + + protected SocketReadingMode(Socket socket, SocketReader socketReader) { + this.socket = socket; + this.socketReader = socketReader; + } + + /* + * This method is invoked when client send data to the channel. + */ + abstract void run(); + + /** + * Tries to secure the connection using TLS. If the connection is secured then reset + * the parser to use the new secured reader. But if the connection failed to be secured + * then send a stanza and close the connection. + * + * @return true if the connection was secured. + */ + protected boolean negotiateTLS() { + if (socketReader.connection.getTlsPolicy() == Connection.TLSPolicy.disabled) { + // Set the not_authorized error + StreamError error = new StreamError(StreamError.Condition.not_authorized); + // Deliver stanza + socketReader.connection.deliverRawText(error.toXML()); + // Close the underlying connection + socketReader.connection.close(); + // Log a warning so that admins can track this case from the server side + Log.warn("TLS requested by initiator when TLS was never offered by server. " + + "Closing connection : " + socketReader.connection); + return false; + } + // Client requested to secure the connection using TLS. Negotiate TLS. + try { + socketReader.connection.startTLS(false, null); + } + catch (IOException e) { + Log.error("Error while negotiating TLS", e); + socketReader.connection.deliverRawText(""); + socketReader.connection.close(); + return false; + } + return true; + } + + /** + * TLS negotiation was successful so open a new stream and offer the new stream features. + * The new stream features will include available SASL mechanisms and specific features + * depending on the session type such as auth for Non-SASL authentication and register + * for in-band registration. + */ + protected void tlsNegotiated() throws XmlPullParserException, IOException { + // Offer stream features including SASL Mechanisms + StringBuilder sb = new StringBuilder(620); + sb.append(geStreamHeader()); + sb.append(""); + // Include available SASL Mechanisms + sb.append(ConnectionManager.getInstance().getServerSurrogate().getSASLMechanisms( + socketReader.session)); + // Include specific features such as auth and register for client sessions + String specificFeatures = socketReader.session.getAvailableStreamFeatures(); + if (specificFeatures != null) { + sb.append(specificFeatures); + } + sb.append(""); + socketReader.connection.deliverRawText(sb.toString()); + } + + /** + * Notification message indicating that a client needs to response to a SASL + * challenge. + */ + abstract void clientChallenged(); + + /** + * Notification message indicating that sasl authentication has finished. The + * success parameter indicates whether authentication was successful or not. + * + * @param success true when authentication was successful. + */ + abstract void clientAuthenticated(boolean success); + + /** + * After SASL authentication was successful we should open a new stream and offer + * new stream features such as resource binding and session establishment. Notice that + * resource binding and session establishment should only be offered to clients (i.e. not + * to servers or external components) + */ + protected void saslSuccessful() throws XmlPullParserException, IOException { + StringBuilder sb = new StringBuilder(420); + sb.append(geStreamHeader()); + sb.append(""); + + // Include specific features such as resource binding and session establishment + // for client sessions + String specificFeatures = socketReader.session.getAvailableStreamFeatures(); + if (specificFeatures != null) { + sb.append(specificFeatures); + } + sb.append(""); + socketReader.connection.deliverRawText(sb.toString()); + } + + /** + * Start using compression but first check if the connection can and should use compression. + * The connection will be closed if the requested method is not supported, if the connection + * is already using compression or if client requested to use compression but this feature + * is disabled. + * + * @param doc the element sent by the client requesting compression. Compression method is + * included. + * @return true if it was possible to use compression. + * @throws IOException if an error occurs while starting using compression. + */ + protected boolean compressClient(Element doc) throws IOException, XmlPullParserException { + String error = null; + if (socketReader.connection.getCompressionPolicy() == Connection.CompressionPolicy.disabled) { + // Client requested compression but this feature is disabled + error = ""; + // Log a warning so that admins can track this case from the server side + Log.warn("Client requested compression while compression is disabled. Closing " + + "connection : " + socketReader.connection); + } + else if (socketReader.connection.isCompressed()) { + // Client requested compression but connection is already compressed + error = ""; + // Log a warning so that admins can track this case from the server side + Log.warn("Client requested compression and connection is already compressed. Closing " + + "connection : " + socketReader.connection); + } + else { + // Check that the requested method is supported + String method = doc.elementText("method"); + if (!"zlib".equals(method)) { + error = ""; + // Log a warning so that admins can track this case from the server side + Log.warn("Requested compression method is not supported: " + method + + ". Closing connection : " + socketReader.connection); + } + } + + if (error != null) { + // Deliver stanza + socketReader.connection.deliverRawText(error); + return false; + } + else { + // Indicate client that he can proceed and compress the socket + socketReader.connection.deliverRawText(""); + + // Start using compression + socketReader.connection.startCompression(); + return true; + } + } + + /** + * After compression was successful we should open a new stream and offer + * new stream features such as resource binding and session establishment. Notice that + * resource binding and session establishment should only be offered to clients (i.e. not + * to servers or external components) + */ + protected void compressionSuccessful() throws XmlPullParserException, IOException + { + StringBuilder sb = new StringBuilder(340); + sb.append(geStreamHeader()); + sb.append(""); + // Include SASL mechanisms only if client has not been authenticated + if (socketReader.session.getStatus() != Session.STATUS_AUTHENTICATED) { + // Include available SASL Mechanisms + sb.append(ConnectionManager.getInstance().getServerSurrogate().getSASLMechanisms( + socketReader.session)); + } + // Include specific features such as resource binding and session establishment + // for client sessions + String specificFeatures = socketReader.session.getAvailableStreamFeatures(); + if (specificFeatures != null) + { + sb.append(specificFeatures); + } + sb.append(""); + socketReader.connection.deliverRawText(sb.toString()); + } + + private String geStreamHeader() { + StringBuilder sb = new StringBuilder(200); + sb.append(""); + if (socketReader.connection.isFlashClient()) { + sb.append(""); + return sb.toString(); + } + +} diff --git a/src/java/org/jivesoftware/multiplexer/net/SocketSendingTracker.java b/src/java/org/jivesoftware/multiplexer/net/SocketSendingTracker.java new file mode 100644 index 0000000..e260c56 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/SocketSendingTracker.java @@ -0,0 +1,117 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +/** + * A SocketSendingTracker keeps track of all the sockets that are currently sending data and + * checks the health of the sockets to detect hanged connections. If a sending operation takes + * too much time (i.e. exceeds a time limit) then it is assumed that the connection has been + * lost and for some reason the JVM has not been notified of the dead connection. Once a dead + * connection has been detected it will be closed so that the thread that was writing to the + * socket can resume. Resuming locked threads is important since otherwise a complete system halt + * may occur.

+ * + * The time limit to wait before considering a connection dead can be configured changing the + * property xmpp.session.sending-limit. If the property was not defined then a default + * time limit of 60 seconds will be assumed. This means that by default if a sending operation + * takes longer than 60 seconds then the connection will be closed and the client disconnected. + * Therefore, it is important to not set a very low time limit since active clients may be + * incorrectly considered as dead clients. + * + * @author Gaston Dombiak + */ +public class SocketSendingTracker { + + + private static SocketSendingTracker instance = new SocketSendingTracker(); + + /** + * Flag that indicates if the tracket should shutdown the tracking process. + */ + private boolean shutdown = false; + + /** + * Thread used for checking periodically the health of the sockets involved in sending + * operations. + */ + private Thread checkingThread; + + /** + * Returns the unique instance of this class. + * + * @return the unique instance of this class. + */ + public static SocketSendingTracker getInstance() { + return instance; + } + + /** + * Hide the constructor so that only one instance of this class can exist. + */ + private SocketSendingTracker() { + } + + /** + * Start up the daemon thread that will check for the health of the sockets that are + * currently sending data. + */ + public void start() { + shutdown = false; + checkingThread = new Thread("SocketSendingTracker") { + public void run() { + while (!shutdown) { + checkHealth(); + synchronized (this) { + try { + wait(10000); + } + catch (InterruptedException e) { + // Do nothing + } + } + } + } + }; + checkingThread.setDaemon(true); + checkingThread.start(); + } + + /** + * Indicates that the checking thread should be stoped. The thread will be waked up + * so that it can be stoped. + */ + public void shutdown() { + shutdown = true; + if (checkingThread != null) { + // Use a wait/notify algorithm to ensure that the thread stops immediately if it + // was waiting + synchronized (checkingThread) { + checkingThread.notify(); + } + } + } + + /** + * Checks if a socket has been trying to send data for a given amount of time. If it has + * exceded a limit of time then the socket will be closed.

+ * + * It is expected that sending operations will not take too much time so the checking will + * be very fast since very few sockets will be present in the Map and most or all of them + * will not exceed the time limit. Therefore, it is expected the overhead of this class to be + * quite small. + */ + private void checkHealth() { + for (SocketConnection connection : SocketConnection.getInstances()) { + connection.checkHealth(); + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/TLSStatus.java b/src/java/org/jivesoftware/multiplexer/net/TLSStatus.java new file mode 100644 index 0000000..1326b47 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/TLSStatus.java @@ -0,0 +1,47 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +/** + * A TLSStatus enum describing the current handshaking state of this TLS connection. + * + * @author Hao Chen + */ +public enum TLSStatus { + + /** + * ust send data to the remote side before handshaking can continue. + */ + NEED_WRITE, + + /** + * Need to receive data from the remote side before handshaking can continue. + */ + NEED_READ, + + /** + * Not be able to unwrap the incoming data because there were not enough source bytes available + * to make a complete packet. + */ + UNDERFLOW, + + /** + * The operation just closed this side of the SSLEngine, or the operation could not be completed + * because it was already closed. + */ + CLOSED, + + /** + * Handshaking is OK. + */ + OK; +} diff --git a/src/java/org/jivesoftware/multiplexer/net/TLSStreamHandler.java b/src/java/org/jivesoftware/multiplexer/net/TLSStreamHandler.java new file mode 100644 index 0000000..5833d09 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/TLSStreamHandler.java @@ -0,0 +1,407 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.bouncycastle.asn1.*; +import org.jivesoftware.util.Log; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.WritableByteChannel; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * TLSStreamHandler is responsible for securing plain connections by negotiating TLS. By creating + * a new instance of this class the plain connection will be secured. + * + * @author Hao Chen + */ +public class TLSStreamHandler { + + private TLSStreamWriter writer; + + private TLSStreamReader reader; + + private TLSWrapper wrapper; + + private ReadableByteChannel rbc; + private WritableByteChannel wbc; + + private SSLEngine tlsEngine; + + /* + * During the initial handshake, keep track of the next SSLEngine operation that needs to occur: + * + * NEED_WRAP/NEED_UNWRAP + * + * Once the initial handshake has completed, we can short circuit handshake checks with + * initialHSComplete. + */ + private HandshakeStatus initialHSStatus; + private boolean initialHSComplete; + + private int appBBSize; + private int netBBSize; + + /* + * All I/O goes through these buffers. It might be nice to use a cache of ByteBuffers so we're + * not alloc/dealloc'ing ByteBuffer's for each new SSLEngine. Outbound application data is + * supplied to us by our callers. + */ + private ByteBuffer incomingNetBB; + private ByteBuffer outgoingNetBB; + + private ByteBuffer appBB; + + /* + * An empty ByteBuffer for use when one isn't available, say as a source buffer during initial + * handshake wraps or for close operations. + */ + private static ByteBuffer hsBB = ByteBuffer.allocate(0); + + /** + * Returns the identities of the remote server as defined in the specified certificate. The + * identities are defined in the subjectDN of the certificate and it can also be defined in + * the subjectAltName extensions of type "xmpp". When the extension is being used then the + * identities defined in the extension are going to be returned. Otherwise, the value stored in + * the subjectDN is returned. + * + * @param x509Certificate the certificate the holds the identities of the remote server. + * @return the identities of the remote server as defined in the specified certificate. + */ + public static List getPeerIdentities(X509Certificate x509Certificate) { + // Look the identity in the subjectAltName extension if available + List names = getSubjectAlternativeNames(x509Certificate); + if (names.isEmpty()) { + String name = x509Certificate.getSubjectDN().getName(); + name = name.replace("CN=", ""); + // Create an array with the unique identity + names = new ArrayList(); + names.add(name); + } + return names; + } + + /** + * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension + * in the certificate. If none was found then return null. + * + * @param certificate the certificate presented by the remote entity. + * @return the JID representation of an XMPP entity contained as a SubjectAltName extension + * in the certificate. If none was found then return null. + */ + private static List getSubjectAlternativeNames(X509Certificate certificate) { + List identities = new ArrayList(); + try { + Collection altNames = certificate.getSubjectAlternativeNames(); + // Check that the certificate includes the SubjectAltName extension + if (altNames == null) { + return Collections.emptyList(); + } + // Use the type OtherName to search for the certified server name + for (Iterator lists=altNames.iterator(); lists.hasNext();) { + List item = (List) lists.next(); + Integer type = (Integer) item.get(0); + if (type.intValue() == 0) { + // Type OtherName found so return the associated value + try { + // Value is encoded using ASN.1 so decode it to get the server's identity + ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]); + DEREncodable encoded = decoder.readObject(); + encoded = ((DERSequence) encoded).getObjectAt(1); + encoded = ((DERTaggedObject) encoded).getObject(); + encoded = ((DERTaggedObject) encoded).getObject(); + String identity = ((DERUTF8String) encoded).getString(); + // Add the decoded server name to the list of identities + identities.add(identity); + } + catch (UnsupportedEncodingException e) {} + catch (IOException e) {} + catch (Exception e) { + Log.error("Error decoding subjectAltName", e); + } + } + // Other types are not good for XMPP so ignore them + if (Log.isDebugEnabled()) { + Log.debug("SubjectAltName of invalid type found: " + certificate); + } + } + } + catch (CertificateParsingException e) { + Log.error("Error parsing SubjectAltName in certificate: " + certificate, e); + } + return identities; + } + + /** + * Creates a new TLSStreamHandler and secures the plain socket connection. When connecting + * to a remote server then clientMode will be true and + * remoteServer is the server name of the remote server. Otherwise clientMode + * will be false and remoteServer null. + * + * @param socket the plain socket connection to secure + * @param clientMode boolean indicating if this entity is a client or a server. + * @param remoteServer server name of the remote server we are connecting to or null + * when not in client mode. + * @param needClientAuth boolean that indicates if client should authenticate during the TLS + * negotiation. This option is only required when the client is a server since + * EXTERNAL SASL is going to be used. + * @throws java.io.IOException + */ + public TLSStreamHandler(Socket socket, boolean clientMode, String remoteServer, + boolean needClientAuth) throws IOException { + wrapper = new TLSWrapper(clientMode, needClientAuth, remoteServer); + tlsEngine = wrapper.getTlsEngine(); + reader = new TLSStreamReader(wrapper, socket); + writer = new TLSStreamWriter(wrapper, socket); + + // DANIELE: Add code to use directly the socket-channel. + if (socket.getChannel() != null) { + rbc = socket.getChannel(); + wbc = socket.getChannel(); + } + else { + rbc = Channels.newChannel(socket.getInputStream()); + wbc = Channels.newChannel(socket.getOutputStream()); + } + initialHSStatus = HandshakeStatus.NEED_UNWRAP; + initialHSComplete = false; + + netBBSize = tlsEngine.getSession().getPacketBufferSize(); + appBBSize = tlsEngine.getSession().getApplicationBufferSize(); + + incomingNetBB = ByteBuffer.allocate(netBBSize); + outgoingNetBB = ByteBuffer.allocate(netBBSize); + outgoingNetBB.position(0); + outgoingNetBB.limit(0); + + appBB = ByteBuffer.allocate(appBBSize); + + if (clientMode) { + socket.setSoTimeout(0); + socket.setKeepAlive(true); + initialHSStatus = HandshakeStatus.NEED_WRAP; + tlsEngine.beginHandshake(); + } + else if (needClientAuth) { + tlsEngine.setNeedClientAuth(true); + } + } + + public InputStream getInputStream(){ + return reader.getInputStream(); + } + + public OutputStream getOutputStream(){ + return writer.getOutputStream(); + } + + void start() throws IOException { + while (!initialHSComplete) { + initialHSComplete = doHandshake(null); + } + } + + private boolean doHandshake(SelectionKey sk) throws IOException { + + SSLEngineResult result; + + if (initialHSComplete) { + return initialHSComplete; + } + + /* + * Flush out the outgoing buffer, if there's anything left in it. + */ + if (outgoingNetBB.hasRemaining()) { + + if (!flush(outgoingNetBB)) { + return false; + } + + // See if we need to switch from write to read mode. + + switch (initialHSStatus) { + + /* + * Is this the last buffer? + */ + case FINISHED: + initialHSComplete = true; + + case NEED_UNWRAP: + if (sk != null) { + sk.interestOps(SelectionKey.OP_READ); + } + break; + } + + return initialHSComplete; + } + + switch (initialHSStatus) { + + case NEED_UNWRAP: + if (rbc.read(incomingNetBB) == -1) { + tlsEngine.closeInbound(); + return initialHSComplete; + } + + needIO: while (initialHSStatus == HandshakeStatus.NEED_UNWRAP) { + /* + * Don't need to resize requestBB, since no app data should be generated here. + */ + incomingNetBB.flip(); + result = tlsEngine.unwrap(incomingNetBB, appBB); + incomingNetBB.compact(); + + initialHSStatus = result.getHandshakeStatus(); + + switch (result.getStatus()) { + + case OK: + switch (initialHSStatus) { + case NOT_HANDSHAKING: + throw new IOException("Not handshaking during initial handshake"); + + case NEED_TASK: + initialHSStatus = doTasks(); + break; + + case FINISHED: + initialHSComplete = true; + break needIO; + } + + break; + + case BUFFER_UNDERFLOW: + /* + * Need to go reread the Channel for more data. + */ + if (sk != null) { + sk.interestOps(SelectionKey.OP_READ); + } + break needIO; + + default: // BUFFER_OVERFLOW/CLOSED: + throw new IOException("Received" + result.getStatus() + + "during initial handshaking"); + } + } + + /* + * Just transitioned from read to write. + */ + if (initialHSStatus != HandshakeStatus.NEED_WRAP) { + break; + } + + // Fall through and fill the write buffers. + + case NEED_WRAP: + /* + * The flush above guarantees the out buffer to be empty + */ + outgoingNetBB.clear(); + result = tlsEngine.wrap(hsBB, outgoingNetBB); + outgoingNetBB.flip(); + + initialHSStatus = result.getHandshakeStatus(); + + switch (result.getStatus()) { + case OK: + + if (initialHSStatus == HandshakeStatus.NEED_TASK) { + initialHSStatus = doTasks(); + } + + if (sk != null) { + sk.interestOps(SelectionKey.OP_WRITE); + } + + break; + + default: // BUFFER_OVERFLOW/BUFFER_UNDERFLOW/CLOSED: + throw new IOException("Received" + result.getStatus() + + "during initial handshaking"); + } + break; + + default: // NOT_HANDSHAKING/NEED_TASK/FINISHED + throw new RuntimeException("Invalid Handshaking State" + initialHSStatus); + } // switch + + return initialHSComplete; + } + + /* + * Writes ByteBuffer to the SocketChannel. Returns true when the ByteBuffer has no remaining + * data. + */ + private boolean flush(ByteBuffer bb) throws IOException { + wbc.write(bb); + return !bb.hasRemaining(); + } + + /* + * Do all the outstanding handshake tasks in the current Thread. + */ + private SSLEngineResult.HandshakeStatus doTasks() { + + Runnable runnable; + + /* + * We could run this in a separate thread, but do in the current for now. + */ + while ((runnable = tlsEngine.getDelegatedTask()) != null) { + runnable.run(); + } + return tlsEngine.getHandshakeStatus(); + } + + /** + * Closes the channels that will end up closing the input and output streams of the connection. + * The channels implement the InterruptibleChannel interface so any other thread that was + * blocked in an I/O operation will be interrupted and will get an exception. + * + * @throws IOException if an I/O error occurs. + */ + public void close() throws IOException { + wbc.close(); + rbc.close(); + } + + /** + * Returns the SSLSession in use. The session specifies a particular cipher suite which + * is being actively used by all connections in that session, as well as the identities + * of the session's client and server. + * + * @return the SSLSession in use. + */ + public SSLSession getSSLSession() { + return tlsEngine.getSession(); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/TLSStreamReader.java b/src/java/org/jivesoftware/multiplexer/net/TLSStreamReader.java new file mode 100644 index 0000000..d886b2a --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/TLSStreamReader.java @@ -0,0 +1,193 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; + +/** + * A TLSStreamReader that returns a special InputStream that hides the ByteBuffers + * used by the underlying Channels. + * + * @author Hao Chen + */ +public class TLSStreamReader { + + /** + * TLSWrapper is a TLS wrapper for connections requiring TLS protocol. + */ + private TLSWrapper wrapper; + + private ReadableByteChannel rbc; + + /** + * inNetBB buffer keeps data read from socket. + */ + private ByteBuffer inNetBB; + + /** + * inAppBB buffer keeps decypted data. + */ + private ByteBuffer inAppBB; + + private TLSStatus lastStatus; + + public TLSStreamReader(TLSWrapper tlsWrapper, Socket socket) throws IOException { + wrapper = tlsWrapper; + // DANIELE: Add code to use directly the socket channel + if (socket.getChannel() != null) { + rbc = socket.getChannel(); + } + else { + rbc = Channels.newChannel(socket.getInputStream()); + } + inNetBB = ByteBuffer.allocate(wrapper.getNetBuffSize()); + inAppBB = ByteBuffer.allocate(wrapper.getAppBuffSize()); + } + + /* + * Read TLS encrpyted data from SocketChannel, and use decrypt method to decypt. + */ + private void doRead() throws IOException { + //System.out.println("doRead inNet position: " + inNetBB.position() + " capacity: " + inNetBB.capacity() + " (before read)"); + + // Read from the channel and fill inNetBB with the encrypted data + final int cnt = rbc.read(inNetBB); + if (cnt > 0) { + //System.out.println("doRead inNet position: " + inNetBB.position() + " capacity: " + inNetBB.capacity() + " (after read)"); + //System.out.println("doRead inAppBB (before decrypt) position: " + inAppBB.position() + " limit: " + inAppBB.limit() + " capacity: " + inAppBB.capacity()); + + // Decode encrypted data + inAppBB = decrypt(inNetBB, inAppBB); + + ///System.out.println("doRead inAppBB (after decrypt) position: " + inAppBB.position() + " limit: " + inAppBB.limit() + " capacity: " + inAppBB.capacity() + " lastStatus: " + lastStatus); + + if (lastStatus == TLSStatus.OK) { + // All the data contained in inNetBB was read and decrypted so we can safely + // set the position of inAppBB to 0 to process it. + inAppBB.flip(); + } + else { + // Some data in inNetBB was not decrypted since it is not complete. A + // bufferunderflow was detected since the TLS packet is not complete to be + // decrypted. We need to read more data from the channel to decrypt the whole + // TLS packet. The inNetBB byte buffer has been compacted so the read and + // decrypted is discarded and only the unread and encrypted data is left in the + // buffer. The inAppBB has been completed with the decrypted data and we must + // leave the position at the end of the written so that in the next doRead the + // decrypted data is appended to the end of the buffer. + //System.out.println("Reading more data from the channel (UNDERFLOW state)"); + doRead(); + } + } else { + if (cnt == -1) { + inAppBB.flip(); + rbc.close(); + } + } + } + + /* + * This method uses TLSWrapper to decrypt TLS encrypted data. + */ + private ByteBuffer decrypt(ByteBuffer input, ByteBuffer output) throws IOException { + ByteBuffer out = output; + input.flip(); + do { + // Decode SSL/TLS network data and place it in the app buffer + out = wrapper.unwrap(input, out); + + lastStatus = wrapper.getStatus(); + } + while ((lastStatus == TLSStatus.NEED_READ || lastStatus == TLSStatus.OK) && + input.hasRemaining()); + + if (input.hasRemaining()) { + // Complete TLS packets have been read, decrypted and written to the output buffer. + // However, the input buffer contains incomplete TLS packets that cannot be decrpted. + // Discard the read data and keep the unread data in the input buffer. The channel will + // be read again to obtain the missing data to complete the TLS packet. So in the next + // round the TLS packet will be decrypted and written to the output buffer + input.compact(); + } else { + // All the encrypted data in the inpu buffer was decrypted so we can clear + // the input buffer. + input.clear(); + } + + return out; + } + + public InputStream getInputStream() { + return createInputStream(); + } + + /* + * Returns an input stream for a ByteBuffer. The read() methods use the relative ByteBuffer + * get() methods. + */ + private InputStream createInputStream() { + return new InputStream() { + public synchronized int read() throws IOException { + doRead(); + if (!inAppBB.hasRemaining()) { + return -1; + } + return inAppBB.get(); + } + + public synchronized int read(byte[] bytes, int off, int len) throws IOException { + // Check if in the previous read the inAppBB ByteBuffer remained with unread data. + // If all the data was consumed then read from the socket channel. Otherwise, + // consume the data contained in the buffer. + if (inAppBB.position() == 0) { + // Read from the channel the encrypted data, decrypt it and load it + // into inAppBB + doRead(); + } + else { + //System.out.println("#createInputStream. Detected previously unread data. position: " + inAppBB.position()); + + // The inAppBB contains data from a previous read so set the position to 0 + // to consume it + inAppBB.flip(); + } + len = Math.min(len, inAppBB.remaining()); + if (len == 0) { + // Nothing was read so the end of stream should have been reached. + return -1; + } + inAppBB.get(bytes, off, len); + // If the requested length is less than the limit of inAppBB then all the data + // inside inAppBB was not read. In that case we need to discard the read data and + // keep only the unread data to be consume the next time this method is called + if (inAppBB.hasRemaining()) { + // Discard read data and move unread data to the begining of the buffer. Leave + // the position at the end of the buffer as a way to indicate that there is + // unread data + inAppBB.compact(); + + //System.out.println("#createInputStream. Data left unread. inAppBB compacted. position: " + inAppBB.position() + " limit: " + inAppBB.limit()); + } + else { + // Everything was read so reset the buffer + inAppBB.clear(); + } + return len; + } + }; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/TLSStreamWriter.java b/src/java/org/jivesoftware/multiplexer/net/TLSStreamWriter.java new file mode 100644 index 0000000..5d1bc64 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/TLSStreamWriter.java @@ -0,0 +1,135 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; + +/** + * A TLSStreamWriter that returns a special OutputStream that hides the ByteBuffers + * used by the underlying Channels. + * + * @author Hao Chen + * + */ +public class TLSStreamWriter { + + /** + * TLSWrapper is a TLS wrapper for connections requiring TLS protocol. + */ + private TLSWrapper wrapper; + + private WritableByteChannel wbc; + + private ByteBuffer outAppData; + + public TLSStreamWriter(TLSWrapper tlsWrapper, Socket socket) throws IOException { + wrapper = tlsWrapper; + // DANIELE: Add code to use directly the socket channel + if (socket.getChannel() != null) { + wbc = socket.getChannel(); + } + else { + wbc = Channels.newChannel(socket.getOutputStream()); + } + outAppData = ByteBuffer.allocate(tlsWrapper.getAppBuffSize()); + } + + private void doWrite(ByteBuffer buff) throws IOException { + + if (buff == null) { + // Possibly handshaking process + buff = ByteBuffer.allocate(0); + } + + if (wrapper == null) { + writeToSocket(buff); + } else { + tlsWrite(buff); + } + } + + private void tlsWrite(ByteBuffer buf) throws IOException { + ByteBuffer tlsBuffer = null; + ByteBuffer tlsOutput = null; + do { + // TODO Consider optimizing by not creating new instances each time + tlsBuffer = ByteBuffer.allocate(Math.min(buf.remaining(), wrapper.getAppBuffSize())); + tlsOutput = ByteBuffer.allocate(wrapper.getNetBuffSize()); + + while (tlsBuffer.hasRemaining() && buf.hasRemaining()) { + tlsBuffer.put(buf.get()); + } + + tlsBuffer.flip(); + wrapper.wrap(tlsBuffer, tlsOutput); + + tlsOutput.flip(); + writeToSocket(tlsOutput); + + tlsOutput.clear(); + } while (buf.hasRemaining()); + } + + /* + * Writes outNetData to the SocketChannel.

Returns true when the ByteBuffer has no remaining + * data. + */ + private boolean writeToSocket(ByteBuffer outNetData) throws IOException { + wbc.write(outNetData); + return !outNetData.hasRemaining(); + } + + public OutputStream getOutputStream() { + return createOutputStream(); + } + + /* + * Returns an output stream for a ByteBuffer. The write() methods use the relative ByteBuffer + * put() methods. + */ + private OutputStream createOutputStream() { + return new OutputStream() { + public synchronized void write(int b) throws IOException { + outAppData.put((byte) b); + outAppData.flip(); + doWrite(outAppData); + outAppData.clear(); + } + + public synchronized void write(byte[] bytes, int off, int len) throws IOException { + outAppData = resizeApplicationBuffer(bytes.length); + outAppData.put(bytes, off, len); + outAppData.flip(); + doWrite(outAppData); + outAppData.clear(); + } + }; + } + + private ByteBuffer resizeApplicationBuffer(int increment) { + // TODO Creating new buffers and copying over old one may not scale. Consider using views. Thanks to Noah for the tip. + if (outAppData.remaining() < increment) { + ByteBuffer bb = ByteBuffer.allocate(outAppData.capacity() + wrapper.getAppBuffSize()); + outAppData.flip(); + bb.put(outAppData); + return bb; + } else { + return outAppData; + } + } + +} diff --git a/src/java/org/jivesoftware/multiplexer/net/TLSWrapper.java b/src/java/org/jivesoftware/multiplexer/net/TLSWrapper.java new file mode 100644 index 0000000..fef2340 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/TLSWrapper.java @@ -0,0 +1,277 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.Log; + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; + +/** + * Creates and initializes the SSLContext instance to use to secure the plain connection. This + * class is also responsible for encoding and decoding the encrypted data and place it into + * the corresponding the {@link ByteBuffer}. + * + * @author Hao Chen + */ +public class TLSWrapper { + + /* + * Enables logging of the SSLEngine operations. + */ + private boolean logging = false; + + /* + * Enables the JSSE system debugging system property: + * + * -Djavax.net.debug=all + * + * This gives a lot of low-level information about operations underway, including specific + * handshake messages, and might be best examined after gaining some familiarity with this + * application. + */ + private static boolean debug = false; + + private static final String PROTOCOL = "TLS"; + + private SSLEngine tlsEngine; + private SSLEngineResult tlsEngineResult; + + private int netBuffSize; + private int appBuffSize; + + public TLSWrapper(boolean clientMode, boolean needClientAuth, String remoteServer) { + + if (debug) { + System.setProperty("javax.net.debug", "all"); + } + + // Create/initialize the SSLContext with key material + try { + // First initialize the key and trust material. + KeyStore ksKeys = SSLConfig.getKeyStore(); + String keypass = SSLConfig.getKeyPassword(); + + KeyStore ksTrust = SSLConfig.getTrustStore(); + String trustpass = SSLConfig.getTrustPassword(); + + // KeyManager's decide which key material to use. + KeyManager[] km = SSLJiveKeyManagerFactory.getKeyManagers(ksKeys, keypass); + + // TrustManager's decide whether to allow connections. + TrustManager[] tm = SSLJiveTrustManagerFactory.getTrustManagers(ksTrust, trustpass); + if (clientMode || needClientAuth) { + // Check if we can trust certificates presented by the server + tm = new TrustManager[]{new ServerTrustManager(remoteServer, ksTrust)}; + } + + SSLContext tlsContext = SSLContext.getInstance(PROTOCOL); + + tlsContext.init(km, tm, null); + + /* + * Configure the tlsEngine to act as a server in the SSL/TLS handshake. We're a server, + * so no need to use host/port variant. + * + * The first call for a server is a NEED_UNWRAP. + */ + tlsEngine = tlsContext.createSSLEngine(); + tlsEngine.setUseClientMode(clientMode); + SSLSession session = tlsEngine.getSession(); + + netBuffSize = session.getPacketBufferSize(); + appBuffSize = session.getApplicationBufferSize(); + + } catch (KeyManagementException e) { + Log.error("TLSHandler startup problem.\n" + " SSLContext initialisation failed.", e); + } catch (NoSuchAlgorithmException e) { + Log.error("TLSHandler startup problem.\n" + " The " + PROTOCOL + " does not exist", e); + } catch (IOException e) { + Log.error("TLSHandler startup problem.\n" + + " the KeyStore or TrustStore does not exist", e); + } + } + + public int getNetBuffSize() { + return netBuffSize; + } + + public int getAppBuffSize() { + return appBuffSize; + } + + /** + * Returns whether unwrap(ByteBuffer, ByteBuffer) will accept any more inbound data messages and + * whether wrap(ByteBuffer, ByteBuffer) will produce any more outbound data messages. + * + * @return true if the TLSHandler will not consume anymore network data and will not produce any + * anymore network data. + */ + public boolean isEngineClosed() { + return (tlsEngine.isOutboundDone() && tlsEngine.isInboundDone()); + } + + public void enableLogging(boolean logging) { + this.logging = logging; + } + + /** + * Attempts to decode SSL/TLS network data into a subsequence of plaintext application data + * buffers. Depending on the state of the TLSWrapper, this method may consume network data + * without producing any application data (for example, it may consume handshake data.) + * + * If this TLSWrapper has not yet started its initial handshake, this method will automatically + * start the handshake. + * + * @param net a ByteBuffer containing inbound network data + * @param app a ByteBuffer to hold inbound application data + * @return a ByteBuffer containing inbound application data + * @throws SSLException A problem was encountered while processing the data that caused the + * TLSHandler to abort. + */ + public ByteBuffer unwrap(ByteBuffer net, ByteBuffer app) throws SSLException { + ByteBuffer out = app; + out = resizeApplicationBuffer(out);// guarantees enough room for unwrap + tlsEngineResult = tlsEngine.unwrap(net, out); + log("server unwrap: ", tlsEngineResult); + if (tlsEngineResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + // If the result indicates that we have outstanding tasks to do, go + // ahead and run them in this thread. + doTasks(); + } + return out; + } + + /** + * Attempts to encode a buffer of plaintext application data into TLS network data. Depending on + * the state of the TLSWrapper, this method may produce network data without consuming any + * application data (for example, it may generate handshake data). + * + * If this TLSWrapper has not yet started its initial handshake, this method will automatically + * start the handshake. + * + * @param app a ByteBuffer containing outbound application data + * @param net a ByteBuffer to hold outbound network data + * @throws SSLException A problem was encountered while processing the data that caused the + * TLSWrapper to abort. + */ + public void wrap(ByteBuffer app, ByteBuffer net) throws SSLException { + tlsEngineResult = tlsEngine.wrap(app, net); + log("server wrap: ", tlsEngineResult); + if (tlsEngineResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + // If the result indicates that we have outstanding tasks to do, go + // ahead and run them in this thread. + doTasks(); + } + } + + /** + * Signals that no more outbound application data will be sent on this TLSHandler. + * + * @throws SSLException + */ + public void close() throws SSLException { + // Indicate that application is done with engine + tlsEngine.closeOutbound(); + } + + /** + * Returns the current status for this TLSHandler. + * + * @return the current TLSStatus + */ + public TLSStatus getStatus() { + TLSStatus status = null; + if (tlsEngineResult != null && tlsEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) { + status = TLSStatus.UNDERFLOW; + } else { + if (tlsEngineResult != null && tlsEngineResult.getStatus() == Status.CLOSED) { + status = TLSStatus.CLOSED; + } else { + switch (tlsEngine.getHandshakeStatus()) { + case NEED_WRAP: + status = TLSStatus.NEED_WRITE; + break; + case NEED_UNWRAP: + status = TLSStatus.NEED_READ; + break; + default: + status = TLSStatus.OK; + break; + } + } + } + return status; + } + + private ByteBuffer resizeApplicationBuffer(ByteBuffer app) { + // TODO Creating new buffers and copying over old one may not scale and may even be a + // security risk. Consider using views. Thanks to Noah for the tip. + if (app.remaining() < appBuffSize) { + ByteBuffer bb = ByteBuffer.allocate(app.capacity() + appBuffSize); + app.flip(); + bb.put(app); + return bb; + } else { + return app; + } + } + + /* + * Do all the outstanding handshake tasks in the current Thread. + */ + private SSLEngineResult.HandshakeStatus doTasks() { + + Runnable runnable; + + /* + * We could run this in a separate thread, but do in the current for now. + */ + while ((runnable = tlsEngine.getDelegatedTask()) != null) { + runnable.run(); + } + return tlsEngine.getHandshakeStatus(); + } + + /* + * Logging code + */ + private boolean resultOnce = true; + + private void log(String str, SSLEngineResult result) { + if (!logging) { + return; + } + if (resultOnce) { + resultOnce = false; + Log.info("The format of the SSLEngineResult is: \n" + + "\t\"getStatus() / getHandshakeStatus()\" +\n" + + "\t\"bytesConsumed() / bytesProduced()\"\n"); + } + HandshakeStatus hsStatus = result.getHandshakeStatus(); + Log.info(str + result.getStatus() + "/" + hsStatus + ", " + result.bytesConsumed() + "/" + + result.bytesProduced() + " bytes"); + if (hsStatus == HandshakeStatus.FINISHED) { + Log.info("\t...ready for application data"); + } + } + + protected SSLEngine getTlsEngine() { + return tlsEngine; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/XMLSocketWriter.java b/src/java/org/jivesoftware/multiplexer/net/XMLSocketWriter.java new file mode 100644 index 0000000..e9553e6 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/XMLSocketWriter.java @@ -0,0 +1,49 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.net; + +import org.jivesoftware.util.XMLWriter; + +import java.io.IOException; +import java.io.Writer; + +/** + * XMLWriter whose writer is actually sending data on a socket connection. Since sending data over + * a socket may have particular type of errors this class tries to deal with those errors. + */ +public class XMLSocketWriter extends XMLWriter { + + private SocketConnection connection; + + public XMLSocketWriter(Writer writer, SocketConnection connection) { + super( writer, DEFAULT_FORMAT ); + this.connection = connection; + } + + /** + * Flushes the underlying writer making sure that if the connection is dead then the thread + * that is flushing does not end up in an endless wait. + * + * @throws IOException if an I/O error occurs while flushing the writer. + */ + public void flush() throws IOException { + // Register that we have started sending data + connection.writeStarted(); + try { + super.flush(); + } + finally { + // Register that we have finished sending data + connection.writeFinished(); + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/spi/ClientFailoverDeliverer.java b/src/java/org/jivesoftware/multiplexer/spi/ClientFailoverDeliverer.java new file mode 100644 index 0000000..1defb17 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/spi/ClientFailoverDeliverer.java @@ -0,0 +1,57 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.spi; + +import org.jivesoftware.multiplexer.PacketDeliverer; +import org.jivesoftware.multiplexer.ServerSurrogate; +import org.jivesoftware.multiplexer.ConnectionManager; +import org.dom4j.Element; + +/** + * Deliverer to use when a stanza received from the server failed to be forwarded + * to a client. The deliverer will inform the server of the failed operation. + * + * @author Gaston Dombiak + */ +public class ClientFailoverDeliverer implements PacketDeliverer { + + private ServerSurrogate serverSurrogate = ConnectionManager.getInstance().getServerSurrogate(); + private String streamID; + + public void setStreamID(String streamID) { + this.streamID = streamID; + } + + public void deliver(Element stanza) { + // Inform the server that the wrapped stanza was not delivered + String tag = stanza.getName(); + if ("message".equals(tag)) { + serverSurrogate.deliveryFailed(stanza, streamID); + } + else if ("iq".equals(tag)) { + String type = stanza.attributeValue("type", "get"); + if ("get".equals(type) || "set".equals(type)) { + // Build IQ of type ERROR + Element reply = stanza.createCopy(); + reply.addAttribute("type", "error"); + reply.addAttribute("from", stanza.attributeValue("to")); + reply.addAttribute("to", stanza.attributeValue("from")); + Element error = reply.addElement("error"); + error.addAttribute("type", "wait"); + error.addElement("unexpected-request") + .addAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas"); + // Bounce the failed IQ packet + serverSurrogate.send(reply, streamID); + } + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/spi/ServerFailoverDeliverer.java b/src/java/org/jivesoftware/multiplexer/spi/ServerFailoverDeliverer.java new file mode 100644 index 0000000..00d4677 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/spi/ServerFailoverDeliverer.java @@ -0,0 +1,53 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.spi; + +import org.dom4j.Element; +import org.jivesoftware.multiplexer.ClientSession; +import org.jivesoftware.multiplexer.PacketDeliverer; + +/** + * Deliverer to use when a stanza received from a client failed to be forwarded + * to the server. The deliverer will try to return it to the sender. + * + * @author Gaston Dombiak + */ +public class ServerFailoverDeliverer implements PacketDeliverer { + + public void deliver(Element stanza) { + if ("route".equals(stanza.getName())) { + // Inform the client that the stanza was not delivered to the server + // Get the stream id that identifies the client that sent the stanza + String streamID = stanza.attributeValue("streamid"); + // Get the wrapped stanza + Element wrapped = (Element) stanza.elementIterator().next(); + String tag = wrapped.getName(); + if ("message".equals(tag) || "iq".equals(tag) || "presence".equals(tag)) { + // Build ERROR bouncing packet + Element reply = wrapped.createCopy(); + reply.addAttribute("type", "error"); + reply.addAttribute("from", wrapped.attributeValue("to")); + reply.addAttribute("to", wrapped.attributeValue("from")); + Element error = reply.addElement("error"); + error.addAttribute("type", "wait"); + error.addElement("internal-server-error") + .addAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas"); + // Get the session that matches the specified stream ID + ClientSession session = ClientSession.getSession(streamID); + if (session != null) { + // Bounce the failed packet + session.deliver(reply); + } + } + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/spi/ServerRouter.java b/src/java/org/jivesoftware/multiplexer/spi/ServerRouter.java new file mode 100644 index 0000000..dd0a656 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/spi/ServerRouter.java @@ -0,0 +1,35 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.spi; + +import org.jivesoftware.multiplexer.PacketRouter; +import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.multiplexer.ServerSurrogate; +import org.dom4j.Element; + +/** + * Packet router that will route all traffic to the server. + * + * @author Gaston Dombiak + */ +public class ServerRouter implements PacketRouter { + + private ServerSurrogate serverSurrogate; + + public ServerRouter() { + serverSurrogate = ConnectionManager.getInstance().getServerSurrogate(); + } + + public void route(Element stanza, String streamID) { + serverSurrogate.send(stanza, streamID); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/starter/JiveClassLoader.java b/src/java/org/jivesoftware/multiplexer/starter/JiveClassLoader.java new file mode 100644 index 0000000..e794f70 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/starter/JiveClassLoader.java @@ -0,0 +1,69 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.starter; + +import java.io.File; +import java.io.FilenameFilter; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * A simple classloader to extend the classpath to + * include all jars in a lib directory.

+ * + * The new classpath includes all *.jar and *.zip + * archives (zip is commonly used in packaging JDBC drivers). The extended + * classpath is used for both the initial server startup, as well as loading + * plug-in support jars. + * + * @author Derek DeMoro + * @author Iain Shigeoka + */ +class JiveClassLoader extends URLClassLoader { + + /** + * Constructs the classloader. + * + * @param parent the parent class loader (or null for none). + * @param libDir the directory to load jar files from. + * @throws java.net.MalformedURLException if the libDir path is not valid. + */ + JiveClassLoader(ClassLoader parent, File libDir) throws MalformedURLException { + super(new URL[] { libDir.toURL() }, parent); + + File[] jars = libDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + boolean accept = false; + String smallName = name.toLowerCase(); + if (smallName.endsWith(".jar")) { + accept = true; + } + else if (smallName.endsWith(".zip")) { + accept = true; + } + return accept; + } + }); + + // Do nothing if no jar or zip files were found + if (jars == null) { + return; + } + + for (int i = 0; i < jars.length; i++) { + if (jars[i].isFile()) { + addURL(jars[i].toURL()); + } + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/starter/ServerStarter.java b/src/java/org/jivesoftware/multiplexer/starter/ServerStarter.java new file mode 100644 index 0000000..e8e1ed6 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/starter/ServerStarter.java @@ -0,0 +1,174 @@ +/* + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.starter; + +import org.jivesoftware.util.Log; + +import java.io.*; +import java.util.jar.Pack200; +import java.util.jar.JarOutputStream; + +/** + * Starts the core XMPP server. A bootstrap class that configures classloaders + * to ensure easy, dynamic server startup. + * + * This class should be for standalone mode only. Connection managers launched + * through a J2EE container (servlet/EJB) will use those environment's + * classloading facilities to ensure proper startup.

+ * + * Tasks:

    + *
  • Unpack any pack files in the lib directory (Pack200 encoded JAR files).
  • + *
  • Add all jars in the lib directory to the classpath.
  • + *
  • Add the config directory to the classpath for loadResource()
  • + *
  • Start the server
  • + *
+ * + * Note: if the enviroment property cmanager.lib.dir is specified + * ServerStarter will attempt to use this value as the value for Connection Manager's lib + * directory. If the property is not specified the default value of ../lib will be used. + * + * @author Gaston Dombiak + */ +public class ServerStarter { + + /** + * Default to this location if one has not been specified + */ + private static final String DEFAULT_LIB_DIR = "../lib"; + + public static void main(String [] args) { + new ServerStarter().start(); + } + + /** + * Starts the server by loading and instantiating the bootstrap + * container. Once the start method is called, the server is + * started and the server starter should not be used again. + */ + private void start() { + // Verify that Java 1.5 or later is being used + if (System.getProperty("java.version").compareTo("1.5") < 0) { + System.out.println("Connection Manager requires Java 1.5 or later"); + System.err.println("Connection Manager requires Java 1.5 or later"); + return; + } + + // Setup the classpath using JiveClassLoader + try { + // Load up the bootstrap container + final ClassLoader parent = findParentClassLoader(); + + String libDirString = System.getProperty("cmanager.lib.dir"); + + File libDir; + if (libDirString != null) { + // If the lib directory property has been specified and it actually + // exists use it, else use the default + libDir = new File(libDirString); + if (!libDir.exists()) { + Log.warn("Lib directory " + libDirString + + " does not exist. Using default " + DEFAULT_LIB_DIR); + libDir = new File(DEFAULT_LIB_DIR); + } + } + else { + libDir = new File(DEFAULT_LIB_DIR); + } + + // Unpack any pack files. + unpackArchives(libDir); + + ClassLoader loader = new JiveClassLoader(parent, libDir); + + Thread.currentThread().setContextClassLoader(loader); + Class containerClass = loader.loadClass( + "org.jivesoftware.multiplexer.ConnectionManager"); + containerClass.newInstance(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Locates the best class loader based on context (see class description). + * + * @return The best parent classloader to use + */ + private ClassLoader findParentClassLoader() { + ClassLoader parent = Thread.currentThread().getContextClassLoader(); + if (parent == null) { + parent = this.getClass().getClassLoader(); + if (parent == null) { + parent = ClassLoader.getSystemClassLoader(); + } + } + return parent; + } + + /** + * Converts any pack files in a directory into standard JAR files. Each + * pack file will be deleted after being converted to a JAR. If no + * pack files are found, this method does nothing. + * + * @param libDir the directory containing pack files. + */ + private void unpackArchives(File libDir) { + // Get a list of all packed files in the lib directory. + File [] packedFiles = libDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".pack"); + } + }); + + if (packedFiles == null) { + // Do nothing since no .pack files were found + return; + } + + // Unpack each. + boolean unpacked = false; + for (File packedFile : packedFiles) { + try { + String jarName = packedFile.getName().substring(0, + packedFile.getName().length() - ".pack".length()); + // Delete JAR file with same name if it exists (could be due to upgrade + // from old Wildfire release). + File jarFile = new File(libDir, jarName); + if (jarFile.exists()) { + jarFile.delete(); + } + + InputStream in = new BufferedInputStream(new FileInputStream(packedFile)); + JarOutputStream out = new JarOutputStream(new BufferedOutputStream( + new FileOutputStream(new File(libDir, jarName)))); + Pack200.Unpacker unpacker = Pack200.newUnpacker(); + // Print something so the user knows something is happening. + System.out.print("."); + // Call the unpacker + unpacker.unpack(in, out); + + in.close(); + out.close(); + packedFile.delete(); + unpacked = true; + } + catch (Exception e) { + e.printStackTrace(); + } + } + // Print newline if unpacking happened. + if (unpacked) { + System.out.println(); + } + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/task/ClientTask.java b/src/java/org/jivesoftware/multiplexer/task/ClientTask.java new file mode 100644 index 0000000..72ec395 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/ClientTask.java @@ -0,0 +1,33 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.task; + +/** + * Base class for tasks that were requested by clients and that involves the server. + * Example of tasks are: forwarding stanzas to the server or indicating the server that + * a new client has connected. + * + * @author Gaston Dombiak + */ +public abstract class ClientTask implements Runnable { + + protected String streamID; + + protected ClientTask(String streamID) { + this.streamID = streamID; + } + + /** + * Execute the corresponding action when the server is not available. + */ + public abstract void serverNotAvailable(); +} diff --git a/src/java/org/jivesoftware/multiplexer/task/CloseSessionTask.java b/src/java/org/jivesoftware/multiplexer/task/CloseSessionTask.java new file mode 100644 index 0000000..5131ceb --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/CloseSessionTask.java @@ -0,0 +1,37 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.task; + +import org.jivesoftware.multiplexer.ConnectionWorkerThread; + +/** + * Task that notifies the server that a client session has been closed. This task + * is executed right after clients send their end of stream element or if clients + * connections are lost. + * + * @author Gaston Dombiak + */ +public class CloseSessionTask extends ClientTask { + + public CloseSessionTask(String streamID) { + super(streamID); + } + + public void run() { + ConnectionWorkerThread workerThread = (ConnectionWorkerThread) Thread.currentThread(); + workerThread.clientSessionClosed(streamID); + } + + public void serverNotAvailable() { + // Do nothing; + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/task/DeliveryFailedTask.java b/src/java/org/jivesoftware/multiplexer/task/DeliveryFailedTask.java new file mode 100644 index 0000000..d8c7507 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/DeliveryFailedTask.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.task; + +import org.jivesoftware.multiplexer.ConnectionWorkerThread; +import org.dom4j.Element; + +/** + * Task that indicates the server that delivery of a packet to a client has failed. + * The most probable reason for this is that the client logged out at the time + * the server sent a stanza to the client. + * + * @author Gaston Dombiak + */ +public class DeliveryFailedTask extends ClientTask { + + private Element stanza; + + public DeliveryFailedTask(String streamID, Element stanza) { + super(streamID); + this.stanza = stanza; + } + + public void run() { + ConnectionWorkerThread workerThread = (ConnectionWorkerThread) Thread.currentThread(); + workerThread.deliveryFailed(stanza, streamID); + } + + public void serverNotAvailable() { + // Do nothing; + } +} diff --git a/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java b/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java new file mode 100644 index 0000000..ea87e20 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java @@ -0,0 +1,38 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.task; + +import org.jivesoftware.multiplexer.ConnectionWorkerThread; +import org.jivesoftware.multiplexer.ClientSession; + +/** + * Task that notifies the server that a new client session has been created. This task + * is executed right after clients send their initial stream header. + * + * @author Gaston Dombiak + */ +public class NewSessionTask extends ClientTask { + + public NewSessionTask(String streamID) { + super(streamID); + } + + public void run() { + ConnectionWorkerThread workerThread = (ConnectionWorkerThread) Thread.currentThread(); + workerThread.clientSessionCreated(streamID); + } + + public void serverNotAvailable() { + // Close client session indicating that the server is not available + ClientSession.getSession(streamID).close(true); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/task/RouteTask.java b/src/java/org/jivesoftware/multiplexer/task/RouteTask.java new file mode 100644 index 0000000..ba8b95d --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/RouteTask.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.multiplexer.task; + +import org.jivesoftware.multiplexer.ConnectionWorkerThread; +import org.jivesoftware.multiplexer.ClientSession; +import org.dom4j.Element; + +/** + * Task that forwards client packets to the server. + * + * @author Gaston Dombiak + */ +public class RouteTask extends ClientTask { + + private Element stanza; + + public RouteTask(String streamID, Element stanza) { + super(streamID); + this.stanza = stanza; + } + + public void run() { + ConnectionWorkerThread workerThread = (ConnectionWorkerThread) Thread.currentThread(); + workerThread.deliver(stanza, streamID); + } + + public void serverNotAvailable() { + // Close client session indicating that the server is not available + ClientSession.getSession(streamID).close(true); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/task/overview.html b/src/java/org/jivesoftware/multiplexer/task/overview.html new file mode 100644 index 0000000..44c80e6 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/task/overview.html @@ -0,0 +1,5 @@ + +Tasks originated by client actions that imply a server notification. Example of +tasks are: forwarding stanzas to the server or indicating the server that a new +client has connected.. + \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/Base64.java b/src/java/org/jivesoftware/util/Base64.java new file mode 100644 index 0000000..4c136fb --- /dev/null +++ b/src/java/org/jivesoftware/util/Base64.java @@ -0,0 +1,1416 @@ +package org.jivesoftware.util; + +/** + * Encodes and decodes to and from Base64 notation.

Change Log: + *

+ *
    + *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. + * Added some convenience methods for reading and writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on + * systems with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. Now + * everything is more consolidated and cleaner. The code now detects when data + * that's being decoded is gzip-compressed and will decompress it automatically. + * Generally things are cleaner. You'll probably have to change some method + * calls that you were making to support the new options format (ints + * that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using + * decode( String s, boolean gzipCompressed ). Added the ability to + * "suspend" encoding in the Output Stream so you can turn on and off the + * encoding if you need to embed base64 data in an otherwise "normal" stream + * (like an XML file).
  • + *
  • v1.5 - Output stream pases on flush() command but doesn't do anything + * itself. This helps when using GZIP streams. Added the ability to + * GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input + * stream where last buffer being read, if not completely full, was not + * returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the + * wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ *

I am placing this code in the Public Domain. Do with it as you + * will. This software comes with no guarantees or warranties but with plenty of + * well-wishing instead! Please visit http://iharder.net/base64 periodically + * to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.1 + */ +class Base64 { + + /* ******** P U B L I C F I E L D S ******** */ + + /** + * No options specified. Value is zero. + */ + public final static int NO_OPTIONS = 0; + + /** + * Specify encoding. + */ + public final static int ENCODE = 1; + + /** + * Specify decoding. + */ + public final static int DECODE = 0; + + /** + * Specify that data should be gzip-compressed. + */ + public final static int GZIP = 2; + + /** + * Don't break lines when encoding (violates strict Base64 specification) + */ + public final static int DONT_BREAK_LINES = 8; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** + * Maximum line length (76) of Base64 output. + */ + private final static int MAX_LINE_LENGTH = 76; + + /** + * The equals sign (=) as a byte. + */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** + * The new line character (\n) as a byte. + */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * Preferred encoding. + */ + private final static String PREFERRED_ENCODING = "UTF-8"; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET; + + private final static byte[] _NATIVE_ALPHABET = /* + * May be something funny + * like EBCDIC + */ + { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/' }; + + /** Determine which ALPHABET to use. */ + static { + byte[] __bytes; + try { + __bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException use) { + __bytes = _NATIVE_ALPHABET; // Fall back to native encoding + } // end catch + ALPHABET = __bytes; + } // end static + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a + * negative number indicating some other meaning. + */ + private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, + -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - + // 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' + // through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' + // through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' + // through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' + // through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + // I think I end up not using the BAD_ENCODING indicator. + // private final static byte BAD_ENCODING = -9; // Indicates error in + // encoding + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in + // encoding + + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in + // encoding + + /** + * Defeats instantiation. + */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array threeBytes and + * returns a four-byte array in Base64 notation. The actual number of + * significant bytes in your array is given by numSigBytes. The + * array threeBytes needs only be as big as numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 + * A reusable byte array to reduce array instantiation + * @param threeBytes + * the array to convert + * @param numSigBytes + * the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, + int numSigBytes) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0); + return b4; + } // end encode3to4 + + /** + * Encodes up to three bytes of the array source and writes the + * resulting four Base64 bytes to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate + * srcOffset + 3 for the source array or + * destOffset + 4 for the destination array. The + * actual number of significant bytes in your array is given by + * numSigBytes. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param numSigBytes + * the number of significant bytes in your array + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an + // int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return null. The object is not + * GZip-compressed before being encoded. + * + * @param serializableObject + * The object to encode + * @return The Base64-encoded object + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return null.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeObject( myObj, Base64.GZIP ) or

+ * Example: + * encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param serializableObject + * The object to encode + * @param options + * Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, + int options) { + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + java.util.zip.GZIPOutputStream gzos = null; + + // Isolate options + int gzip = (options & GZIP); + int dontBreakLines = (options & DONT_BREAK_LINES); + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + + // GZip? + if (gzip == GZIP) { + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } // end if: gzip + else { + oos = new java.io.ObjectOutputStream(b64os); + } + + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + return encodeBytes(source, 0, source.length, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeBytes( myData, Base64.GZIP ) or

+ * Example: + * encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source + * The data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + return encodeBytes(source, off, len, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeBytes( myData, Base64.GZIP ) or

+ * Example: + * encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, + int options) { + // Isolate options + int dontBreakLines = (options & DONT_BREAK_LINES); + int gzip = (options & GZIP); + + // Compress? + if (gzip == GZIP) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + // Convert option to boolean in way that code likes it. + boolean breakLines = dontBreakLines == 0; + + int len43 = len * 4 / 3; + byte[] outBuff = new byte[(len43) // Main 4:3 + + ((len % 3) > 0 ? 4 : 0) // Account for padding + + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New + // lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e); + + lineLength += 4; + if (breakLines && lineLength == MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e); + e += 4; + } // end if: some padding needed + + // Return value according to relevant encoding. + try { + return new String(outBuff, 0, e, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(outBuff, 0, e); + } // end catch + + } // end else: don't compress + + } // end encodeBytes + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source and writes the resulting + * bytes (up to three of them) to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate + * srcOffset + 4 for the source array or + * destOffset + 3 for the destination array. This + * method returns the actual number of bytes that were converted from the + * Base64 encoding. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + try { + // Two ways to do the same thing. Don't know which way I like + // best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) + // >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } catch (Exception e) { + System.out.println("" + source[srcOffset] + ": " + + (DECODABET[source[srcOffset]])); + System.out.println("" + source[srcOffset + 1] + ": " + + (DECODABET[source[srcOffset + 1]])); + System.out.println("" + source[srcOffset + 2] + ": " + + (DECODABET[source[srcOffset + 2]])); + System.out.println("" + source[srcOffset + 3] + ": " + + (DECODABET[source[srcOffset + 3]])); + return -1; + } // e nd catch + } + } // end decodeToBytes + + /** + * Very low-level access to decoding ASCII characters in the form of a byte + * array. Does not support automatically gunzipping or any other "fancy" + * features. + * + * @param source + * The Base64 encoded data + * @param off + * The offset of where to begin decoding + * @param len + * The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len) { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = off; i < off + len; i++) { + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or + // better + { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; + if (b4Posn > 3) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) { + break; + } + } // end if: quartet built + + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else { + System.err.println("Bad Base64 input character at " + i + ": " + + source[i] + "(decimal)"); + return null; + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting + * gzip-compressed data and decompressing it. + * + * @param s + * the string to decode + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) { + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if (bytes != null && bytes.length >= 4) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. + * Returns null if there was an error. + * + * @param encodedObject + * The Base64 data to decode + * @return The decoded and deserialized object + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) { + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + ois = new java.io.ObjectInputStream(bais); + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + obj = null; + } // end catch + catch (java.lang.ClassNotFoundException e) { + e.printStackTrace(); + obj = null; + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Convenience method for encoding data to a file. + * + * @param dataToEncode + * byte array of data to encode in base64 form + * @param filename + * Filename for saving encoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean encodeToFile(byte[] dataToEncode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + success = true; + } // end try + catch (java.io.IOException e) { + + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end encodeToFile + + /** + * Convenience method for decoding data to a file. + * + * @param dataToDecode + * Base64-encoded data as a string + * @param filename + * Filename for saving decoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean decodeToFile(String dataToDecode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + success = true; + } // end try + catch (java.io.IOException e) { + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end decodeToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it. + * + * @param filename + * Filename for reading encoded data + * @return decoded byte array or null if unsuccessful + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) { + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + System.err + .println("File is too big for this convenience method (" + + file.length() + " bytes)."); + return null; + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error decoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for reading a binary file and base64-encoding it. + * + * @param filename + * Filename for reading binary data + * @return base64-encoded string or null if unsuccessful + * @since 2.1 + */ + public static String encodeFromFile(String filename) { + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[(int) (file.length() * 1.4)]; + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + encodedData = new String(buffer, 0, length, + Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error encoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + private boolean encode; // Encoding or decoding + + private int position; // Current position in the buffer + + private byte[] buffer; // Small buffer holding converted data + + private int bufferLength; // Length of buffer (3 or 4) + + private int numSigBytes; // Number of meaningful bytes in the buffer + + private int lineLength; + + private boolean breakLines; // Break lines at less than 80 characters + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in + * the java.io.InputStream from which to read + * data. + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + /** + * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE + * mode.

Valid options: + * + *

+		 *    ENCODE or DECODE: Encode or Decode as data is read.
+		 *    DONT_BREAK_LINES: don't break lines at 76 characters
+		 *      (only meaningful when encoding)
+		 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+		 * 
+ * + *

Example: + * new Base64.InputStream( in, Base64.DECODE ) + * + * @param in + * the java.io.InputStream from which to read + * data. + * @param options + * Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + super(in); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + } // end constructor + + /** + * Reads enough of the input stream to convert to/from Base64 and + * returns the next byte. + * + * @return next byte + * @since 1.3 + */ + public int read() throws java.io.IOException { + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + try { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } // end if: not end of stream + + } // end try: read + catch (java.io.IOException e) { + // Only a problem if we got no data at all. + if (i == 0) { + throw e; + } + + } // end catch + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } while (b >= 0 + && DECODABET[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException( + "Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if (/* !encode && */position >= numSigBytes) { + return -1; + } + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + // When JDK1.4 is more accepted, use an assertion here. + throw new java.io.IOException( + "Error in Base64 code reading stream."); + } // end else + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream is reached + * or len bytes are read. Returns number of bytes read into + * array or -1 if end of stream is encountered. + * + * @param dest + * array to hold values + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + public int read(byte[] dest, int off, int len) + throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + // if( b < 0 && i == 0 ) + // return -1; + + if (b >= 0) { + dest[off + i] = (byte) b; + } else if (i == 0) { + return -1; + } else { + break; // Out of 'for' loop + } + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + private boolean encode; + + private int position; + + private byte[] buffer; + + private int bufferLength; + + private int lineLength; + + private boolean breakLines; + + private byte[] b4; // Scratch used in a few places + + private boolean suspendEncoding; + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out + * the java.io.OutputStream to which data will be + * written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + /** + * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE + * mode.

Valid options: + * + *

+		 *    ENCODE or DECODE: Encode or Decode as data is read.
+		 *    DONT_BREAK_LINES: don't break lines at 76 characters
+		 *      (only meaningful when encoding)
+		 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+		 * 
+ * + *

Example: + * new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out + * the java.io.OutputStream to which data will be + * written. + * @param options + * Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + } // end constructor + + /** + * Writes the byte to the output stream after converting to/from Base64 + * notation. When encoding, bytes are buffered three at a time before + * the output stream actually gets a write() call. When decoding, bytes + * are buffered four at a time. + * + * @param theByte + * the byte to write + * @since 1.3 + */ + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theByte); + return; + } // end if: supsended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to encode. + { + out.write(encode3to4(b4, buffer, bufferLength)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (DECODABET[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to output. + { + int len = Base64.decode4to3(buffer, 0, b4, 0); + out.write(b4, 0, len); + // out.write( Base64.decode4to3( buffer ) ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (DECODABET[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException( + "Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + /** + * Calls {@link #write(int)} repeatedly until len bytes are + * written. + * + * @param theBytes + * array from which to read bytes + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @since 1.3 + */ + public void write(byte[] theBytes, int off, int len) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer + * without closing the stream. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException( + "Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + /** + * Suspends encoding of the stream. May be helpful if you need to embed + * a piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + /** + * Resumes encoding of the stream. May be helpful if you need to embed a + * piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + } // end inner class OutputStream + +} // end class Base64 + diff --git a/src/java/org/jivesoftware/util/FastDateFormat.java b/src/java/org/jivesoftware/util/FastDateFormat.java new file mode 100644 index 0000000..8866ee0 --- /dev/null +++ b/src/java/org/jivesoftware/util/FastDateFormat.java @@ -0,0 +1,1191 @@ +/* ==================================================================== + * Trove - Copyright (c) 1997-2001 Walt Disney Internet Group + * ==================================================================== + * The Tea Software License, Version 1.1 + * + * Copyright (c) 2000 Walt Disney Internet Group. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Walt Disney Internet Group (http://opensource.go.com/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Tea", "TeaServlet", "Kettle", "Trove" and "BeanDoc" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact opensource@dig.com. + * + * 5. Products derived from this software may not be called "Tea", + * "TeaServlet", "Kettle" or "Trove", nor may "Tea", "TeaServlet", + * "Kettle", "Trove" or "BeanDoc" appear in their name, without prior + * written permission of the Walt Disney Internet Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE WALT DISNEY INTERNET GROUP OR ITS + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * For more information about Tea, please see http://opensource.go.com/. + */ + +package org.jivesoftware.util; + +import java.util.Date; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.text.DateFormatSymbols; +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +/** + *

Similar to {@link java.text.SimpleDateFormat}, but faster and thread-safe. + * Only formatting is supported, but all patterns are compatible with + * SimpleDateFormat.

+ * + *

Note, this class is from the open source Tea project (http://sourceforge.net/projects/teatrove/).

+ * + * @author Brian S O'Neill + */ +public class FastDateFormat { + /** Style pattern */ + public static final Object + FULL = new Integer(SimpleDateFormat.FULL), + LONG = new Integer(SimpleDateFormat.LONG), + MEDIUM = new Integer(SimpleDateFormat.MEDIUM), + SHORT = new Integer(SimpleDateFormat.SHORT); + + private static final double LOG_10 = Math.log(10); + + private static String cDefaultPattern; + private static TimeZone cDefaultTimeZone = TimeZone.getDefault(); + + private static Map cTimeZoneDisplayCache = new HashMap(); + + private static Map cInstanceCache = new HashMap(7); + private static Map cDateInstanceCache = new HashMap(7); + private static Map cTimeInstanceCache = new HashMap(7); + private static Map cDateTimeInstanceCache = new HashMap(7); + + public static FastDateFormat getInstance() { + return getInstance(getDefaultPattern(), null, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + */ + public static FastDateFormat getInstance(String pattern) + throws IllegalArgumentException + { + return getInstance(pattern, null, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + */ + public static FastDateFormat getInstance + (String pattern, TimeZone timeZone) throws IllegalArgumentException + { + return getInstance(pattern, timeZone, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param locale optional locale, overrides system locale + */ + public static FastDateFormat getInstance + (String pattern, Locale locale) throws IllegalArgumentException + { + return getInstance(pattern, null, locale, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param symbols optional date format symbols, overrides symbols for + * system locale + */ + public static FastDateFormat getInstance + (String pattern, DateFormatSymbols symbols) + throws IllegalArgumentException + { + return getInstance(pattern, null, null, symbols); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + */ + public static FastDateFormat getInstance + (String pattern, TimeZone timeZone, Locale locale) + throws IllegalArgumentException + { + return getInstance(pattern, timeZone, locale, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + * @param symbols optional date format symbols, overrides symbols for + * provided locale + */ + public static synchronized FastDateFormat getInstance + (String pattern, TimeZone timeZone, Locale locale, + DateFormatSymbols symbols) + throws IllegalArgumentException + { + Object key = pattern; + + if (timeZone != null) { + key = new Pair(key, timeZone); + } + if (locale != null) { + key = new Pair(key, locale); + } + if (symbols != null) { + key = new Pair(key, symbols); + } + + FastDateFormat format = (FastDateFormat)cInstanceCache.get(key); + if (format == null) { + if (locale == null) { + locale = Locale.getDefault(); + } + if (symbols == null) { + symbols = new DateFormatSymbols(locale); + } + format = new FastDateFormat(pattern, timeZone, locale, symbols); + cInstanceCache.put(key, format); + } + return format; + } + + /** + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + */ + public static synchronized FastDateFormat getDateInstance + (Object style, TimeZone timeZone, Locale locale) + throws IllegalArgumentException + { + Object key = style; + + if (timeZone != null) { + key = new Pair(key, timeZone); + } + if (locale == null) { + key = new Pair(key, locale); + } + + FastDateFormat format = (FastDateFormat)cDateInstanceCache.get(key); + + if (format == null) { + int ds; + try { + ds = ((Integer)style).intValue(); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("Illegal date style: " + style); + } + + if (locale == null) { + locale = Locale.getDefault(); + } + + try { + String pattern = ((SimpleDateFormat)DateFormat.getDateInstance(ds, locale)).toPattern(); + format = getInstance(pattern, timeZone, locale); + cDateInstanceCache.put(key, format); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("No date pattern for locale: " + locale); + } + } + + return format; + } + + /** + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + */ + public static synchronized FastDateFormat getTimeInstance + (Object style, TimeZone timeZone, Locale locale) + throws IllegalArgumentException + { + Object key = style; + + if (timeZone != null) { + key = new Pair(key, timeZone); + } + if (locale != null) { + key = new Pair(key, locale); + } + + FastDateFormat format = (FastDateFormat)cTimeInstanceCache.get(key); + + if (format == null) { + int ts; + try { + ts = ((Integer)style).intValue(); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("Illegal time style: " + style); + } + + if (locale == null) { + locale = Locale.getDefault(); + } + + try { + String pattern = ((SimpleDateFormat)DateFormat.getTimeInstance(ts, locale)).toPattern(); + format = getInstance(pattern, timeZone, locale); + cTimeInstanceCache.put(key, format); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("No date pattern for locale: " + locale); + } + } + + return format; + } + + /** + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + */ + public static synchronized FastDateFormat getDateTimeInstance + (Object dateStyle, Object timeStyle, TimeZone timeZone, Locale locale) + throws IllegalArgumentException + { + Object key = new Pair(dateStyle, timeStyle); + + if (timeZone != null) { + key = new Pair(key, timeZone); + } + if (locale != null) { + key = new Pair(key, locale); + } + + FastDateFormat format = + (FastDateFormat)cDateTimeInstanceCache.get(key); + + if (format == null) { + int ds; + try { + ds = ((Integer)dateStyle).intValue(); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("Illegal date style: " + dateStyle); + } + + int ts; + try { + ts = ((Integer)timeStyle).intValue(); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("Illegal time style: " + timeStyle); + } + + if (locale == null) { + locale = Locale.getDefault(); + } + + try { + String pattern = ((SimpleDateFormat)DateFormat.getDateTimeInstance(ds, ts, locale)).toPattern(); + format = getInstance(pattern, timeZone, locale); + cDateTimeInstanceCache.put(key, format); + } + catch (ClassCastException e) { + throw new IllegalArgumentException + ("No date time pattern for locale: " + locale); + } + } + + return format; + } + + static synchronized String getTimeZoneDisplay(TimeZone tz, + boolean daylight, + int style, + Locale locale) { + Object key = new TimeZoneDisplayKey(tz, daylight, style, locale); + String value = (String)cTimeZoneDisplayCache.get(key); + if (value == null) { + // This is a very slow call, so cache the results. + value = tz.getDisplayName(daylight, style, locale); + cTimeZoneDisplayCache.put(key, value); + } + return value; + } + + private static synchronized String getDefaultPattern() { + if (cDefaultPattern == null) { + cDefaultPattern = new SimpleDateFormat().toPattern(); + } + return cDefaultPattern; + } + + /** + * Returns a list of Rules. + */ + private static List parse(String pattern, TimeZone timeZone, Locale locale, + DateFormatSymbols symbols) { + List rules = new ArrayList(); + + String[] ERAs = symbols.getEras(); + String[] months = symbols.getMonths(); + String[] shortMonths = symbols.getShortMonths(); + String[] weekdays = symbols.getWeekdays(); + String[] shortWeekdays = symbols.getShortWeekdays(); + String[] AmPmStrings = symbols.getAmPmStrings(); + + int length = pattern.length(); + int[] indexRef = new int[1]; + + for (int i=0; i= 4) { + rule = new UnpaddedNumberField(Calendar.YEAR); + } + else { + rule = new TwoDigitYearField(); + } + break; + case 'M': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, months); + } + else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, shortMonths); + } + else if (tokenLen == 2) { + rule = new TwoDigitMonthField(); + } + else { + rule = new UnpaddedMonthField(); + } + break; + case 'd': // day in month (number) + rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); + break; + case 'h': // hour in am/pm (number, 1..12) + rule = new TwelveHourField + (selectNumberRule(Calendar.HOUR, tokenLen)); + break; + case 'H': // hour in day (number, 0..23) + rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); + break; + case 'm': // minute in hour (number) + rule = selectNumberRule(Calendar.MINUTE, tokenLen); + break; + case 's': // second in minute (number) + rule = selectNumberRule(Calendar.SECOND, tokenLen); + break; + case 'S': // millisecond (number) + rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); + break; + case 'E': // day in week (text) + rule = new TextField + (Calendar.DAY_OF_WEEK, + tokenLen < 4 ? shortWeekdays : weekdays); + break; + case 'D': // day in year (number) + rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); + break; + case 'F': // day of week in month (number) + rule = selectNumberRule + (Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); + break; + case 'w': // week in year (number) + rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); + break; + case 'W': // week in month (number) + rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); + break; + case 'a': // am/pm marker (text) + rule = new TextField(Calendar.AM_PM, AmPmStrings); + break; + case 'k': // hour in day (1..24) + rule = new TwentyFourHourField + (selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); + break; + case 'K': // hour in am/pm (0..11) + rule = selectNumberRule(Calendar.HOUR, tokenLen); + break; + case 'z': // time zone (text) + if (tokenLen >= 4) { + rule = new TimeZoneRule(timeZone, locale, TimeZone.LONG); + } + else { + rule = new TimeZoneRule(timeZone, locale, TimeZone.SHORT); + } + break; + case '\'': // literal text + String sub = token.substring(1); + if (sub.length() == 1) { + rule = new CharacterLiteral(sub.charAt(0)); + } + else { + rule = new StringLiteral(new String(sub)); + } + break; + default: + throw new IllegalArgumentException + ("Illegal pattern component: " + token); + } + + rules.add(rule); + } + + return rules; + } + + private static String parseToken(String pattern, int[] indexRef) { + StringBuffer buf = new StringBuffer(); + + int i = indexRef[0]; + int length = pattern.length(); + + char c = pattern.charAt(i); + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + // Scan a run of the same character, which indicates a time + // pattern. + buf.append(c); + + while (i + 1 < length) { + char peek = pattern.charAt(i + 1); + if (peek == c) { + buf.append(c); + i++; + } + else { + break; + } + } + } + else { + // This will identify token as text. + buf.append('\''); + + boolean inLiteral = false; + + for (; i < length; i++) { + c = pattern.charAt(i); + + if (c == '\'') { + if (i + 1 < length && pattern.charAt(i + 1) == '\'') { + // '' is treated as escaped ' + i++; + buf.append(c); + } + else { + inLiteral = !inLiteral; + } + } + else if (!inLiteral && + (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { + i--; + break; + } + else { + buf.append(c); + } + } + } + + indexRef[0] = i; + return buf.toString(); + } + + private static NumberRule selectNumberRule(int field, int padding) { + switch (padding) { + case 1: + return new UnpaddedNumberField(field); + case 2: + return new TwoDigitNumberField(field); + default: + return new PaddedNumberField(field, padding); + } + } + + private final String mPattern; + private final TimeZone mTimeZone; + private final Locale mLocale; + private final Rule[] mRules; + private final int mMaxLengthEstimate; + + private FastDateFormat() { + this(getDefaultPattern(), null, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + */ + private FastDateFormat(String pattern) throws IllegalArgumentException { + this(pattern, null, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + */ + private FastDateFormat(String pattern, TimeZone timeZone) + throws IllegalArgumentException + { + this(pattern, timeZone, null, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param locale optional locale, overrides system locale + */ + private FastDateFormat(String pattern, Locale locale) + throws IllegalArgumentException + { + this(pattern, null, locale, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param symbols optional date format symbols, overrides symbols for + * system locale + */ + private FastDateFormat(String pattern, DateFormatSymbols symbols) + throws IllegalArgumentException + { + this(pattern, null, null, symbols); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + */ + private FastDateFormat(String pattern, TimeZone timeZone, Locale locale) + throws IllegalArgumentException + { + this(pattern, timeZone, locale, null); + } + + /** + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted + * date + * @param locale optional locale, overrides system locale + * @param symbols optional date format symbols, overrides symbols for + * provided locale + */ + private FastDateFormat(String pattern, TimeZone timeZone, Locale locale, + DateFormatSymbols symbols) + throws IllegalArgumentException + { + if (locale == null) { + locale = Locale.getDefault(); + } + + mPattern = pattern; + mTimeZone = timeZone; + mLocale = locale; + + if (symbols == null) { + symbols = new DateFormatSymbols(locale); + } + + List rulesList = parse(pattern, timeZone, locale, symbols); + mRules = (Rule[])rulesList.toArray(new Rule[rulesList.size()]); + + int len = 0; + for (int i=mRules.length; --i >= 0; ) { + len += mRules[i].estimateLength(); + } + + mMaxLengthEstimate = len; + } + + public String format(Date date) { + Calendar c = new GregorianCalendar(cDefaultTimeZone); + c.setTime(date); + if (mTimeZone != null) { + c.setTimeZone(mTimeZone); + } + return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString(); + } + + public String format(Calendar calendar) { + return format(calendar, new StringBuffer(mMaxLengthEstimate)) + .toString(); + } + + public StringBuffer format(Date date, StringBuffer buf) { + Calendar c = new GregorianCalendar(cDefaultTimeZone); + c.setTime(date); + if (mTimeZone != null) { + c.setTimeZone(mTimeZone); + } + return applyRules(c, buf); + } + + public StringBuffer format(Calendar calendar, StringBuffer buf) { + if (mTimeZone != null) { + calendar = (Calendar)calendar.clone(); + calendar.setTimeZone(mTimeZone); + } + return applyRules(calendar, buf); + } + + private StringBuffer applyRules(Calendar calendar, StringBuffer buf) { + Rule[] rules = mRules; + int len = mRules.length; + for (int i=0; i= 0; ) { + int len = mValues[i].length(); + if (len > max) { + max = len; + } + } + return max; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + buffer.append(mValues[calendar.get(mField)]); + } + } + + private static class UnpaddedNumberField implements NumberRule { + private final int mField; + + UnpaddedNumberField(int field) { + mField = field; + } + + public int estimateLength() { + return 4; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + public final void appendTo(StringBuffer buffer, int value) { + if (value < 10) { + buffer.append((char)(value + '0')); + } + else if (value < 100) { + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + else { + buffer.append(Integer.toString(value)); + } + } + } + + private static class UnpaddedMonthField implements NumberRule { + UnpaddedMonthField() { + } + + public int estimateLength() { + return 2; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + public final void appendTo(StringBuffer buffer, int value) { + if (value < 10) { + buffer.append((char)(value + '0')); + } + else { + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + } + } + + private static class PaddedNumberField implements NumberRule { + private final int mField; + private final int mSize; + + PaddedNumberField(int field, int size) { + if (size < 3) { + // Should use UnpaddedNumberField or TwoDigitNumberField. + throw new IllegalArgumentException(); + } + mField = field; + mSize = size; + } + + public int estimateLength() { + return 4; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + public final void appendTo(StringBuffer buffer, int value) { + if (value < 100) { + for (int i = mSize; --i >= 2; ) { + buffer.append('0'); + } + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + else { + int digits; + if (value < 1000) { + digits = 3; + } + else { + digits = (int)(Math.log(value) / LOG_10) + 1; + } + for (int i = mSize; --i >= digits; ) { + buffer.append('0'); + } + buffer.append(Integer.toString(value)); + } + } + } + + private static class TwoDigitNumberField implements NumberRule { + private final int mField; + + TwoDigitNumberField(int field) { + mField = field; + } + + public int estimateLength() { + return 2; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + public final void appendTo(StringBuffer buffer, int value) { + if (value < 100) { + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + else { + buffer.append(Integer.toString(value)); + } + } + } + + private static class TwoDigitYearField implements NumberRule { + TwoDigitYearField() { + } + + public int estimateLength() { + return 2; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + public final void appendTo(StringBuffer buffer, int value) { + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + } + + private static class TwoDigitMonthField implements NumberRule { + TwoDigitMonthField() { + } + + public int estimateLength() { + return 2; + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + public final void appendTo(StringBuffer buffer, int value) { + buffer.append((char)(value / 10 + '0')); + buffer.append((char)(value % 10 + '0')); + } + } + + private static class TwelveHourField implements NumberRule { + private final NumberRule mRule; + + TwelveHourField(NumberRule rule) { + mRule = rule; + } + + public int estimateLength() { + return mRule.estimateLength(); + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + int value = calendar.get(Calendar.HOUR); + if (value == 0) { + value = calendar.getLeastMaximum(Calendar.HOUR) + 1; + } + mRule.appendTo(buffer, value); + } + + public void appendTo(StringBuffer buffer, int value) { + mRule.appendTo(buffer, value); + } + } + + private static class TwentyFourHourField implements NumberRule { + private final NumberRule mRule; + + TwentyFourHourField(NumberRule rule) { + mRule = rule; + } + + public int estimateLength() { + return mRule.estimateLength(); + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + int value = calendar.get(Calendar.HOUR_OF_DAY); + if (value == 0) { + value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; + } + mRule.appendTo(buffer, value); + } + + public void appendTo(StringBuffer buffer, int value) { + mRule.appendTo(buffer, value); + } + } + + private static class TimeZoneRule implements Rule { + private final TimeZone mTimeZone; + private final Locale mLocale; + private final int mStyle; + private final String mStandard; + private final String mDaylight; + + TimeZoneRule(TimeZone timeZone, Locale locale, int style) { + mTimeZone = timeZone; + mLocale = locale; + mStyle = style; + + if (timeZone != null) { + mStandard = getTimeZoneDisplay(timeZone, false, style, locale); + mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); + } + else { + mStandard = null; + mDaylight = null; + } + } + + public int estimateLength() { + if (mTimeZone != null) { + return Math.max(mStandard.length(), mDaylight.length()); + } + else if (mStyle == TimeZone.SHORT) { + return 4; + } + else { + return 40; + } + } + + public void appendTo(StringBuffer buffer, Calendar calendar) { + TimeZone timeZone; + if ((timeZone = mTimeZone) != null) { + if (timeZone.useDaylightTime() && + calendar.get(Calendar.DST_OFFSET) != 0) { + + buffer.append(mDaylight); + } + else { + buffer.append(mStandard); + } + } + else { + timeZone = calendar.getTimeZone(); + if (timeZone.useDaylightTime() && + calendar.get(Calendar.DST_OFFSET) != 0) { + + buffer.append(getTimeZoneDisplay + (timeZone, true, mStyle, mLocale)); + } + else { + buffer.append(getTimeZoneDisplay + (timeZone, false, mStyle, mLocale)); + } + } + } + } + + private static class TimeZoneDisplayKey { + private final TimeZone mTimeZone; + private final int mStyle; + private final Locale mLocale; + + TimeZoneDisplayKey(TimeZone timeZone, + boolean daylight, int style, Locale locale) { + mTimeZone = timeZone; + if (daylight) { + style |= 0x80000000; + } + mStyle = style; + mLocale = locale; + } + + public int hashCode() { + return mStyle * 31 + mLocale.hashCode(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TimeZoneDisplayKey) { + TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj; + return + mTimeZone.equals(other.mTimeZone) && + mStyle == other.mStyle && + mLocale.equals(other.mLocale); + } + return false; + } + } + + private static class Pair implements Comparable, java.io.Serializable { + private final Object mObj1; + private final Object mObj2; + + public Pair(Object obj1, Object obj2) { + mObj1 = obj1; + mObj2 = obj2; + } + + public int compareTo(Object obj) { + if (this == obj) { + return 0; + } + + Pair other = (Pair)obj; + + Object a = mObj1; + Object b = other.mObj1; + + firstTest: { + if (a == null) { + if (b != null) { + return 1; + } + // Both a and b are null. + break firstTest; + } + else { + if (b == null) { + return -1; + } + } + + int result = ((Comparable)a).compareTo(b); + + if (result != 0) { + return result; + } + } + + a = mObj2; + b = other.mObj2; + + if (a == null) { + if (b != null) { + return 1; + } + // Both a and b are null. + return 0; + } + else { + if (b == null) { + return -1; + } + } + + return ((Comparable)a).compareTo(b); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Pair)) { + return false; + } + + Pair key = (Pair)obj; + + return + (mObj1 == null ? + key.mObj1 == null : mObj1.equals(key.mObj1)) && + (mObj2 == null ? + key.mObj2 == null : mObj2.equals(key.mObj2)); + } + + public int hashCode() { + return + (mObj1 == null ? 0 : mObj1.hashCode()) + + (mObj2 == null ? 0 : mObj2.hashCode()); + } + + public String toString() { + return "[" + mObj1 + ':' + mObj2 + ']'; + } + } +} diff --git a/src/java/org/jivesoftware/util/JiveConstants.java b/src/java/org/jivesoftware/util/JiveConstants.java new file mode 100644 index 0000000..6e8bd18 --- /dev/null +++ b/src/java/org/jivesoftware/util/JiveConstants.java @@ -0,0 +1,36 @@ +/** + * $RCSfile$ + * $Revision: 1715 $ + * $Date: 2005-07-26 21:05:38 -0300 (Tue, 26 Jul 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +/** + * Contains constant values representing various objects in Jive. + */ +public class JiveConstants { + + public static final int SYSTEM = 17; + public static final int ROSTER = 18; + public static final int OFFLINE = 19; + public static final int MUC_ROOM = 23; + + public static final long SECOND = 1000; + public static final long MINUTE = 60 * SECOND; + public static final long HOUR = 60 * MINUTE; + public static final long DAY = 24 * HOUR; + public static final long WEEK = 7 * DAY; + + /** + * Date/time format for use by SimpleDateFormat. The format conforms to + * JEP-0082, which defines + * a unified date/time format for XMPP. + */ + public static final String XMPP_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/JiveGlobals.java b/src/java/org/jivesoftware/util/JiveGlobals.java new file mode 100644 index 0000000..527b5ad --- /dev/null +++ b/src/java/org/jivesoftware/util/JiveGlobals.java @@ -0,0 +1,468 @@ +/** + * $RCSfile$ + * $Revision: 3505 $ + * $Date: 2006-03-01 20:00:53 -0300 (Wed, 01 Mar 2006) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.text.DateFormat; + +/** + * Controls Jive properties. Jive properties are only meant to be set and retrieved + * by core Jive classes. Properties are stored in XML format.

+ * + * When starting up the application this class needs to be configured so that the initial + * configuration of the application may be loaded from the configuration file. The configuration + * file holds properties stored in XML format. Use {@link #setHomeDirectory(String)} and + * {@link #setConfigName(String)} for setting the home directory and path to the configuration file.

+ * + * XML property names must be in the form prop.name - parts of the name must + * be seperated by ".". The value can be any valid String, including strings with line breaks.

+ * + * This class was copied from Wildfire. Properties stored in the DB were removed from the code. + * Modified #getBooleanProperty to use properties stored in XML. + */ +public class JiveGlobals { + + private static String JIVE_CONFIG_FILENAME = "conf" + File.separator + "manager.xml"; + + /** + * Location of the jiveHome directory. All configuration files should be + * located here. + */ + private static String home = null; + + public static boolean failedLoading = false; + + private static XMLProperties xmlProperties = null; + + private static Locale locale = null; + private static TimeZone timeZone = null; + private static DateFormat dateTimeFormat = null; + + /** + * Returns the global Locale used by Jive. A locale specifies language + * and country codes, and is used for internationalization. The default + * locale is system dependant - Locale.getDefault(). + * + * @return the global locale used by Jive. + */ + public static Locale getLocale() { + if (locale == null) { + if (xmlProperties != null) { + String [] localeArray; + String localeProperty = xmlProperties.getProperty("locale"); + if (localeProperty != null) { + localeArray = localeProperty.split("_"); + } + else { + localeArray = new String[] {"", ""}; + } + + String language = localeArray[0]; + if (language == null) { + language = ""; + } + String country = ""; + if (localeArray.length == 2) { + country = localeArray[1]; + } + // If no locale info is specified, return the system default Locale. + if (language.equals("") && country.equals("")) { + locale = Locale.getDefault(); + } + else { + locale = new Locale(language, country); + } + } + else { + return Locale.getDefault(); + } + } + return locale; + } + + /** + * Sets the global locale used by Jive. A locale specifies language + * and country codes, and is used for formatting dates and numbers. + * The default locale is Locale.US. + * + * @param newLocale the global Locale for Jive. + */ + public static void setLocale(Locale newLocale) { + locale = newLocale; + // Save values to Jive properties. + setXMLProperty("locale", locale.toString()); + } + + /** + * Returns the global TimeZone used by Jive. The default is the VM's + * default time zone. + * + * @return the global time zone used by Jive. + */ + public static TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + return timeZone; + } + + /** + * Formats a Date object to return a date and time using the global locale. + * + * @param date the Date to format. + * @return a String representing the date and time. + */ + public static String formatDateTime(Date date) { + if (dateTimeFormat == null) { + dateTimeFormat = DateFormat + .getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, getLocale()); + dateTimeFormat.setTimeZone(getTimeZone()); + } + return dateTimeFormat.format(date); + } + + /** + * Returns the location of the home directory. + * + * @return the location of the home dir. + */ + public static String getHomeDirectory() { + if (xmlProperties == null) { + loadSetupProperties(); + } + return home; + } + + /** + * Sets the location of the home directory. The directory must exist and the + * user running the application must have read and write permissions over the specified + * directory. + * + * @param pathname the location of the home dir. + */ + public static void setHomeDirectory(String pathname) { + File mh = new File(pathname); + // Do a permission check on the new home directory + if (!mh.exists()) { + Log.error("Error - the specified home directory does not exist (" + pathname + ")"); + } + else if (!mh.canRead() || !mh.canWrite()) { + Log.error("Error - the user running this application can not read " + + "and write to the specified home directory (" + pathname + "). " + + "Please grant the executing user read and write permissions."); + } + else { + home = pathname; + } + } + + /** + * Returns a local property. Local properties are stored in the file defined in + * JIVE_CONFIG_FILENAME that exists in the home directory. + * Properties are always specified as "foo.bar.prop", which would map to + * the following entry in the XML file: + *

+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * @param name the name of the property to return. + * @return the property value specified by name. + */ + public static String getXMLProperty(String name) { + if (xmlProperties == null) { + loadSetupProperties(); + } + + // home not loaded? + if (xmlProperties == null) { + return null; + } + + return xmlProperties.getProperty(name); + } + + /** + * Returns a local property. Local properties are stored in the file defined in + * JIVE_CONFIG_FILENAME that exists in the home directory. + * Properties are always specified as "foo.bar.prop", which would map to + * the following entry in the XML file: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * If the specified property can't be found, the defaultValue will be returned. + * + * @param name the name of the property to return. + * @param defaultValue the default value for the property. + * @return the property value specified by name. + */ + public static String getXMLProperty(String name, String defaultValue) { + if (xmlProperties == null) { + loadSetupProperties(); + } + + // home not loaded? + if (xmlProperties == null) { + return null; + } + + String value = xmlProperties.getProperty(name); + if (value == null) { + value = defaultValue; + } + return value; + } + + /** + * Returns an integer value local property. Local properties are stored in the file defined in + * JIVE_CONFIG_FILENAME that exists in the home directory. + * Properties are always specified as "foo.bar.prop", which would map to + * the following entry in the XML file: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * If the specified property can't be found, or if the value is not a number, the + * defaultValue will be returned. + * + * @param name the name of the property to return. + * @param defaultValue value returned if the property could not be loaded or was not + * a number. + * @return the property value specified by name or defaultValue. + */ + public static int getXMLProperty(String name, int defaultValue) { + String value = getXMLProperty(name); + if (value != null) { + try { + return Integer.parseInt(value); + } + catch (NumberFormatException nfe) { + // Ignore. + } + } + return defaultValue; + } + + /** + * Sets a local property. If the property doesn't already exists, a new + * one will be created. Local properties are stored in the file defined in + * JIVE_CONFIG_FILENAME that exists in the home directory. + * Properties are always specified as "foo.bar.prop", which would map to + * the following entry in the XML file: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * @param name the name of the property being set. + * @param value the value of the property being set. + */ + public static void setXMLProperty(String name, String value) { + if (xmlProperties == null) { + loadSetupProperties(); + } + + // jiveHome not loaded? + if (xmlProperties != null) { + xmlProperties.setProperty(name, value); + } + } + + /** + * Sets multiple local properties at once. If a property doesn't already exists, a new + * one will be created. Local properties are stored in the file defined in + * JIVE_CONFIG_FILENAME that exists in the home directory. + * Properties are always specified as "foo.bar.prop", which would map to + * the following entry in the XML file: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * @param propertyMap a map of properties, keyed on property name. + */ + public static void setXMLProperties(Map propertyMap) { + if (xmlProperties == null) { + loadSetupProperties(); + } + + if (xmlProperties != null) { + xmlProperties.setProperties(propertyMap); + } + } + + /** + * Return all immediate children property values of a parent local property as a list of strings, + * or an empty list if there are no children. For example, given + * the properties X.Y.A, X.Y.B, X.Y.C and X.Y.C.D, then + * the immediate child properties of X.Y are A, B, and + * C (the value of C.D would not be returned using this method).

+ * + * Local properties are stored in the file defined in JIVE_CONFIG_FILENAME that exists + * in the home directory. Properties are always specified as "foo.bar.prop", + * which would map to the following entry in the XML file: + *

+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * + * @param parent the name of the parent property to return the children for. + * @return all child property values for the given parent. + */ + public static List getXMLProperties(String parent) { + if (xmlProperties == null) { + loadSetupProperties(); + } + + // jiveHome not loaded? + if (xmlProperties == null) { + return Collections.EMPTY_LIST; + } + + String[] propNames = xmlProperties.getChildrenProperties(parent); + List values = new ArrayList(); + for (String propName : propNames) { + String value = getXMLProperty(parent + "." + propName); + if (value != null) { + values.add(value); + } + } + + return values; + } + + /** + * Returns an integer value Jive property. If the specified property doesn't exist, the + * defaultValue will be returned. + * + * @param name the name of the property to return. + * @param defaultValue value returned if the property doesn't exist or was not + * a number. + * @return the property value specified by name or defaultValue. + */ + public static int getIntProperty(String name, int defaultValue) { + String value = getXMLProperty(name); + if (value != null) { + try { + return Integer.parseInt(value); + } + catch (NumberFormatException nfe) { + // Ignore. + } + } + return defaultValue; + } + + /** + * Returns a boolean value Jive property. If the property doesn't exist, the defaultValue + * will be returned. + * + * If the specified property can't be found, or if the value is not a number, the + * defaultValue will be returned. + * + * @param name the name of the property to return. + * @param defaultValue value returned if the property doesn't exist. + * @return true if the property value exists and is set to "true" (ignoring case). + * Otherwise false is returned. + */ + public static boolean getBooleanProperty(String name, boolean defaultValue) { + String value = getXMLProperty(name); + if (value != null) { + return Boolean.valueOf(value); + } + else { + return defaultValue; + } + } + /** + * Deletes a locale property. If the property doesn't exist, the method + * does nothing. + * + * @param name the name of the property to delete. + */ + public static void deleteXMLProperty(String name) { + if (xmlProperties == null) { + loadSetupProperties(); + } + xmlProperties.deleteProperty(name); + } + + /** + * Allows the name of the local config file name to be changed. The + * default is "manager.xml". + * + * @param configName the name of the config file. + */ + public static void setConfigName(String configName) { + JIVE_CONFIG_FILENAME = configName; + } + + /** + * Returns the name of the local config file name. + * + * @return the name of the config file. + */ + static String getConfigName() { + return JIVE_CONFIG_FILENAME; + } + + /** + * Loads properties if necessary. Property loading must be done lazily so + * that we give outside classes a chance to set home. + */ + private synchronized static void loadSetupProperties() { + if (xmlProperties == null) { + // If home is null then log that the application will not work correctly + if (home == null && !failedLoading) { + failedLoading = true; + StringBuilder msg = new StringBuilder(); + msg.append("Critical Error! The home directory has not been configured, \n"); + msg.append("which will prevent the application from working correctly.\n\n"); + System.err.println(msg.toString()); + } + // Create a manager with the full path to the xml config file. + else { + try { + xmlProperties = new XMLProperties(home + File.separator + getConfigName()); + } + catch (IOException ioe) { + Log.error(ioe); + failedLoading = true; + } + } + } + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/LocaleUtils.java b/src/java/org/jivesoftware/util/LocaleUtils.java new file mode 100644 index 0000000..663072e --- /dev/null +++ b/src/java/org/jivesoftware/util/LocaleUtils.java @@ -0,0 +1,493 @@ +/** + * $RCSfile$ + * $Revision: 3195 $ + * $Date: 2005-12-13 15:07:30 -0300 (Tue, 13 Dec 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import java.text.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A set of methods for retrieving and converting locale specific strings and numbers. + * + * @author Jive Software + */ +public class LocaleUtils { + + private static final Map timeZoneLists = + new ConcurrentHashMap(); + + // The basename to use for looking up the appropriate resource bundles + // TODO - extract this out into a test that grabs the resource name from JiveGlobals + // and defaults to cmanager_i18n if nothing set. + private static final String resourceBaseName = "cmanager_i18n"; + + private LocaleUtils() { + } + + /** + * Converts a locale string like "en", "en_US" or "en_US_win" to a Java + * locale object. If the conversion fails, null is returned. + * + * @param localeCode the locale code for a Java locale. See the {@link java.util.Locale} + * class for more details. + */ + public static Locale localeCodeToLocale(String localeCode) { + Locale locale = null; + if (localeCode != null) { + String language = null; + String country = null; + String variant = null; + StringTokenizer tokenizer = new StringTokenizer(localeCode, "_"); + if (tokenizer.hasMoreTokens()) { + language = tokenizer.nextToken(); + if (tokenizer.hasMoreTokens()) { + country = tokenizer.nextToken(); + if (tokenizer.hasMoreTokens()) { + variant = tokenizer.nextToken(); + } + } + } + locale = new Locale(language, + ((country != null) ? country : ""), + ((variant != null) ? variant : "")); + } + return locale; + } + + // The list of supported timezone ids. The list tries to include all of the relevant + // time zones for the world without any extraneous zones. + private static String[] timeZoneIds = new String[]{"GMT", + "Pacific/Apia", + "HST", + "AST", + "America/Los_Angeles", + "America/Phoenix", + "America/Mazatlan", + "America/Denver", + "America/Belize", + "America/Chicago", + "America/Mexico_City", + "America/Regina", + "America/Bogota", + "America/New_York", + "America/Indianapolis", + "America/Halifax", + "America/Caracas", + "America/Santiago", + "America/St_Johns", + "America/Sao_Paulo", + "America/Buenos_Aires", + "America/Godthab", + "Atlantic/South_Georgia", + "Atlantic/Azores", + "Atlantic/Cape_Verde", + "Africa/Casablanca", + "Europe/Dublin", + "Europe/Berlin", + "Europe/Belgrade", + "Europe/Paris", + "Europe/Warsaw", + "ECT", + "Europe/Athens", + "Europe/Bucharest", + "Africa/Cairo", + "Africa/Harare", + "Europe/Helsinki", + "Asia/Jerusalem", + "Asia/Baghdad", + "Asia/Kuwait", + "Europe/Moscow", + "Africa/Nairobi", + "Asia/Tehran", + "Asia/Muscat", + "Asia/Baku", + "Asia/Kabul", + "Asia/Yekaterinburg", + "Asia/Karachi", + "Asia/Calcutta", + "Asia/Katmandu", + "Asia/Almaty", + "Asia/Dhaka", + "Asia/Colombo", + "Asia/Rangoon", + "Asia/Bangkok", + "Asia/Krasnoyarsk", + "Asia/Hong_Kong", + "Asia/Irkutsk", + "Asia/Kuala_Lumpur", + "Australia/Perth", + "Asia/Taipei", + "Asia/Tokyo", + "Asia/Seoul", + "Asia/Yakutsk", + "Australia/Adelaide", + "Australia/Darwin", + "Australia/Brisbane", + "Australia/Sydney", + "Pacific/Guam", + "Australia/Hobart", + "Asia/Vladivostok", + "Pacific/Noumea", + "Pacific/Auckland", + "Pacific/Fiji", + "Pacific/Tongatapu" + }; + + // A mapping from the supported timezone ids to friendly english names. + private static final Map nameMap = new HashMap(); + + static { + nameMap.put(timeZoneIds[0], "International Date Line West"); + nameMap.put(timeZoneIds[1], "Midway Island, Samoa"); + nameMap.put(timeZoneIds[2], "Hawaii"); + nameMap.put(timeZoneIds[3], "Alaska"); + nameMap.put(timeZoneIds[4], "Pacific Time (US & Canada); Tijuana"); + nameMap.put(timeZoneIds[5], "Arizona"); + nameMap.put(timeZoneIds[6], "Chihuahua, La Pax, Mazatlan"); + nameMap.put(timeZoneIds[7], "Mountain Time (US & Canada)"); + nameMap.put(timeZoneIds[8], "Central America"); + nameMap.put(timeZoneIds[9], "Central Time (US & Canada)"); + nameMap.put(timeZoneIds[10], "Guadalajara, Mexico City, Monterrey"); + nameMap.put(timeZoneIds[11], "Saskatchewan"); + nameMap.put(timeZoneIds[12], "Bogota, Lima, Quito"); + nameMap.put(timeZoneIds[13], "Eastern Time (US & Canada)"); + nameMap.put(timeZoneIds[14], "Indiana (East)"); + nameMap.put(timeZoneIds[15], "Atlantic Time (Canada)"); + nameMap.put(timeZoneIds[16], "Caracas, La Paz"); + nameMap.put(timeZoneIds[17], "Santiago"); + nameMap.put(timeZoneIds[18], "Newfoundland"); + nameMap.put(timeZoneIds[19], "Brasilia"); + nameMap.put(timeZoneIds[20], "Buenos Aires, Georgetown"); + nameMap.put(timeZoneIds[21], "Greenland"); + nameMap.put(timeZoneIds[22], "Mid-Atlantic"); + nameMap.put(timeZoneIds[23], "Azores"); + nameMap.put(timeZoneIds[24], "Cape Verde Is."); + nameMap.put(timeZoneIds[25], "Casablanca, Monrovia"); + nameMap.put(timeZoneIds[26], "Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"); + nameMap.put(timeZoneIds[27], "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); + nameMap.put(timeZoneIds[28], "Belgrade, Bratislava, Budapest, Ljubljana, Prague"); + nameMap.put(timeZoneIds[29], "Brussels, Copenhagen, Madrid, Paris"); + nameMap.put(timeZoneIds[30], "Sarajevo, Skopje, Warsaw, Zagreb"); + nameMap.put(timeZoneIds[31], "West Central Africa"); + nameMap.put(timeZoneIds[32], "Athens, Istanbul, Minsk"); + nameMap.put(timeZoneIds[33], "Bucharest"); + nameMap.put(timeZoneIds[34], "Cairo"); + nameMap.put(timeZoneIds[35], "Harare, Pretoria"); + nameMap.put(timeZoneIds[36], "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"); + nameMap.put(timeZoneIds[37], "Jerusalem"); + nameMap.put(timeZoneIds[38], "Baghdad"); + nameMap.put(timeZoneIds[39], "Kuwait, Riyadh"); + nameMap.put(timeZoneIds[40], "Moscow, St. Petersburg, Volgograd"); + nameMap.put(timeZoneIds[41], "Nairobi"); + nameMap.put(timeZoneIds[42], "Tehran"); + nameMap.put(timeZoneIds[43], "Abu Dhabi, Muscat"); + nameMap.put(timeZoneIds[44], "Baku, Tbilisi, Muscat"); + nameMap.put(timeZoneIds[45], "Kabul"); + nameMap.put(timeZoneIds[46], "Ekaterinburg"); + nameMap.put(timeZoneIds[47], "Islamabad, Karachi, Tashkent"); + nameMap.put(timeZoneIds[48], "Chennai, Kolkata, Mumbai, New Dehli"); + nameMap.put(timeZoneIds[49], "Kathmandu"); + nameMap.put(timeZoneIds[50], "Almaty, Novosibirsk"); + nameMap.put(timeZoneIds[51], "Astana, Dhaka"); + nameMap.put(timeZoneIds[52], "Sri Jayawardenepura"); + nameMap.put(timeZoneIds[53], "Rangoon"); + nameMap.put(timeZoneIds[54], "Bangkok, Hanoi, Jakarta"); + nameMap.put(timeZoneIds[55], "Krasnoyarsk"); + nameMap.put(timeZoneIds[56], "Beijing, Chongqing, Hong Kong, Urumqi"); + nameMap.put(timeZoneIds[57], "Irkutsk, Ulaan Bataar"); + nameMap.put(timeZoneIds[58], "Kuala Lampur, Singapore"); + nameMap.put(timeZoneIds[59], "Perth"); + nameMap.put(timeZoneIds[60], "Taipei"); + nameMap.put(timeZoneIds[61], "Osaka, Sapporo, Tokyo"); + nameMap.put(timeZoneIds[62], "Seoul"); + nameMap.put(timeZoneIds[63], "Yakutsk"); + nameMap.put(timeZoneIds[64], "Adelaide"); + nameMap.put(timeZoneIds[65], "Darwin"); + nameMap.put(timeZoneIds[66], "Brisbane"); + nameMap.put(timeZoneIds[67], "Canberra, Melbourne, Sydney"); + nameMap.put(timeZoneIds[68], "Guam, Port Moresby"); + nameMap.put(timeZoneIds[69], "Hobart"); + nameMap.put(timeZoneIds[70], "Vladivostok"); + nameMap.put(timeZoneIds[71], "Magadan, Solomon Is., New Caledonia"); + nameMap.put(timeZoneIds[72], "Auckland, Wellington"); + nameMap.put(timeZoneIds[73], "Fiji, Kamchatka, Marshall Is."); + nameMap.put(timeZoneIds[74], "Nuku'alofa"); + } + + /** + * Returns a list of all available time zone's as a String [][]. The first + * entry in each list item is the timeZoneID, and the second is the + * display name.

+ *

+ * The list of time zones attempts to be inclusive of all of the worlds + * zones while being as concise as possible. For "en" language locales + * the name is a friendly english name. For non-"en" language locales + * the standard JDK name is used for the given Locale. The GMT+/- time + * is also included for readability. + * + * @return a list of time zones, as a tuple of the zime zone ID, and its + * display name. + */ + public static String[][] getTimeZoneList() { + Locale jiveLocale = JiveGlobals.getLocale(); + + String[][] timeZoneList = timeZoneLists.get(jiveLocale); + if (timeZoneList == null) { + String[] timeZoneIDs = timeZoneIds; + // Now, create String[][] using the unique zones. + timeZoneList = new String[timeZoneIDs.length][2]; + for (int i = 0; i < timeZoneList.length; i++) { + String zoneID = timeZoneIDs[i]; + timeZoneList[i][0] = zoneID; + timeZoneList[i][1] = getTimeZoneName(zoneID, jiveLocale); + } + + // Add the new list to the map of locales to lists + timeZoneLists.put(jiveLocale, timeZoneList); + } + + return timeZoneList; + } + + /** + * Returns the display name for a time zone. The display name is the name + * specified by the Java TimeZone class for non-"en" locales or a friendly english + * name for "en", with the addition of the GMT offset + * for human readability. + * + * @param zoneID the time zone to get the name for. + * @param locale the locale to use. + * @return the display name for the time zone. + */ + public static String getTimeZoneName(String zoneID, Locale locale) { + TimeZone zone = TimeZone.getTimeZone(zoneID); + StringBuffer buf = new StringBuffer(); + // Add in the GMT part to the name. First, figure out the offset. + int offset = zone.getRawOffset(); + if (zone.inDaylightTime(new Date()) && zone.useDaylightTime()) { + offset += (int) JiveConstants.HOUR; + } + + buf.append("("); + if (offset < 0) { + buf.append("GMT-"); + } + else { + buf.append("GMT+"); + } + offset = Math.abs(offset); + int hours = offset / (int) JiveConstants.HOUR; + int minutes = (offset % (int) JiveConstants.HOUR) / (int) JiveConstants.MINUTE; + buf.append(hours).append(":"); + if (minutes < 10) { + buf.append("0").append(minutes); + } + else { + buf.append(minutes); + } + buf.append(") "); + + // Use a friendly english timezone name if the locale is en, otherwise use the timezone id + if ("en".equals(locale.getLanguage())) { + String name = nameMap.get(zoneID); + if (name == null) { + name = zoneID; + } + + buf.append(name); + } + else { + buf.append( + zone.getDisplayName(true, TimeZone.LONG, locale).replace('_', ' ').replace('/', + ' ')); + } + + return buf.toString(); + } + + /** + * Returns the specified resource bundle, which is a properties file + * that aids in localization of skins. This method is handy since it + * uses the class loader that other Jive classes are loaded from (hence, + * it can load bundles that are stored in jive.jar). + * + * @param baseName the name of the resource bundle to load. + * @param locale the desired Locale. + * @return the specified resource bundle, if it exists. + */ + public static ResourceBundle getResourceBundle(String baseName, + Locale locale) { + return ResourceBundle.getBundle(baseName, locale); + } + + /** + * Returns an internationalized string loaded from a resource bundle. + * The locale used will be the locale specified by JiveGlobals.getLocale(). + * + * @param key the key to use for retrieving the string from the + * appropriate resource bundle. + * @return the localized string. + */ + public static String getLocalizedString(String key) { + return getLocalizedString(key, JiveGlobals.getLocale(), null); + } + + /** + * Returns an internationalized string loaded from a resource bundle using + * the passed in Locale. + * + * @param key the key to use for retrieving the string from the + * appropriate resource bundle. + * @param locale the locale to use for retrieving the appropriate + * locale-specific string. + * @return the localized string. + */ + public static String getLocalizedString(String key, Locale locale) { + return getLocalizedString(key, locale, null); + } + + /** + * Returns an internationalized string loaded from a resource bundle using + * the locale specified by JiveGlobals.getLocale() substituting the passed + * in arguments. Substitution is handled using the + * {@link java.text.MessageFormat} class. + * + * @param key the key to use for retrieving the string from the + * appropriate resource bundle. + * @param arguments a list of objects to use which are formatted, then + * inserted into the pattern at the appropriate places. + * @return the localized string. + */ + public static String getLocalizedString(String key, List arguments) { + return getLocalizedString(key, JiveGlobals.getLocale(), arguments); + } + + /** + * Returns an internationalized string loaded from a resource bundle using + * the passed in Locale substituting the passed in arguments. Substitution + * is handled using the {@link java.text.MessageFormat} class. + * + * @param key the key to use for retrieving the string from the + * appropriate resource bundle. + * @param locale the locale to use for retrieving the appropriate + * locale-specific string. + * @param arguments a list of objects to use which are formatted, then + * inserted into the pattern at the appropriate places. + * @return the localized string. + */ + public static String getLocalizedString(String key, Locale locale, List arguments) { + if (key == null) { + throw new NullPointerException("Key cannot be null"); + } + if (locale == null) { + locale = JiveGlobals.getLocale(); + } + + String value; + + // See if the bundle has a value + try { + // The jdk caches resource bundles on it's own, so we won't bother. + ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale); + value = bundle.getString(key); + // perform argument substitutions + if (arguments != null) { + MessageFormat messageFormat = new MessageFormat(""); + messageFormat.setLocale(bundle.getLocale()); + messageFormat.applyPattern(value); + try { + // This isn't fool-proof, but it's better than nothing + // The idea is to try and convert strings into the + // types of objects that the formatters expects + // i.e. Numbers and Dates + Format[] formats = messageFormat.getFormats(); + for (int i = 0; i < formats.length; i++) { + Format format = formats[i]; + if (format != null) { + if (format instanceof DateFormat) { + if (arguments.size() > i) { + Object val = arguments.get(i); + if (val instanceof String) { + DateFormat dateFmt = (DateFormat)format; + try { + val = dateFmt.parse((String)val); + arguments.set(i, val); + } + catch (ParseException e) { + Log.error(e); + } + } + } + } + else if (format instanceof NumberFormat) { + if (arguments.size() > i) { + Object val = arguments.get(i); + if (val instanceof String) { + NumberFormat nbrFmt = (NumberFormat)format; + try { + val = nbrFmt.parse((String)val); + arguments.set(i, val); + } + catch (ParseException e) { + Log.error(e); + } + } + } + } + } + } + value = messageFormat.format(arguments.toArray()); + } + catch (IllegalArgumentException e) { + Log.error("Unable to format resource string for key: " + + key + ", argument type not supported"); + value = ""; + } + } + } + catch (java.util.MissingResourceException mre) { + Log.warn("Missing resource for key: " + key + + " in locale " + locale.toString()); + value = ""; + } + + return value; + } + + /** + * + */ + public static String getLocalizedNumber(long number) { + return NumberFormat.getInstance().format(number); + } + + /** + * + */ + public static String getLocalizedNumber(long number, Locale locale) { + return NumberFormat.getInstance(locale).format(number); + } + + /** + * + */ + public static String getLocalizedNumber(double number) { + return NumberFormat.getInstance().format(number); + } + + /** + * + */ + public static String getLocalizedNumber(double number, Locale locale) { + return NumberFormat.getInstance(locale).format(number); + } +} diff --git a/src/java/org/jivesoftware/util/Log.java b/src/java/org/jivesoftware/util/Log.java new file mode 100644 index 0000000..bb90b6b --- /dev/null +++ b/src/java/org/jivesoftware/util/Log.java @@ -0,0 +1,479 @@ +/** + * $RCSfile$ + * $Revision: 3195 $ + * $Date: 2005-12-13 15:07:30 -0300 (Tue, 13 Dec 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is the proprietary information of Jive Software. + * Use is subject to license terms. + */ + +package org.jivesoftware.util; + +import org.jivesoftware.util.log.Hierarchy; +import org.jivesoftware.util.log.LogTarget; +import org.jivesoftware.util.log.Logger; +import org.jivesoftware.util.log.Priority; +import org.jivesoftware.util.log.format.ExtendedPatternFormatter; +import org.jivesoftware.util.log.output.io.StreamTarget; +import org.jivesoftware.util.log.output.io.rotate.RevolvingFileStrategy; +import org.jivesoftware.util.log.output.io.rotate.RotateStrategyBySize; +import org.jivesoftware.util.log.output.io.rotate.RotatingFileTarget; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Simple wrapper to the incorporated LogKit to log under a single logging name. + * + * @author Bruce Ritchie + */ +public class Log { + + private static final Logger debugLog = Hierarchy.getDefaultHierarchy().getLoggerFor("Jive-DEBUG"); + private static final Logger infoLog = Hierarchy.getDefaultHierarchy().getLoggerFor("Jive-INFO"); + private static final Logger warnLog = Hierarchy.getDefaultHierarchy().getLoggerFor("Jive-WARN"); + private static final Logger errorLog = Hierarchy.getDefaultHierarchy().getLoggerFor("Jive-ERR"); + + private static String logNameDebug = null; + private static String logNameInfo = null; + private static String logNameWarn = null; + private static String logNameError = null; + private static String debugPattern = null; + private static String infoPattern = null; + private static String warnPattern = null; + private static String errorPattern = null; + private static String logDirectory = null; + + private static long maxDebugSize = 1024; + private static long maxInfoSize = 1024; + private static long maxWarnSize = 1024; + private static long maxErrorSize = 1024; + + private static boolean debugEnabled; + + static { + initLog(); + } + + private Log() { } + + /** + * This method is used to initialize the Log class. For normal operations this method + * should never be called, rather it's only publically available so that the class + * can be reset by the setup process once the home directory has been specified. + */ + public static void initLog() { + try { + logDirectory = JiveGlobals.getXMLProperty("log.directory"); + if (logDirectory == null) { + if (JiveGlobals.getHomeDirectory() != null) { + File managerHome = new File(JiveGlobals.getHomeDirectory()); + if (managerHome.exists() && managerHome.canWrite()) { + logDirectory = (new File(managerHome, "logs")).toString(); + } + } + } + + if (!logDirectory.endsWith(File.separator)) { + logDirectory = logDirectory + File.separator; + } + + // Make sure the logs directory exists. If not, make it: + File logDir = new File(logDirectory); + if (!logDir.exists()) { + logDir.mkdir(); + } + + logNameDebug = logDirectory + "debug.log"; + logNameInfo = logDirectory + "info.log"; + logNameWarn = logDirectory + "warn.log"; + logNameError = logDirectory + "error.log"; + + debugPattern = JiveGlobals.getXMLProperty("log.debug.format"); + infoPattern = JiveGlobals.getXMLProperty("log.info.format"); + warnPattern = JiveGlobals.getXMLProperty("log.warn.format"); + errorPattern = JiveGlobals.getXMLProperty("log.error.format"); + + try { maxDebugSize = Long.parseLong(JiveGlobals.getXMLProperty("log.debug.size")); } + catch (NumberFormatException e) { /* ignore */ } + try { maxInfoSize = Long.parseLong(JiveGlobals.getXMLProperty("log.info.size")); } + catch (NumberFormatException e) { /* ignore */ } + try { maxWarnSize = Long.parseLong(JiveGlobals.getXMLProperty("log.warn.size")); } + catch (NumberFormatException e) { /* ignore */ } + try { maxErrorSize = Long.parseLong(JiveGlobals.getXMLProperty("log.error.size")); } + catch (NumberFormatException e) { /* ignore */ } + + debugEnabled = "true".equals(JiveGlobals.getXMLProperty("log.debug.enabled")); + } + catch (Exception e) { + // we'll get an exception if home isn't setup yet - we ignore that since + // it's sure to be logged elsewhere :) + } + + if (debugPattern == null) { + debugPattern = "%{time:yyyy.MM.dd HH:mm:ss} %{message}\\n%{throwable}"; + } + if (infoPattern == null) { + infoPattern = "%{time:yyyy.MM.dd HH:mm:ss} %{message}\\n%{throwable}"; + } + if (warnPattern == null) { + warnPattern = "%{time:yyyy.MM.dd HH:mm:ss} %{message}\\n%{throwable}"; + } + if (errorPattern == null) { + errorPattern = "%{time:yyyy.MM.dd HH:mm:ss} [%{method}] %{message}\\n%{throwable}"; + } + + createLogger(debugPattern, logNameDebug, maxDebugSize, debugLog, Priority.DEBUG); + createLogger(infoPattern, logNameInfo, maxInfoSize, infoLog, Priority.INFO); + createLogger(warnPattern, logNameWarn, maxWarnSize, warnLog, Priority.WARN); + createLogger(errorPattern, logNameError, maxErrorSize, errorLog, Priority.ERROR); + + // set up the ties into jdk logging + Handler jdkLogHandler = new JiveLogHandler(); + jdkLogHandler.setLevel(Level.ALL); + java.util.logging.Logger.getLogger("").addHandler(jdkLogHandler); + } + + private static void createLogger(String pattern, String logName, long maxLogSize, + Logger logger, Priority priority) + { + // debug log file + ExtendedPatternFormatter formatter = new ExtendedPatternFormatter(pattern); + StreamTarget target = null; + Exception ioe = null; + + try { + // home was not setup correctly + if (logName == null) { + throw new IOException("LogName was null - managerHome not set?"); + } + else { + RevolvingFileStrategy fileStrategy = new RevolvingFileStrategy(logName, 5); + RotateStrategyBySize rotateStrategy = new RotateStrategyBySize(maxLogSize * 1024); + target = new RotatingFileTarget(formatter, rotateStrategy, fileStrategy); + } + } + catch (IOException e) { + ioe = e; + // can't log to file, log to stderr + target = new StreamTarget(System.err, formatter); + } + + logger.setLogTargets(new LogTarget[] { target } ); + logger.setPriority(priority); + + if (ioe != null) { + logger.debug("Error occurred opening log file: " + ioe.getMessage()); + } + } + + public static void setProductName(String productName) { + debugPattern = productName + " " + debugPattern; + infoPattern = productName + " " + infoPattern; + warnPattern = productName + " " + warnPattern; + errorPattern = productName + " " + errorPattern; + + createLogger(debugPattern, logNameDebug, maxDebugSize, debugLog, Priority.DEBUG); + createLogger(infoPattern, logNameInfo, maxInfoSize, infoLog, Priority.INFO); + createLogger(warnPattern, logNameWarn, maxWarnSize, warnLog, Priority.WARN); + createLogger(errorPattern, logNameError, maxErrorSize, errorLog, Priority.ERROR); + } + + public static boolean isErrorEnabled() { + return errorLog.isErrorEnabled(); + } + + public static boolean isFatalEnabled() { + return errorLog.isFatalErrorEnabled(); + } + + public static boolean isDebugEnabled() { + return debugEnabled; + } + + public static void setDebugEnabled(boolean enabled) { + JiveGlobals.setXMLProperty("log.debug.enabled", Boolean.toString(enabled)); + debugEnabled = enabled; + } + + public static boolean isInfoEnabled() { + return infoLog.isInfoEnabled(); + } + + public static boolean isWarnEnabled() { + return warnLog.isWarnEnabled(); + } + + public static void debug(String s) { + if (isDebugEnabled()) { + debugLog.debug(s); + } + } + + public static void debug(Throwable throwable) { + if (isDebugEnabled()) { + debugLog.debug("", throwable); + } + } + + public static void debug(String s, Throwable throwable) { + if (isDebugEnabled()) { + debugLog.debug(s, throwable); + } + } + + public static void markDebugLogFile(String username) { + RotatingFileTarget target = (RotatingFileTarget) debugLog.getLogTargets()[0]; + markLogFile(username, target); + } + + public static void rotateDebugLogFile() { + RotatingFileTarget target = (RotatingFileTarget) debugLog.getLogTargets()[0]; + try { + target.rotate(); + } + catch (IOException e) { + System.err.println("Warning: There was an error rotating the Jive debug log file. " + + "Logging may not work correctly until a restart happens."); + } + } + + public static void info(String s) { + if (isInfoEnabled()) { + infoLog.info(s); + } + } + + public static void info(Throwable throwable) { + if (isInfoEnabled()) { + infoLog.info("", throwable); + } + } + + public static void info(String s, Throwable throwable) { + if (isInfoEnabled()) { + infoLog.info(s, throwable); + } + } + + public static void markInfoLogFile(String username) { + RotatingFileTarget target = (RotatingFileTarget) infoLog.getLogTargets()[0]; + markLogFile(username, target); + } + + public static void rotateInfoLogFile() { + RotatingFileTarget target = (RotatingFileTarget) infoLog.getLogTargets()[0]; + try { + target.rotate(); + } + catch (IOException e) { + System.err.println("Warning: There was an error rotating the Jive info log file. " + + "Logging may not work correctly until a restart happens."); + } + } + + public static void warn(String s) { + if (isWarnEnabled()) { + warnLog.warn(s); + } + } + + public static void warn(Throwable throwable) { + if (isWarnEnabled()) { + warnLog.warn("", throwable); + } + } + + public static void warn(String s, Throwable throwable) { + if (isWarnEnabled()) { + warnLog.warn(s, throwable); + } + } + + public static void markWarnLogFile(String username) { + RotatingFileTarget target = (RotatingFileTarget) warnLog.getLogTargets()[0]; + markLogFile(username, target); + } + + public static void rotateWarnLogFile() { + RotatingFileTarget target = (RotatingFileTarget) warnLog.getLogTargets()[0]; + try { + target.rotate(); + } + catch (IOException e) { + System.err.println("Warning: There was an error rotating the Jive warn log file. " + + "Logging may not work correctly until a restart happens."); + } + } + + public static void error(String s) { + if (isErrorEnabled()) { + errorLog.error(s); + if (isDebugEnabled()) { + printToStdErr(s, null); + } + } + } + + public static void error(Throwable throwable) { + if (isErrorEnabled()) { + errorLog.error("", throwable); + if (isDebugEnabled()) { + printToStdErr(null, throwable); + } + } + } + + public static void error(String s, Throwable throwable) { + if (isErrorEnabled()) { + errorLog.error(s, throwable); + if (isDebugEnabled()) { + printToStdErr(s, throwable); + } + } + } + + public static void markErrorLogFile(String username) { + RotatingFileTarget target = (RotatingFileTarget) errorLog.getLogTargets()[0]; + markLogFile(username, target); + } + + public static void rotateErrorLogFile() { + RotatingFileTarget target = (RotatingFileTarget) errorLog.getLogTargets()[0]; + try { + target.rotate(); + } + catch (IOException e) { + System.err.println("Warning: There was an error rotating the Jive error log file. " + + "Logging may not work correctly until a restart happens."); + } + } + + public static void fatal(String s) { + if (isFatalEnabled()) { + errorLog.fatalError(s); + if (isDebugEnabled()) { + printToStdErr(s, null); + } + } + } + + public static void fatal(Throwable throwable) { + if (isFatalEnabled()) { + errorLog.fatalError("", throwable); + if (isDebugEnabled()) { + printToStdErr(null, throwable); + } + } + } + + public static void fatal(String s, Throwable throwable) { + if (isFatalEnabled()) { + errorLog.fatalError(s, throwable); + if (isDebugEnabled()) { + printToStdErr(s, throwable); + } + } + } + + /** + * Returns the directory that log files exist in. The directory name will + * have a File.separator as the last character in the string. + * + * @return the directory that log files exist in. + */ + public static String getLogDirectory() { + return logDirectory; + } + + private static void markLogFile(String username, RotatingFileTarget target) { + List args = new ArrayList(); + args.add(username); + args.add(JiveGlobals.formatDateTime(new java.util.Date())); + target.write(LocaleUtils.getLocalizedString("log.marker_inserted_by", args) + "\n"); + } + + private static void printToStdErr(String s, Throwable throwable) { + if (s != null) { + System.err.println(s); + } + if (throwable != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + System.err.print(sw.toString()); + System.err.print("\n"); + } + } + + private static final class JiveLogHandler extends Handler { + + public void publish(LogRecord record) { + + Level level = record.getLevel(); + Throwable throwable = record.getThrown(); + + + if (Level.SEVERE.equals(level)) { + + if (throwable != null) { + Log.error(record.getMessage(), throwable); + } + else { + Log.error(record.getMessage()); + } + + } + else if (Level.WARNING.equals(level)) { + + if (throwable != null) { + Log.warn(record.getMessage(), throwable); + } + else { + Log.warn(record.getMessage()); + } + + + } + else if (Level.INFO.equals(level)) { + + if (throwable != null) { + Log.info(record.getMessage(), throwable); + } + else { + Log.info(record.getMessage()); + } + + } + else { + // else FINE,FINER,FINEST + + if (throwable != null) { + Log.debug(record.getMessage(), throwable); + } + else { + Log.debug(record.getMessage()); + } + + } + } + + public void flush() { + // do nothing + } + + public void close() throws SecurityException { + // do nothing + } + } + +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/PropertyEventDispatcher.java b/src/java/org/jivesoftware/util/PropertyEventDispatcher.java new file mode 100644 index 0000000..d3ccc2d --- /dev/null +++ b/src/java/org/jivesoftware/util/PropertyEventDispatcher.java @@ -0,0 +1,128 @@ +/** + * $RCSfile$ + * $Revision: 1705 $ + * $Date: 2005-07-26 14:10:33 -0300 (Tue, 26 Jul 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import org.jivesoftware.util.Log; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Dispatches property events. Each event has a {@link EventType type} + * and optional parameters, as follows:

+ * + * + * + * + * + * + * + *
Event TypeExtra Params
{@link EventType#property_set property_set}A param named value that + * has the value of the property set.
{@link EventType#property_deleted property_deleted}None
{@link EventType#xml_property_set xml_property_set}A param named value that + * has the value of the property set.
{@link EventType#xml_property_deleted xml_property_deleted}None
+ * + * @author Matt Tucker + */ +public class PropertyEventDispatcher { + + private static List listeners = + new CopyOnWriteArrayList(); + + private PropertyEventDispatcher() { + // Not instantiable. + } + + /** + * Registers a listener to receive events. + * + * @param listener the listener. + */ + public static void addListener(PropertyEventListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + listeners.add(listener); + } + + /** + * Unregisters a listener to receive events. + * + * @param listener the listener. + */ + public static void removeListener(PropertyEventListener listener) { + listeners.remove(listener); + } + + /** + * Dispatches an event to all listeners. + * + * @param property the property. + * @param eventType the event type. + * @param params event parameters. + */ + public static void dispatchEvent(String property, EventType eventType, Map params) { + for (PropertyEventListener listener : listeners) { + try { + switch (eventType) { + case property_set: { + listener.propertySet(property, params); + break; + } + case property_deleted: { + listener.propertyDeleted(property, params); + break; + } + case xml_property_set: { + listener.xmlPropertySet(property, params); + break; + } + case xml_property_deleted: { + listener.xmlPropertyDeleted(property, params); + break; + } + default: + break; + } + } + catch (Exception e) { + Log.error(e); + } + } + } + + /** + * Represents valid event types. + */ + public enum EventType { + + /** + * A property was set. + */ + property_set, + + /** + * A property was deleted. + */ + property_deleted, + + /** + * An XML property was set. + */ + xml_property_set, + + /** + * An XML property was deleted. + */ + xml_property_deleted; + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/PropertyEventListener.java b/src/java/org/jivesoftware/util/PropertyEventListener.java new file mode 100644 index 0000000..9121d12 --- /dev/null +++ b/src/java/org/jivesoftware/util/PropertyEventListener.java @@ -0,0 +1,57 @@ +/** + * $RCSfile$ + * $Revision: 1705 $ + * $Date: 2005-07-26 14:10:33 -0300 (Tue, 26 Jul 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import java.util.Map; + +/** + * Interface to listen for property events. Use the + * {@link org.jivesoftware.util.PropertyEventDispatcher#addListener(PropertyEventListener)} + * method to register for events. + * + * @author Matt Tucker + */ +public interface PropertyEventListener { + + /** + * A property was set. + * + * @param property the property. + * @param params event parameters. + */ + public void propertySet(String property, Map params); + + /** + * A property was deleted. + * + * @param property the deleted. + * @param params event parameters. + */ + public void propertyDeleted(String property, Map params); + + /** + * An XML property was set. + * + * @param property the property. + * @param params event parameters. + */ + public void xmlPropertySet(String property, Map params); + + /** + * An XML property was deleted. + * + * @param property the property. + * @param params event parameters. + */ + public void xmlPropertyDeleted(String property, Map params); + +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/StringUtils.java b/src/java/org/jivesoftware/util/StringUtils.java new file mode 100644 index 0000000..7f74f1b --- /dev/null +++ b/src/java/org/jivesoftware/util/StringUtils.java @@ -0,0 +1,987 @@ +/** + * $Revision: 3870 $ + * $Date: 2006-05-10 15:48:11 -0300 (Wed, 10 May 2006) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.BreakIterator; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Utility class to peform common String manipulation algorithms. + */ +public class StringUtils { + + // Constants used by escapeHTMLTags + private static final char[] QUOTE_ENCODE = """.toCharArray(); + private static final char[] AMP_ENCODE = "&".toCharArray(); + private static final char[] LT_ENCODE = "<".toCharArray(); + private static final char[] GT_ENCODE = ">".toCharArray(); + + private StringUtils() { + // Not instantiable. + } + + /** + * Returns the name portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "matt" would be returned. If no + * username is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the name portion of the XMPP address. + */ + public static String parseName(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.indexOf("@"); + if (atIndex <= 0) { + return ""; + } + else { + return XMPPAddress.substring(0, atIndex); + } + } + + /** + * Returns the server portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "jivesoftware.com" would be returned. + * If no server is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the server portion of the XMPP address. + */ + public static String parseServer(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.indexOf("@"); + // If the String ends with '@', return the empty string. + if (atIndex + 1 > XMPPAddress.length()) { + return ""; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex > 0) { + return XMPPAddress.substring(atIndex + 1, slashIndex); + } + else { + return XMPPAddress.substring(atIndex + 1); + } + } + + /** + * Returns the resource portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "Smack" would be returned. If no + * resource is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the resource portion of the XMPP address. + */ + public static String parseResource(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex + 1 > XMPPAddress.length() || slashIndex < 0) { + return ""; + } + else { + return XMPPAddress.substring(slashIndex + 1); + } + } + + /** + * Returns the XMPP address with any resource information removed. For example, + * for the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would + * be returned. + * + * @param XMPPAddress the XMPP address. + * @return the bare XMPP address without resource information. + */ + public static String parseBareAddress(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex < 0) { + return XMPPAddress; + } + else if (slashIndex == 0) { + return ""; + } + else { + return XMPPAddress.substring(0, slashIndex); + } + } + + /** + * Replaces all instances of oldString with newString in string. + * + * @param string the String to search to perform replacements on. + * @param oldString the String that should be replaced by newString. + * @param newString the String that will replace all instances of oldString. + * @return a String will all instances of oldString replaced by newString. + */ + public static String replace(String string, String oldString, String newString) { + if (string == null) { + return null; + } + int i = 0; + // Make sure that oldString appears at least once before doing any processing. + if ((i = string.indexOf(oldString, i)) >= 0) { + // Use char []'s, as they are more efficient to deal with. + char[] string2 = string.toCharArray(); + char[] newString2 = newString.toCharArray(); + int oLength = oldString.length(); + StringBuilder buf = new StringBuilder(string2.length); + buf.append(string2, 0, i).append(newString2); + i += oLength; + int j = i; + // Replace all remaining instances of oldString with newString. + while ((i = string.indexOf(oldString, i)) > 0) { + buf.append(string2, j, i - j).append(newString2); + i += oLength; + j = i; + } + buf.append(string2, j, string2.length - j); + return buf.toString(); + } + return string; + } + + /** + * Replaces all instances of oldString with newString in line with the + * added feature that matches of newString in oldString ignore case. + * + * @param line the String to search to perform replacements on + * @param oldString the String that should be replaced by newString + * @param newString the String that will replace all instances of oldString + * @return a String will all instances of oldString replaced by newString + */ + public static String replaceIgnoreCase(String line, String oldString, + String newString) { + if (line == null) { + return null; + } + String lcLine = line.toLowerCase(); + String lcOldString = oldString.toLowerCase(); + int i = 0; + if ((i = lcLine.indexOf(lcOldString, i)) >= 0) { + char[] line2 = line.toCharArray(); + char[] newString2 = newString.toCharArray(); + int oLength = oldString.length(); + StringBuilder buf = new StringBuilder(line2.length); + buf.append(line2, 0, i).append(newString2); + i += oLength; + int j = i; + while ((i = lcLine.indexOf(lcOldString, i)) > 0) { + buf.append(line2, j, i - j).append(newString2); + i += oLength; + j = i; + } + buf.append(line2, j, line2.length - j); + return buf.toString(); + } + return line; + } + + /** + * Replaces all instances of oldString with newString in line with the + * added feature that matches of newString in oldString ignore case. + * The count paramater is set to the number of replaces performed. + * + * @param line the String to search to perform replacements on + * @param oldString the String that should be replaced by newString + * @param newString the String that will replace all instances of oldString + * @param count a value that will be updated with the number of replaces + * performed. + * @return a String will all instances of oldString replaced by newString + */ + public static String replaceIgnoreCase(String line, String oldString, + String newString, int[] count) + { + if (line == null) { + return null; + } + String lcLine = line.toLowerCase(); + String lcOldString = oldString.toLowerCase(); + int i = 0; + if ((i = lcLine.indexOf(lcOldString, i)) >= 0) { + int counter = 1; + char[] line2 = line.toCharArray(); + char[] newString2 = newString.toCharArray(); + int oLength = oldString.length(); + StringBuilder buf = new StringBuilder(line2.length); + buf.append(line2, 0, i).append(newString2); + i += oLength; + int j = i; + while ((i = lcLine.indexOf(lcOldString, i)) > 0) { + counter++; + buf.append(line2, j, i - j).append(newString2); + i += oLength; + j = i; + } + buf.append(line2, j, line2.length - j); + count[0] = counter; + return buf.toString(); + } + return line; + } + + /** + * Replaces all instances of oldString with newString in line. + * The count Integer is updated with number of replaces. + * + * @param line the String to search to perform replacements on. + * @param oldString the String that should be replaced by newString. + * @param newString the String that will replace all instances of oldString. + * @return a String will all instances of oldString replaced by newString. + */ + public static String replace(String line, String oldString, + String newString, int[] count) + { + if (line == null) { + return null; + } + int i = 0; + if ((i = line.indexOf(oldString, i)) >= 0) { + int counter = 1; + char[] line2 = line.toCharArray(); + char[] newString2 = newString.toCharArray(); + int oLength = oldString.length(); + StringBuilder buf = new StringBuilder(line2.length); + buf.append(line2, 0, i).append(newString2); + i += oLength; + int j = i; + while ((i = line.indexOf(oldString, i)) > 0) { + counter++; + buf.append(line2, j, i - j).append(newString2); + i += oLength; + j = i; + } + buf.append(line2, j, line2.length - j); + count[0] = counter; + return buf.toString(); + } + return line; + } + + /** + * This method takes a string and strips out all tags except
tags while still leaving + * the tag body intact. + * + * @param in the text to be converted. + * @return the input string with all tags removed. + */ + public static String stripTags(String in) { + if (in == null) { + return null; + } + char ch; + int i = 0; + int last = 0; + char[] input = in.toCharArray(); + int len = input.length; + StringBuilder out = new StringBuilder((int)(len * 1.3)); + for (; i < len; i++) { + ch = input[i]; + if (ch > '>') { + } + else if (ch == '<') { + if (i + 3 < len && input[i + 1] == 'b' && input[i + 2] == 'r' && input[i + 3] == '>') { + i += 3; + continue; + } + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + } + else if (ch == '>') { + last = i + 1; + } + } + if (last == 0) { + return in; + } + if (i > last) { + out.append(input, last, i - last); + } + return out.toString(); + } + + /** + * This method takes a string which may contain HTML tags (ie, <b>, + * <table>, etc) and converts the '<'' and '>' characters to + * their HTML escape sequences. + * + * @param in the text to be converted. + * @return the input string with the characters '<' and '>' replaced + * with their HTML escape sequences. + */ + public static String escapeHTMLTags(String in) { + if (in == null) { + return null; + } + char ch; + int i = 0; + int last = 0; + char[] input = in.toCharArray(); + int len = input.length; + StringBuilder out = new StringBuilder((int)(len * 1.3)); + for (; i < len; i++) { + ch = input[i]; + if (ch > '>') { + } + else if (ch == '<') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(LT_ENCODE); + } + else if (ch == '>') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(GT_ENCODE); + } + } + if (last == 0) { + return in; + } + if (i > last) { + out.append(input, last, i - last); + } + return out.toString(); + } + + /** + * Used by the hash method. + */ + private static Map digests = + new ConcurrentHashMap(); + + /** + * Hashes a String using the Md5 algorithm and returns the result as a + * String of hexadecimal numbers. This method is synchronized to avoid + * excessive MessageDigest object creation. If calling this method becomes + * a bottleneck in your code, you may wish to maintain a pool of + * MessageDigest objects instead of using this method. + *

+ * A hash is a one-way function -- that is, given an + * input, an output is easily computed. However, given the output, the + * input is almost impossible to compute. This is useful for passwords + * since we can store the hash and a hacker will then have a very hard time + * determining the original password. + *

+ * In Jive, every time a user logs in, we simply + * take their plain text password, compute the hash, and compare the + * generated hash to the stored hash. Since it is almost impossible that + * two passwords will generate the same hash, we know if the user gave us + * the correct password or not. The only negative to this system is that + * password recovery is basically impossible. Therefore, a reset password + * method is used instead. + * + * @param data the String to compute the hash of. + * @return a hashed version of the passed-in String + */ + public static String hash(String data) { + return hash(data, "MD5"); + } + + /** + * Hashes a String using the specified algorithm and returns the result as a + * String of hexadecimal numbers. This method is synchronized to avoid + * excessive MessageDigest object creation. If calling this method becomes + * a bottleneck in your code, you may wish to maintain a pool of + * MessageDigest objects instead of using this method. + *

+ * A hash is a one-way function -- that is, given an + * input, an output is easily computed. However, given the output, the + * input is almost impossible to compute. This is useful for passwords + * since we can store the hash and a hacker will then have a very hard time + * determining the original password. + *

+ * In Jive, every time a user logs in, we simply + * take their plain text password, compute the hash, and compare the + * generated hash to the stored hash. Since it is almost impossible that + * two passwords will generate the same hash, we know if the user gave us + * the correct password or not. The only negative to this system is that + * password recovery is basically impossible. Therefore, a reset password + * method is used instead. + * + * @param data the String to compute the hash of. + * @param algorithm the name of the algorithm requested. + * @return a hashed version of the passed-in String + */ + public static String hash(String data, String algorithm) { + try { + return hash(data.getBytes("utf-8"), algorithm); + } + catch (UnsupportedEncodingException e) { + Log.error(e); + } + return data; + } + + /** + * Hashes a byte array using the specified algorithm and returns the result as a + * String of hexadecimal numbers. This method is synchronized to avoid + * excessive MessageDigest object creation. If calling this method becomes + * a bottleneck in your code, you may wish to maintain a pool of + * MessageDigest objects instead of using this method. + *

+ * A hash is a one-way function -- that is, given an + * input, an output is easily computed. However, given the output, the + * input is almost impossible to compute. This is useful for passwords + * since we can store the hash and a hacker will then have a very hard time + * determining the original password. + *

+ * In Jive, every time a user logs in, we simply + * take their plain text password, compute the hash, and compare the + * generated hash to the stored hash. Since it is almost impossible that + * two passwords will generate the same hash, we know if the user gave us + * the correct password or not. The only negative to this system is that + * password recovery is basically impossible. Therefore, a reset password + * method is used instead. + * + * @param bytes the byte array to compute the hash of. + * @param algorithm the name of the algorithm requested. + * @return a hashed version of the passed-in String + */ + public static String hash(byte[] bytes, String algorithm) { + synchronized (algorithm.intern()) { + MessageDigest digest = digests.get(algorithm); + if (digest == null) { + try { + digest = MessageDigest.getInstance(algorithm); + digests.put(algorithm, digest); + } + catch (NoSuchAlgorithmException nsae) { + Log.error("Failed to load the " + algorithm + " MessageDigest. " + + "Jive will be unable to function normally.", nsae); + return null; + } + } + // Now, compute hash. + digest.update(bytes); + return encodeHex(digest.digest()); + } + } + + /** + * Turns an array of bytes into a String representing each byte as an + * unsigned hex number. + *

+ * Method by Santeri Paavolainen, Helsinki Finland 1996
+ * (c) Santeri Paavolainen, Helsinki Finland 1996
+ * Distributed under LGPL. + * + * @param bytes an array of bytes to convert to a hex-string + * @return generated hex string + */ + public static String encodeHex(byte[] bytes) { + StringBuilder buf = new StringBuilder(bytes.length * 2); + int i; + + for (i = 0; i < bytes.length; i++) { + if (((int)bytes[i] & 0xff) < 0x10) { + buf.append("0"); + } + buf.append(Long.toString((int)bytes[i] & 0xff, 16)); + } + return buf.toString(); + } + + /** + * Turns a hex encoded string into a byte array. It is specifically meant + * to "reverse" the toHex(byte[]) method. + * + * @param hex a hex encoded String to transform into a byte array. + * @return a byte array representing the hex String[ + */ + public static byte[] decodeHex(String hex) { + char[] chars = hex.toCharArray(); + byte[] bytes = new byte[chars.length / 2]; + int byteCount = 0; + for (int i = 0; i < chars.length; i += 2) { + int newByte = 0x00; + newByte |= hexCharToByte(chars[i]); + newByte <<= 4; + newByte |= hexCharToByte(chars[i + 1]); + bytes[byteCount] = (byte)newByte; + byteCount++; + } + return bytes; + } + + /** + * Returns the the byte value of a hexadecmical char (0-f). It's assumed + * that the hexidecimal chars are lower case as appropriate. + * + * @param ch a hexedicmal character (0-f) + * @return the byte value of the character (0x00-0x0F) + */ + private static byte hexCharToByte(char ch) { + switch (ch) { + case '0': + return 0x00; + case '1': + return 0x01; + case '2': + return 0x02; + case '3': + return 0x03; + case '4': + return 0x04; + case '5': + return 0x05; + case '6': + return 0x06; + case '7': + return 0x07; + case '8': + return 0x08; + case '9': + return 0x09; + case 'a': + return 0x0A; + case 'b': + return 0x0B; + case 'c': + return 0x0C; + case 'd': + return 0x0D; + case 'e': + return 0x0E; + case 'f': + return 0x0F; + } + return 0x00; + } + + /** + * Encodes a String as a base64 String. + * + * @param data a String to encode. + * @return a base64 encoded String. + */ + public static String encodeBase64(String data) { + byte[] bytes = null; + try { + bytes = data.getBytes("UTF-8"); + } + catch (UnsupportedEncodingException uee) { + Log.error(uee); + } + return encodeBase64(bytes); + } + + /** + * Encodes a byte array into a base64 String. + * + * @param data a byte array to encode. + * @return a base64 encode String. + */ + public static String encodeBase64(byte[] data) { + // Encode the String. We pass in a flag to specify that line + // breaks not be added. This is consistent with our previous base64 + // implementation. Section 2.1 of 3548 (base64 spec) also specifies + // no line breaks by default. + return Base64.encodeBytes(data, Base64.DONT_BREAK_LINES); + } + + /** + * Decodes a base64 String. + * + * @param data a base64 encoded String to decode. + * @return the decoded String. + */ + public static byte[] decodeBase64(String data) { + return Base64.decode(data); + } + + /** + * Converts a line of text into an array of lower case words using a + * BreakIterator.wordInstance().

+ * + * This method is under the Jive Open Source Software License and was + * written by Mark Imbriaco. + * + * @param text a String of text to convert into an array of words + * @return text broken up into an array of words. + */ + public static String[] toLowerCaseWordArray(String text) { + if (text == null || text.length() == 0) { + return new String[0]; + } + + List wordList = new ArrayList(); + BreakIterator boundary = BreakIterator.getWordInstance(); + boundary.setText(text); + int start = 0; + + for (int end = boundary.next(); end != BreakIterator.DONE; + start = end, end = boundary.next()) { + String tmp = text.substring(start, end).trim(); + // Remove characters that are not needed. + tmp = replace(tmp, "+", ""); + tmp = replace(tmp, "/", ""); + tmp = replace(tmp, "\\", ""); + tmp = replace(tmp, "#", ""); + tmp = replace(tmp, "*", ""); + tmp = replace(tmp, ")", ""); + tmp = replace(tmp, "(", ""); + tmp = replace(tmp, "&", ""); + if (tmp.length() > 0) { + wordList.add(tmp); + } + } + return wordList.toArray(new String[wordList.size()]); + } + + /** + * Pseudo-random number generator object for use with randomString(). + * The Random class is not considered to be cryptographically secure, so + * only use these random Strings for low to medium security applications. + */ + private static Random randGen = new Random(); + + /** + * Array of numbers and letters of mixed case. Numbers appear in the list + * twice so that there is a more equal chance that a number will be picked. + * We can use the array to get a random number or letter by picking a random + * array index. + */ + private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" + + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); + + /** + * Returns a random String of numbers and letters (lower and upper case) + * of the specified length. The method uses the Random class that is + * built-in to Java which is suitable for low to medium grade security uses. + * This means that the output is only pseudo random, i.e., each number is + * mathematically generated so is not truly random.

+ *

+ * The specified length must be at least one. If not, the method will return + * null. + * + * @param length the desired length of the random String to return. + * @return a random String of numbers and letters of the specified length. + */ + public static String randomString(int length) { + if (length < 1) { + return null; + } + // Create a char buffer to put random letters and numbers in. + char[] randBuffer = new char[length]; + for (int i = 0; i < randBuffer.length; i++) { + randBuffer[i] = numbersAndLetters[randGen.nextInt(71)]; + } + return new String(randBuffer); + } + + /** + * Intelligently chops a String at a word boundary (whitespace) that occurs + * at the specified index in the argument or before. However, if there is a + * newline character before length, the String will be chopped + * there. If no newline or whitespace is found in string up to + * the index length, the String will chopped at length. + *

+ * For example, chopAtWord("This is a nice String", 10) will return + * "This is a" which is the first word boundary less than or equal to 10 + * characters into the original String. + * + * @param string the String to chop. + * @param length the index in string to start looking for a + * whitespace boundary at. + * @return a substring of string whose length is less than or + * equal to length, and that is chopped at whitespace. + */ + public static String chopAtWord(String string, int length) { + if (string == null || string.length() == 0) { + return string; + } + + char[] charArray = string.toCharArray(); + int sLength = string.length(); + if (length < sLength) { + sLength = length; + } + + // First check if there is a newline character before length; if so, + // chop word there. + for (int i = 0; i < sLength - 1; i++) { + // Windows + if (charArray[i] == '\r' && charArray[i + 1] == '\n') { + return string.substring(0, i + 1); + } + // Unix + else if (charArray[i] == '\n') { + return string.substring(0, i); + } + } + // Also check boundary case of Unix newline + if (charArray[sLength - 1] == '\n') { + return string.substring(0, sLength - 1); + } + + // Done checking for newline, now see if the total string is less than + // the specified chop point. + if (string.length() < length) { + return string; + } + + // No newline, so chop at the first whitespace. + for (int i = length - 1; i > 0; i--) { + if (charArray[i] == ' ') { + return string.substring(0, i).trim(); + } + } + + // Did not find word boundary so return original String chopped at + // specified length. + return string.substring(0, length); + } + + /** + * Reformats a string where lines that are longer than width + * are split apart at the earliest wordbreak or at maxLength, whichever is + * sooner. If the width specified is less than 5 or greater than the input + * Strings length the string will be returned as is. + *

+ * Please note that this method can be lossy - trailing spaces on wrapped + * lines may be trimmed. + * + * @param input the String to reformat. + * @param width the maximum length of any one line. + * @return a new String with reformatted as needed. + */ + public static String wordWrap(String input, int width, Locale locale) { + // protect ourselves + if (input == null) { + return ""; + } + else if (width < 5) { + return input; + } + else if (width >= input.length()) { + return input; + } + + // default locale + if (locale == null) { + locale = JiveGlobals.getLocale(); + } + + StringBuilder buf = new StringBuilder(input); + boolean endOfLine = false; + int lineStart = 0; + + for (int i = 0; i < buf.length(); i++) { + if (buf.charAt(i) == '\n') { + lineStart = i + 1; + endOfLine = true; + } + + // handle splitting at width character + if (i > lineStart + width - 1) { + if (!endOfLine) { + int limit = i - lineStart - 1; + BreakIterator breaks = BreakIterator.getLineInstance(locale); + breaks.setText(buf.substring(lineStart, i)); + int end = breaks.last(); + + // if the last character in the search string isn't a space, + // we can't split on it (looks bad). Search for a previous + // break character + if (end == limit + 1) { + if (!Character.isWhitespace(buf.charAt(lineStart + end))) { + end = breaks.preceding(end - 1); + } + } + + // if the last character is a space, replace it with a \n + if (end != BreakIterator.DONE && end == limit + 1) { + buf.replace(lineStart + end, lineStart + end + 1, "\n"); + lineStart = lineStart + end; + } + // otherwise, just insert a \n + else if (end != BreakIterator.DONE && end != 0) { + buf.insert(lineStart + end, '\n'); + lineStart = lineStart + end + 1; + } + else { + buf.insert(i, '\n'); + lineStart = i + 1; + } + } + else { + buf.insert(i, '\n'); + lineStart = i + 1; + endOfLine = false; + } + } + } + + return buf.toString(); + } + + /** + * Escapes all necessary characters in the String so that it can be used in SQL + * + * @param string the string to escape. + * @return the string with appropriate characters escaped. + */ + public static String escapeForSQL(String string) { + if (string == null) { + return null; + } + else if (string.length() == 0) { + return string; + } + + char ch; + char[] input = string.toCharArray(); + int i = 0; + int last = 0; + int len = input.length; + StringBuilder out = null; + for (; i < len; i++) { + ch = input[i]; + + if (ch == '\'') { + if (out == null) { + out = new StringBuilder(len + 2); + } + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append('\'').append('\''); + } + } + + if (out == null) { + return string; + } + else if (i > last) { + out.append(input, last, i - last); + } + + return out.toString(); + } + + /** + * Escapes all necessary characters in the String so that it can be used + * in an XML doc. + * + * @param string the string to escape. + * @return the string with appropriate characters escaped. + */ + public static String escapeForXML(String string) { + if (string == null) { + return null; + } + char ch; + int i = 0; + int last = 0; + char[] input = string.toCharArray(); + int len = input.length; + StringBuilder out = new StringBuilder((int)(len * 1.3)); + for (; i < len; i++) { + ch = input[i]; + if (ch > '>') { + } + else if (ch == '<') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(LT_ENCODE); + } + else if (ch == '&') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(AMP_ENCODE); + } + else if (ch == '"') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(QUOTE_ENCODE); + } + } + if (last == 0) { + return string; + } + if (i > last) { + out.append(input, last, i - last); + } + return out.toString(); + } + + /** + * Unescapes the String by converting XML escape sequences back into normal + * characters. + * + * @param string the string to unescape. + * @return the string with appropriate characters unescaped. + */ + public static String unescapeFromXML(String string) { + string = replace(string, "<", "<"); + string = replace(string, ">", ">"); + string = replace(string, """, "\""); + return replace(string, "&", "&"); + } + + private static final char[] zeroArray = + "0000000000000000000000000000000000000000000000000000000000000000".toCharArray(); + + /** + * Pads the supplied String with 0's to the specified length and returns + * the result as a new String. For example, if the initial String is + * "9999" and the desired length is 8, the result would be "00009999". + * This type of padding is useful for creating numerical values that need + * to be stored and sorted as character data. Note: the current + * implementation of this method allows for a maximum length of + * 64. + * + * @param string the original String to pad. + * @param length the desired length of the new padded String. + * @return a new String padded with the required number of 0's. + */ + public static String zeroPadString(String string, int length) { + if (string == null || string.length() > length) { + return string; + } + StringBuilder buf = new StringBuilder(length); + buf.append(zeroArray, 0, length - string.length()).append(string); + return buf.toString(); + } + + /** + * Formats a Date as a fifteen character long String made up of the Date's + * padded millisecond value. + * + * @return a Date encoded as a String. + */ + public static String dateToMillis(Date date) { + return zeroPadString(Long.toString(date.getTime()), 15); + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/Version.java b/src/java/org/jivesoftware/util/Version.java new file mode 100644 index 0000000..b2eaf97 --- /dev/null +++ b/src/java/org/jivesoftware/util/Version.java @@ -0,0 +1,148 @@ +/** + * $RCSfile$ + * $Revision: 3195 $ + * $Date: 2005-12-13 15:07:30 -0300 (Tue, 13 Dec 2005) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +/** + * Holds version information for Wildfire. + * + * @author Iain Shigeoka + */ +public class Version { + + /** + * The major number (ie 1.x.x). + */ + private int major; + + /** + * The minor version number (ie x.1.x). + */ + private int minor; + + /** + * The micro version number (ie x.x.1). + */ + private int micro; + + /** + * A status release number or -1 to indicate none. + */ + private int statusVersion; + + /** + * The release state of the product (Release, Release Candidate). + */ + private ReleaseStatus status; + + /** + * Cached version string information + */ + private String versionString; + + /** + * Create a new version information object. + * + * @param major the major release number. + * @param minor the minor release number. + * @param micro the micro release number. + * @param status the status of the release. + */ + public Version(int major, int minor, int micro, ReleaseStatus status, int statusVersion) { + this.major = major; + this.minor = minor; + this.micro = micro; + this.status = status; + this.statusVersion = statusVersion; + if (status != null) { + if (status == ReleaseStatus.Release) { + versionString = major + "." + minor + "." + micro; + } + else { + if (statusVersion >= 0) { + versionString = major + "." + minor + "." + micro + " " + status.toString() + + " " + statusVersion; + } + else { + versionString = major + "." + minor + "." + micro + " " + status.toString(); + } + } + } + else { + versionString = major + "." + minor + "." + micro; + } + } + + /** + * Returns the version number of this instance of Wildfire as a + * String (ie major.minor.revision). + * + * @return The version as a string + */ + public String getVersionString() { + return versionString; + } + + /** + * Returns the release status of this product. + * + * @return the release status of this product. + */ + public ReleaseStatus getStatus() { + return status; + } + + /** + * Obtain the major release number for this product. + * + * @return The major release number 1.x.x + */ + public int getMajor() { + return major; + } + + /** + * Obtain the minor release number for this product. + * + * @return The minor release number x.1.x + */ + public int getMinor() { + return minor; + } + + /** + * Obtain the micro release number for this product. + * + * @return The micro release number x.x.1 + */ + public int getMicro() { + return micro; + } + + /** + * Obtain the status relase number for this product. For example, if + * the release status is alpha the release may be 5 + * resulting in a release status of Alpha 5. + * + * @return The status version or -1 if none is set. + */ + public int getStatusVersion() { + return statusVersion; + } + + /** + * A class to represent the release status of the server. Product releases + * are indicated by type safe enum constants. + */ + public enum ReleaseStatus { + Release, Release_Candidate, Beta, Alpha; + } +} diff --git a/src/java/org/jivesoftware/util/XMLProperties.java b/src/java/org/jivesoftware/util/XMLProperties.java new file mode 100644 index 0000000..cc84b6f --- /dev/null +++ b/src/java/org/jivesoftware/util/XMLProperties.java @@ -0,0 +1,576 @@ +/** + * $RCSfile$ + * $Revision: 3632 $ + * $Date: 2006-03-25 21:33:56 -0300 (Sat, 25 Mar 2006) $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; + +import java.io.*; +import java.util.*; + +/** + * Provides the the ability to use simple XML property files. Each property is + * in the form X.Y.Z, which would map to an XML snippet of: + *

+ * <X>
+ *     <Y>
+ *         <Z>someValue</Z>
+ *     </Y>
+ * </X>
+ * 
+ *

+ * The XML file is passed in to the constructor and must be readable and + * writtable. Setting property values will automatically persist those value + * to disk. The file encoding used is UTF-8. + * + * @author Derek DeMoro + * @author Iain Shigeoka + */ +public class XMLProperties { + + private File file; + private Document document; + + /** + * Parsing the XML file every time we need a property is slow. Therefore, + * we use a Map to cache property values that are accessed more than once. + */ + private Map propertyCache = new HashMap(); + + /** + * Creates a new XMLPropertiesTest object. + * + * @param fileName the full path the file that properties should be read from + * and written to. + * @throws IOException if an error occurs loading the properties. + */ + public XMLProperties(String fileName) throws IOException { + this(new File(fileName)); + } + + /** + * Loads XML properties from a stream. + * + * @param in the input stream of XML. + * @throws IOException if an exception occurs when reading the stream. + */ + public XMLProperties(InputStream in) throws IOException { + Reader reader = new BufferedReader(new InputStreamReader(in)); + buildDoc(reader); + } + + /** + * Creates a new XMLPropertiesTest object. + * + * @param file the file that properties should be read from and written to. + * @throws IOException if an error occurs loading the properties. + */ + public XMLProperties(File file) throws IOException { + this.file = file; + if (!file.exists()) { + // Attempt to recover from this error case by seeing if the + // tmp file exists. It's possible that the rename of the + // tmp file failed the last time Jive was running, + // but that it exists now. + File tempFile; + tempFile = new File(file.getParentFile(), file.getName() + ".tmp"); + if (tempFile.exists()) { + Log.error("WARNING: " + file.getName() + " was not found, but temp file from " + + "previous write operation was. Attempting automatic recovery." + + " Please check file for data consistency."); + tempFile.renameTo(file); + } + // There isn't a possible way to recover from the file not + // being there, so throw an error. + else { + throw new FileNotFoundException("XML properties file does not exist: " + + file.getName()); + } + } + // Check read and write privs. + if (!file.canRead()) { + throw new IOException("XML properties file must be readable: " + file.getName()); + } + if (!file.canWrite()) { + throw new IOException("XML properties file must be writable: " + file.getName()); + } + + FileReader reader = new FileReader(file); + buildDoc(reader); + } + + /** + * Returns the value of the specified property. + * + * @param name the name of the property to get. + * @return the value of the specified property. + */ + public synchronized String getProperty(String name) { + String value = propertyCache.get(name); + if (value != null) { + return value; + } + + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length; i++) { + element = element.element(propName[i]); + if (element == null) { + // This node doesn't match this part of the property name which + // indicates this property doesn't exist so return null. + return null; + } + } + // At this point, we found a matching property, so return its value. + // Empty strings are returned as null. + value = element.getTextTrim(); + if ("".equals(value)) { + return null; + } + else { + // Add to cache so that getting property next time is fast. + propertyCache.put(name, value); + return value; + } + } + + /** + * Return all values who's path matches the given property + * name as a String array, or an empty array if the if there + * are no children. This allows you to retrieve several values + * with the same property name. For example, consider the + * XML file entry: + *

+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *         <prop>other value</prop>
+     *         <prop>last value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * If you call getProperties("foo.bar.prop") will return a string array containing + * {"some value", "other value", "last value"}. + * + * @param name the name of the property to retrieve + * @return all child property values for the given node name. + */ + public String[] getProperties(String name) { + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy, + // stopping one short. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length - 1; i++) { + element = element.element(propName[i]); + if (element == null) { + // This node doesn't match this part of the property name which + // indicates this property doesn't exist so return empty array. + return new String[]{}; + } + } + // We found matching property, return names of children. + Iterator iter = element.elementIterator(propName[propName.length - 1]); + List props = new ArrayList(); + String value; + while (iter.hasNext()) { + // Empty strings are skipped. + value = ((Element)iter.next()).getTextTrim(); + if (!"".equals(value)) { + props.add(value); + } + } + String[] childrenNames = new String[props.size()]; + return props.toArray(childrenNames); + } + + /** + * Return all values who's path matches the given property + * name as a String array, or an empty array if the if there + * are no children. This allows you to retrieve several values + * with the same property name. For example, consider the + * XML file entry: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *         <prop>other value</prop>
+     *         <prop>last value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * If you call getProperties("foo.bar.prop") will return a string array containing + * {"some value", "other value", "last value"}. + * + * @param name the name of the property to retrieve + * @return all child property values for the given node name. + */ + public Iterator getChildProperties(String name) { + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy, + // stopping one short. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length - 1; i++) { + element = element.element(propName[i]); + if (element == null) { + // This node doesn't match this part of the property name which + // indicates this property doesn't exist so return empty array. + return Collections.EMPTY_LIST.iterator(); + } + } + // We found matching property, return names of children. + Iterator iter = element.elementIterator(propName[propName.length - 1]); + ArrayList props = new ArrayList(); + while (iter.hasNext()) { + props.add(((Element)iter.next()).getText()); + } + return props.iterator(); + } + + /** + * Returns the value of the attribute of the given property name or null + * if it doesn't exist. Note, this + * + * @param name the property name to lookup - ie, "foo.bar" + * @param attribute the name of the attribute, ie "id" + * @return the value of the attribute of the given property or null if + * it doesn't exist. + */ + public String getAttribute(String name, String attribute) { + if (name == null || attribute == null) { + return null; + } + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length; i++) { + String child = propName[i]; + element = element.element(child); + if (element == null) { + // This node doesn't match this part of the property name which + // indicates this property doesn't exist so return empty array. + break; + } + } + if (element != null) { + // Get its attribute values + return element.attributeValue(attribute); + } + return null; + } + + /** + * Sets a property to an array of values. Multiple values matching the same property + * is mapped to an XML file as multiple elements containing each value. + * For example, using the name "foo.bar.prop", and the value string array containing + * {"some value", "other value", "last value"} would produce the following XML: + *
+     * <foo>
+     *     <bar>
+     *         <prop>some value</prop>
+     *         <prop>other value</prop>
+     *         <prop>last value</prop>
+     *     </bar>
+     * </foo>
+     * 
+ * + * @param name the name of the property. + * @param values the values for the property (can be empty but not null). + */ + public void setProperties(String name, List values) { + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy, + // stopping one short. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length - 1; i++) { + // If we don't find this part of the property in the XML heirarchy + // we add it as a new node + if (element.element(propName[i]) == null) { + element.addElement(propName[i]); + } + element = element.element(propName[i]); + } + String childName = propName[propName.length - 1]; + // We found matching property, clear all children. + List toRemove = new ArrayList(); + Iterator iter = element.elementIterator(childName); + while (iter.hasNext()) { + toRemove.add(iter.next()); + } + for (iter = toRemove.iterator(); iter.hasNext();) { + element.remove((Element)iter.next()); + } + // Add the new children. + for (String value : values) { + element.addElement(childName).setText(value); + } + saveProperties(); + + // Generate event. + Map params = new HashMap(); + params.put("value", values); + PropertyEventDispatcher.dispatchEvent(name, + PropertyEventDispatcher.EventType.xml_property_set, params); + } + + /** + * Return all children property names of a parent property as a String array, + * or an empty array if the if there are no children. For example, given + * the properties X.Y.A, X.Y.B, and X.Y.C, then + * the child properties of X.Y are A, B, and + * C. + * + * @param parent the name of the parent property. + * @return all child property values for the given parent. + */ + public String[] getChildrenProperties(String parent) { + String[] propName = parsePropertyName(parent); + // Search for this property by traversing down the XML heirarchy. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length; i++) { + element = element.element(propName[i]); + if (element == null) { + // This node doesn't match this part of the property name which + // indicates this property doesn't exist so return empty array. + return new String[]{}; + } + } + // We found matching property, return names of children. + List children = element.elements(); + int childCount = children.size(); + String[] childrenNames = new String[childCount]; + for (int i = 0; i < childCount; i++) { + childrenNames[i] = ((Element)children.get(i)).getName(); + } + return childrenNames; + } + + /** + * Sets the value of the specified property. If the property doesn't + * currently exist, it will be automatically created. + * + * @param name the name of the property to set. + * @param value the new value for the property. + */ + public synchronized void setProperty(String name, String value) { + if (name == null) return; + if (value == null) value = ""; + + // Set cache correctly with prop name and value. + propertyCache.put(name, value); + + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length; i++) { + // If we don't find this part of the property in the XML heirarchy + // we add it as a new node + if (element.element(propName[i]) == null) { + element.addElement(propName[i]); + } + element = element.element(propName[i]); + } + // Set the value of the property in this node. + element.setText(value); + // Write the XML properties to disk + saveProperties(); + + // Generate event. + Map params = new HashMap(); + params.put("value", value); + PropertyEventDispatcher.dispatchEvent(name, + PropertyEventDispatcher.EventType.xml_property_set, params); + } + + /** + * Deletes the specified property. + * + * @param name the property to delete. + */ + public synchronized void deleteProperty(String name) { + // Remove property from cache. + propertyCache.remove(name); + + String[] propName = parsePropertyName(name); + // Search for this property by traversing down the XML heirarchy. + Element element = document.getRootElement(); + for (int i = 0; i < propName.length - 1; i++) { + element = element.element(propName[i]); + // Can't find the property so return. + if (element == null) { + return; + } + } + // Found the correct element to remove, so remove it... + element.remove(element.element(propName[propName.length - 1])); + // .. then write to disk. + saveProperties(); + + // Generate event. + PropertyEventDispatcher.dispatchEvent(name, + PropertyEventDispatcher.EventType.xml_property_deleted, Collections.emptyMap()); + } + + /** + * Builds the document XML model up based the given reader of XML data. + */ + private void buildDoc(Reader in) throws IOException { + try { + SAXReader xmlReader = new SAXReader(); + document = xmlReader.read(in); + } + catch (Exception e) { + Log.error("Error reading XML properties", e); + throw new IOException(e.getMessage()); + } + finally { + if (in != null) { + in.close(); + } + } + } + + /** + * Saves the properties to disk as an XML document. A temporary file is + * used during the writing process for maximum safety. + */ + private synchronized void saveProperties() { + boolean error = false; + // Write data out to a temporary file first. + File tempFile = null; + Writer writer = null; + try { + tempFile = new File(file.getParentFile(), file.getName() + ".tmp"); + writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile))); + OutputFormat prettyPrinter = OutputFormat.createPrettyPrint(); + XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter); + xmlWriter.write(document); + } + catch (Exception e) { + Log.error(e); + // There were errors so abort replacing the old property file. + error = true; + } + finally { + if (writer != null) { + try { + writer.close(); + } + catch (IOException e1) { + Log.error(e1); + error = true; + } + } + } + + // No errors occured, so delete the main file. + if (!error) { + // Delete the old file so we can replace it. + if (!file.delete()) { + Log.error("Error deleting property file: " + file.getAbsolutePath()); + return; + } + // Copy new contents to the file. + try { + copy(tempFile, file); + } + catch (Exception e) { + Log.error(e); + // There were errors so abort replacing the old property file. + error = true; + } + // If no errors, delete the temp file. + if (!error) { + tempFile.delete(); + } + } + } + + /** + * Returns an array representation of the given Jive property. Jive + * properties are always in the format "prop.name.is.this" which would be + * represented as an array of four Strings. + * + * @param name the name of the Jive property. + * @return an array representation of the given Jive property. + */ + private String[] parsePropertyName(String name) { + List propName = new ArrayList(5); + // Use a StringTokenizer to tokenize the property name. + StringTokenizer tokenizer = new StringTokenizer(name, "."); + while (tokenizer.hasMoreTokens()) { + propName.add(tokenizer.nextToken()); + } + return propName.toArray(new String[propName.size()]); + } + + public void setProperties(Map propertyMap) { + for (String propertyName : propertyMap.keySet()) { + String propertyValue = propertyMap.get(propertyName); + setProperty(propertyName, propertyValue); + } + } + + /** + * Copies the inFile to the outFile. + * + * @param inFile The file to copy from + * @param outFile The file to copy to + * @throws IOException If there was a problem making the copy + */ + private static void copy(File inFile, File outFile) throws IOException { + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(inFile); + fout = new FileOutputStream(outFile); + copy(fin, fout); + } + finally { + try { + if (fin != null) fin.close(); + } + catch (IOException e) { + // do nothing + } + try { + if (fout != null) fout.close(); + } + catch (IOException e) { + // do nothing + } + } + } + + /** + * Copies data from an input stream to an output stream + * + * @param in the stream to copy data from. + * @param out the stream to copy data to. + * @throws IOException if there's trouble during the copy. + */ + private static void copy(InputStream in, OutputStream out) throws IOException { + // Do not allow other threads to intrude on streams during copy. + synchronized (in) { + synchronized (out) { + byte[] buffer = new byte[256]; + while (true) { + int bytesRead = in.read(buffer); + if (bytesRead == -1) break; + out.write(buffer, 0, bytesRead); + } + } + } + } +} diff --git a/src/java/org/jivesoftware/util/XMLWriter.java b/src/java/org/jivesoftware/util/XMLWriter.java new file mode 100644 index 0000000..154b435 --- /dev/null +++ b/src/java/org/jivesoftware/util/XMLWriter.java @@ -0,0 +1,1500 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2006 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.util; + +import org.dom4j.*; +import org.dom4j.io.OutputFormat; +import org.dom4j.tree.NamespaceStack; +import org.xml.sax.*; +import org.xml.sax.ext.LexicalHandler; +import org.xml.sax.helpers.XMLFilterImpl; + +import java.io.*; +import java.util.*; + +/** + * Replacement class of the original XMLWriter.java (version: 1.77) since the original is still + * using StringBuffer which is not fast. + */ +public class XMLWriter extends XMLFilterImpl implements LexicalHandler { + + private static final String PAD_TEXT = " "; + + protected static final String[] LEXICAL_HANDLER_NAMES = { + "http://xml.org/sax/properties/lexical-handler", + "http://xml.org/sax/handlers/LexicalHandler" + }; + + protected static final OutputFormat DEFAULT_FORMAT = new OutputFormat(); + + /** Should entityRefs by resolved when writing ? */ + private boolean resolveEntityRefs = true; + + /** Stores the last type of node written so algorithms can refer to the + * previous node type */ + protected int lastOutputNodeType; + + /** Stores the xml:space attribute value of preserve for whitespace flag */ + protected boolean preserve=false; + + /** The Writer used to output to */ + protected Writer writer; + + /** The Stack of namespaceStack written so far */ + private NamespaceStack namespaceStack = new NamespaceStack(); + + /** The format used by this writer */ + private OutputFormat format; + + /** whether we should escape text */ + private boolean escapeText = true; + /** The initial number of indentations (so you can print a whole + document indented, if you like) **/ + private int indentLevel = 0; + + /** buffer used when escaping strings */ + private StringBuilder buffer = new StringBuilder(); + + /** whether we have added characters before from the same chunk of characters */ + private boolean charactersAdded = false; + private char lastChar; + + /** Whether a flush should occur after writing a document */ + private boolean autoFlush; + + /** Lexical handler we should delegate to */ + private LexicalHandler lexicalHandler; + + /** Whether comments should appear inside DTD declarations - defaults to false */ + private boolean showCommentsInDTDs; + + /** Is the writer curerntly inside a DTD definition? */ + private boolean inDTD; + + /** The namespaces used for the current element when consuming SAX events */ + private Map namespacesMap; + + /** + * what is the maximum allowed character code + * such as 127 in US-ASCII (7 bit) or 255 in ISO-* (8 bit) + * or -1 to not escape any characters (other than the special XML characters like < > &) + */ + private int maximumAllowedCharacter; + + public XMLWriter(Writer writer) { + this( writer, DEFAULT_FORMAT ); + } + + public XMLWriter(Writer writer, OutputFormat format) { + this.writer = writer; + this.format = format; + namespaceStack.push(Namespace.NO_NAMESPACE); + } + + public XMLWriter() { + this.format = DEFAULT_FORMAT; + this.writer = new BufferedWriter( new OutputStreamWriter( System.out ) ); + this.autoFlush = true; + namespaceStack.push(Namespace.NO_NAMESPACE); + } + + public XMLWriter(OutputStream out) throws UnsupportedEncodingException { + this.format = DEFAULT_FORMAT; + this.writer = createWriter(out, format.getEncoding()); + this.autoFlush = true; + namespaceStack.push(Namespace.NO_NAMESPACE); + } + + public XMLWriter(OutputStream out, OutputFormat format) throws UnsupportedEncodingException { + this.format = format; + this.writer = createWriter(out, format.getEncoding()); + this.autoFlush = true; + namespaceStack.push(Namespace.NO_NAMESPACE); + } + + public XMLWriter(OutputFormat format) throws UnsupportedEncodingException { + this.format = format; + this.writer = createWriter( System.out, format.getEncoding() ); + this.autoFlush = true; + namespaceStack.push(Namespace.NO_NAMESPACE); + } + + public void setWriter(Writer writer) { + this.writer = writer; + this.autoFlush = false; + } + + public void setOutputStream(OutputStream out) throws UnsupportedEncodingException { + this.writer = createWriter(out, format.getEncoding()); + this.autoFlush = true; + } + + /** + * @return true if text thats output should be escaped. + * This is enabled by default. It could be disabled if + * the output format is textual, like in XSLT where we can have + * xml, html or text output. + */ + public boolean isEscapeText() { + return escapeText; + } + + /** + * Sets whether text output should be escaped or not. + * This is enabled by default. It could be disabled if + * the output format is textual, like in XSLT where we can have + * xml, html or text output. + */ + public void setEscapeText(boolean escapeText) { + this.escapeText = escapeText; + } + + + /** Set the initial indentation level. This can be used to output + * a document (or, more likely, an element) starting at a given + * indent level, so it's not always flush against the left margin. + * Default: 0 + * + * @param indentLevel the number of indents to start with + */ + public void setIndentLevel(int indentLevel) { + this.indentLevel = indentLevel; + } + + /** + * Returns the maximum allowed character code that should be allowed + * unescaped which defaults to 127 in US-ASCII (7 bit) or + * 255 in ISO-* (8 bit). + */ + public int getMaximumAllowedCharacter() { + if (maximumAllowedCharacter == 0) { + maximumAllowedCharacter = defaultMaximumAllowedCharacter(); + } + return maximumAllowedCharacter; + } + + /** + * Sets the maximum allowed character code that should be allowed + * unescaped + * such as 127 in US-ASCII (7 bit) or 255 in ISO-* (8 bit) + * or -1 to not escape any characters (other than the special XML characters like < > &) + * + * If this is not explicitly set then it is defaulted from the encoding. + * + * @param maximumAllowedCharacter The maximumAllowedCharacter to set + */ + public void setMaximumAllowedCharacter(int maximumAllowedCharacter) { + this.maximumAllowedCharacter = maximumAllowedCharacter; + } + + /** Flushes the underlying Writer */ + public void flush() throws IOException { + writer.flush(); + } + + /** Closes the underlying Writer */ + public void close() throws IOException { + writer.close(); + } + + /** Writes the new line text to the underlying Writer */ + public void println() throws IOException { + writer.write( format.getLineSeparator() ); + } + + /** Writes the given {@link org.dom4j.Attribute}. + * + * @param attribute Attribute to output. + */ + public void write(Attribute attribute) throws IOException { + writeAttribute(attribute); + + if ( autoFlush ) { + flush(); + } + } + + + /**

This will print the Document to the current Writer.

+ * + *

Warning: using your own Writer may cause the writer's + * preferred character encoding to be ignored. If you use + * encodings other than UTF8, we recommend using the method that + * takes an OutputStream instead.

+ * + *

Note: as with all Writers, you may need to flush() yours + * after this method returns.

+ * + * @param doc Document to format. + * @throws IOException - if there's any problem writing. + **/ + public void write(Document doc) throws IOException { + writeDeclaration(); + + if (doc.getDocType() != null) { + indent(); + writeDocType(doc.getDocType()); + } + + for ( int i = 0, size = doc.nodeCount(); i < size; i++ ) { + Node node = doc.node(i); + writeNode( node ); + } + writePrintln(); + + if ( autoFlush ) { + flush(); + } + } + + /**

Writes the {@link org.dom4j.Element}, including + * its {@link Attribute}s, and its value, and all + * its content (child nodes) to the current Writer.

+ * + * @param element Element to output. + */ + public void write(Element element) throws IOException { + writeElement(element); + + if ( autoFlush ) { + flush(); + } + } + + + /** Writes the given {@link CDATA}. + * + * @param cdata CDATA to output. + */ + public void write(CDATA cdata) throws IOException { + writeCDATA( cdata.getText() ); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given {@link Comment}. + * + * @param comment Comment to output. + */ + public void write(Comment comment) throws IOException { + writeComment( comment.getText() ); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given {@link DocumentType}. + * + * @param docType DocumentType to output. + */ + public void write(DocumentType docType) throws IOException { + writeDocType(docType); + + if ( autoFlush ) { + flush(); + } + } + + + /** Writes the given {@link Entity}. + * + * @param entity Entity to output. + */ + public void write(Entity entity) throws IOException { + writeEntity( entity ); + + if ( autoFlush ) { + flush(); + } + } + + + /** Writes the given {@link Namespace}. + * + * @param namespace Namespace to output. + */ + public void write(Namespace namespace) throws IOException { + writeNamespace(namespace); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given {@link ProcessingInstruction}. + * + * @param processingInstruction ProcessingInstruction to output. + */ + public void write(ProcessingInstruction processingInstruction) throws IOException { + writeProcessingInstruction(processingInstruction); + + if ( autoFlush ) { + flush(); + } + } + + /**

Print out a {@link String}, Perfoms + * the necessary entity escaping and whitespace stripping.

+ * + * @param text is the text to output + */ + public void write(String text) throws IOException { + writeString(text); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given {@link Text}. + * + * @param text Text to output. + */ + public void write(Text text) throws IOException { + writeString(text.getText()); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given {@link Node}. + * + * @param node Node to output. + */ + public void write(Node node) throws IOException { + writeNode(node); + + if ( autoFlush ) { + flush(); + } + } + + /** Writes the given object which should be a String, a Node or a List + * of Nodes. + * + * @param object is the object to output. + */ + public void write(Object object) throws IOException { + if (object instanceof Node) { + write((Node) object); + } + else if (object instanceof String) { + write((String) object); + } + else if (object instanceof List) { + List list = (List) object; + for ( int i = 0, size = list.size(); i < size; i++ ) { + write( list.get(i) ); + } + } + else if (object != null) { + throw new IOException( "Invalid object: " + object ); + } + } + + + /**

Writes the opening tag of an {@link Element}, + * including its {@link Attribute}s + * but without its content.

+ * + * @param element Element to output. + */ + public void writeOpen(Element element) throws IOException { + writer.write("<"); + writer.write( element.getQualifiedName() ); + writeAttributes(element); + writer.write(">"); + } + + /**

Writes the closing tag of an {@link Element}

+ * + * @param element Element to output. + */ + public void writeClose(Element element) throws IOException { + writeClose( element.getQualifiedName() ); + } + + + // XMLFilterImpl methods + //------------------------------------------------------------------------- + public void parse(InputSource source) throws IOException, SAXException { + installLexicalHandler(); + super.parse(source); + } + + + public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) { + if (LEXICAL_HANDLER_NAMES[i].equals(name)) { + setLexicalHandler((LexicalHandler) value); + return; + } + } + super.setProperty(name, value); + } + + public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) { + if (LEXICAL_HANDLER_NAMES[i].equals(name)) { + return getLexicalHandler(); + } + } + return super.getProperty(name); + } + + public void setLexicalHandler (LexicalHandler handler) { + if (handler == null) { + throw new NullPointerException("Null lexical handler"); + } + else { + this.lexicalHandler = handler; + } + } + + public LexicalHandler getLexicalHandler(){ + return lexicalHandler; + } + + + // ContentHandler interface + //------------------------------------------------------------------------- + public void setDocumentLocator(Locator locator) { + super.setDocumentLocator(locator); + } + + public void startDocument() throws SAXException { + try { + writeDeclaration(); + super.startDocument(); + } + catch (IOException e) { + handleException(e); + } + } + + public void endDocument() throws SAXException { + super.endDocument(); + + if ( autoFlush ) { + try { + flush(); + } catch ( IOException e) {} + } + } + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + if ( namespacesMap == null ) { + namespacesMap = new HashMap(); + } + namespacesMap.put(prefix, uri); + super.startPrefixMapping(prefix, uri); + } + + public void endPrefixMapping(String prefix) throws SAXException { + super.endPrefixMapping(prefix); + } + + public void startElement(String namespaceURI, String localName, String qName, Attributes attributes) throws SAXException { + try { + charactersAdded = false; + + writePrintln(); + indent(); + writer.write("<"); + writer.write(qName); + writeNamespaces(); + writeAttributes( attributes ); + writer.write(">"); + ++indentLevel; + lastOutputNodeType = Node.ELEMENT_NODE; + + super.startElement( namespaceURI, localName, qName, attributes ); + } + catch (IOException e) { + handleException(e); + } + } + + public void endElement(String namespaceURI, String localName, String qName) throws SAXException { + try { + charactersAdded = false; + --indentLevel; + if ( lastOutputNodeType == Node.ELEMENT_NODE ) { + writePrintln(); + indent(); + } + + // XXXX: need to determine this using a stack and checking for + // content / children + boolean hadContent = true; + if ( hadContent ) { + writeClose(qName); + } + else { + writeEmptyElementClose(qName); + } + lastOutputNodeType = Node.ELEMENT_NODE; + + super.endElement( namespaceURI, localName, qName ); + } + catch (IOException e) { + handleException(e); + } + } + + public void characters(char[] ch, int start, int length) throws SAXException { + if (ch == null || ch.length == 0 || length <= 0) { + return; + } + + try { + /* + * we can't use the writeString method here because it's possible + * we don't receive all characters at once and calling writeString + * would cause unwanted spaces to be added in between these chunks + * of character arrays. + */ + String string = new String(ch, start, length); + + if (escapeText) { + string = escapeElementEntities(string); + } + + if (format.isTrimText()) { + if ((lastOutputNodeType == Node.TEXT_NODE) && !charactersAdded) { + writer.write(" "); + } else if (charactersAdded && Character.isWhitespace(lastChar)) { + writer.write(lastChar); + } + + String delim = ""; + StringTokenizer tokens = new StringTokenizer(string); + while (tokens.hasMoreTokens()) { + writer.write(delim); + writer.write(tokens.nextToken()); + delim = " "; + } + } else { + writer.write(string); + } + + charactersAdded = true; + lastChar = ch[start + length - 1]; + lastOutputNodeType = Node.TEXT_NODE; + + super.characters(ch, start, length); + } + catch (IOException e) { + handleException(e); + } + } + + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + super.ignorableWhitespace(ch, start, length); + } + + public void processingInstruction(String target, String data) throws SAXException { + try { + indent(); + writer.write(""); + writePrintln(); + lastOutputNodeType = Node.PROCESSING_INSTRUCTION_NODE; + + super.processingInstruction(target, data); + } + catch (IOException e) { + handleException(e); + } + } + + + + // DTDHandler interface + //------------------------------------------------------------------------- + public void notationDecl(String name, String publicID, String systemID) throws SAXException { + super.notationDecl(name, publicID, systemID); + } + + public void unparsedEntityDecl(String name, String publicID, String systemID, String notationName) throws SAXException { + super.unparsedEntityDecl(name, publicID, systemID, notationName); + } + + + // LexicalHandler interface + //------------------------------------------------------------------------- + public void startDTD(String name, String publicID, String systemID) throws SAXException { + inDTD = true; + try { + writeDocType(name, publicID, systemID); + } + catch (IOException e) { + handleException(e); + } + + if (lexicalHandler != null) { + lexicalHandler.startDTD(name, publicID, systemID); + } + } + + public void endDTD() throws SAXException { + inDTD = false; + if (lexicalHandler != null) { + lexicalHandler.endDTD(); + } + } + + public void startCDATA() throws SAXException { + try { + writer.write( "" ); + } + catch (IOException e) { + handleException(e); + } + + if (lexicalHandler != null) { + lexicalHandler.endCDATA(); + } + } + + public void startEntity(String name) throws SAXException { + try { + writeEntityRef(name); + } + catch (IOException e) { + handleException(e); + } + + if (lexicalHandler != null) { + lexicalHandler.startEntity(name); + } + } + + public void endEntity(String name) throws SAXException { + if (lexicalHandler != null) { + lexicalHandler.endEntity(name); + } + } + + public void comment(char[] ch, int start, int length) throws SAXException { + if ( showCommentsInDTDs || ! inDTD ) { + try { + charactersAdded = false; + writeComment( new String(ch, start, length) ); + } + catch (IOException e) { + handleException(e); + } + } + + if (lexicalHandler != null) { + lexicalHandler.comment(ch, start, length); + } + } + + + + // Implementation methods + //------------------------------------------------------------------------- + protected void writeElement(Element element) throws IOException { + int size = element.nodeCount(); + String qualifiedName = element.getQualifiedName(); + + writePrintln(); + indent(); + + writer.write("<"); + writer.write(qualifiedName); + + int previouslyDeclaredNamespaces = namespaceStack.size(); + Namespace ns = element.getNamespace(); + if (isNamespaceDeclaration( ns ) ) { + namespaceStack.push(ns); + writeNamespace(ns); + } + + // Print out additional namespace declarations + boolean textOnly = true; + for ( int i = 0; i < size; i++ ) { + Node node = element.node(i); + if ( node instanceof Namespace ) { + Namespace additional = (Namespace) node; + if (isNamespaceDeclaration( additional ) ) { + namespaceStack.push(additional); + writeNamespace(additional); + } + } + else if ( node instanceof Element) { + textOnly = false; + } + else if ( node instanceof Comment) { + textOnly = false; + } + } + + writeAttributes(element); + + lastOutputNodeType = Node.ELEMENT_NODE; + + if ( size <= 0 ) { + writeEmptyElementClose(qualifiedName); + } + else { + writer.write(">"); + if ( textOnly ) { + // we have at least one text node so lets assume + // that its non-empty + writeElementContent(element); + } + else { + // we know it's not null or empty from above + ++indentLevel; + + writeElementContent(element); + + --indentLevel; + + writePrintln(); + indent(); + } + writer.write(""); + } + + // remove declared namespaceStack from stack + while (namespaceStack.size() > previouslyDeclaredNamespaces) { + namespaceStack.pop(); + } + + lastOutputNodeType = Node.ELEMENT_NODE; + } + + /** + * Determines if element is a special case of XML elements + * where it contains an xml:space attribute of "preserve". + * If it does, then retain whitespace. + */ + protected final boolean isElementSpacePreserved(Element element) { + final Attribute attr = (Attribute)element.attribute("space"); + boolean preserveFound=preserve; //default to global state + if (attr!=null) { + if ("xml".equals(attr.getNamespacePrefix()) && + "preserve".equals(attr.getText())) { + preserveFound = true; + } + else { + preserveFound = false; + } + } + return preserveFound; + } + /** Outputs the content of the given element. If whitespace trimming is + * enabled then all adjacent text nodes are appended together before + * the whitespace trimming occurs to avoid problems with multiple + * text nodes being created due to text content that spans parser buffers + * in a SAX parser. + */ + protected void writeElementContent(Element element) throws IOException { + boolean trim = format.isTrimText(); + boolean oldPreserve=preserve; + if (trim) { //verify we have to before more expensive test + preserve=isElementSpacePreserved(element); + trim = !preserve; + } + if (trim) { + // concatenate adjacent text nodes together + // so that whitespace trimming works properly + Text lastTextNode = null; + StringBuilder buffer = null; + boolean textOnly = true; + for ( int i = 0, size = element.nodeCount(); i < size; i++ ) { + Node node = element.node(i); + if ( node instanceof Text ) { + if ( lastTextNode == null ) { + lastTextNode = (Text) node; + } + else { + if (buffer == null) { + buffer = new StringBuilder( lastTextNode.getText() ); + } + buffer.append( ((Text) node).getText() ); + } + } + else { + if (!textOnly && format.isPadText()) { + writer.write(PAD_TEXT); + } + + textOnly = false; + + if ( lastTextNode != null ) { + if ( buffer != null ) { + writeString( buffer.toString() ); + buffer = null; + } + else { + writeString( lastTextNode.getText() ); + } + lastTextNode = null; + + if (format.isPadText()) { + writer.write(PAD_TEXT); + } + } + writeNode(node); + } + } + if ( lastTextNode != null ) { + if (!textOnly && format.isPadText()) { + writer.write(PAD_TEXT); + } + if ( buffer != null ) { + writeString( buffer.toString() ); + buffer = null; + } + else { + writeString( lastTextNode.getText() ); + } + lastTextNode = null; + } + } + else { + Node lastTextNode = null; + for ( int i = 0, size = element.nodeCount(); i < size; i++ ) { + Node node = element.node(i); + if (node instanceof Text) { + writeNode(node); + lastTextNode = node; + } else { + if ((lastTextNode != null) && format.isPadText()) { + writer.write(PAD_TEXT); + } + writeNode(node); + if ((lastTextNode != null) && format.isPadText()) { + writer.write(PAD_TEXT); + } + lastTextNode = null; + } + } + } + preserve=oldPreserve; + } + protected void writeCDATA(String text) throws IOException { + writer.write( "" ); + + lastOutputNodeType = Node.CDATA_SECTION_NODE; + } + + protected void writeDocType(DocumentType docType) throws IOException { + if (docType != null) { + docType.write( writer ); + //writeDocType( docType.getElementName(), docType.getPublicID(), docType.getSystemID() ); + writePrintln(); + } + } + + + protected void writeNamespace(Namespace namespace) throws IOException { + if ( namespace != null ) { + writeNamespace(namespace.getPrefix(), namespace.getURI()); + } + } + + /** + * Writes the SAX namepsaces + */ + protected void writeNamespaces() throws IOException { + if ( namespacesMap != null ) { + for ( Iterator iter = namespacesMap.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = (Map.Entry) iter.next(); + String prefix = (String) entry.getKey(); + String uri = (String) entry.getValue(); + writeNamespace(prefix, uri); + } + namespacesMap = null; + } + } + + /** + * Writes the SAX namepsaces + */ + protected void writeNamespace(String prefix, String uri) throws IOException { + if ( prefix != null && prefix.length() > 0 ) { + writer.write(" xmlns:"); + writer.write(prefix); + writer.write("=\""); + } + else { + writer.write(" xmlns=\""); + } + writer.write(uri); + writer.write("\""); + } + + protected void writeProcessingInstruction(ProcessingInstruction processingInstruction) throws IOException { + //indent(); + writer.write( "" ); + writePrintln(); + + lastOutputNodeType = Node.PROCESSING_INSTRUCTION_NODE; + } + + protected void writeString(String text) throws IOException { + if ( text != null && text.length() > 0 ) { + if ( escapeText ) { + text = escapeElementEntities(text); + } + +// if (format.isPadText()) { +// if (lastOutputNodeType == Node.ELEMENT_NODE) { +// writer.write(PAD_TEXT); +// } +// } + + if (format.isTrimText()) { + boolean first = true; + StringTokenizer tokenizer = new StringTokenizer(text); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if ( first ) { + first = false; + if ( lastOutputNodeType == Node.TEXT_NODE ) { + writer.write(" "); + } + } + else { + writer.write(" "); + } + writer.write(token); + lastOutputNodeType = Node.TEXT_NODE; + } + } + else { + lastOutputNodeType = Node.TEXT_NODE; + writer.write(text); + } + } + } + + /** + * This method is used to write out Nodes that contain text + * and still allow for xml:space to be handled properly. + * + */ + protected void writeNodeText(Node node) throws IOException { + String text = node.getText(); + if (text != null && text.length() > 0) { + if (escapeText) { + text = escapeElementEntities(text); + } + + lastOutputNodeType = Node.TEXT_NODE; + writer.write(text); + } + } + + protected void writeNode(Node node) throws IOException { + int nodeType = node.getNodeType(); + switch (nodeType) { + case Node.ELEMENT_NODE: + writeElement((Element) node); + break; + case Node.ATTRIBUTE_NODE: + writeAttribute((Attribute) node); + break; + case Node.TEXT_NODE: + writeNodeText(node); + //write((Text) node); + break; + case Node.CDATA_SECTION_NODE: + writeCDATA(node.getText()); + break; + case Node.ENTITY_REFERENCE_NODE: + writeEntity((Entity) node); + break; + case Node.PROCESSING_INSTRUCTION_NODE: + writeProcessingInstruction((ProcessingInstruction) node); + break; + case Node.COMMENT_NODE: + writeComment(node.getText()); + break; + case Node.DOCUMENT_NODE: + write((Document) node); + break; + case Node.DOCUMENT_TYPE_NODE: + writeDocType((DocumentType) node); + break; + case Node.NAMESPACE_NODE: + // Will be output with attributes + //write((Namespace) node); + break; + default: + throw new IOException( "Invalid node type: " + node ); + } + } + + + + + protected void installLexicalHandler() { + XMLReader parent = getParent(); + if (parent == null) { + throw new NullPointerException("No parent for filter"); + } + // try to register for lexical events + for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) { + try { + parent.setProperty(LEXICAL_HANDLER_NAMES[i], this); + break; + } + catch (SAXNotRecognizedException ex) { + // ignore + } + catch (SAXNotSupportedException ex) { + // ignore + } + } + } + + protected void writeDocType(String name, String publicID, String systemID) throws IOException { + boolean hasPublic = false; + + writer.write(""); + writePrintln(); + } + + protected void writeEntity(Entity entity) throws IOException { + if (!resolveEntityRefs()) { + writeEntityRef( entity.getName() ); + } else { + writer.write(entity.getText()); + } + } + + protected void writeEntityRef(String name) throws IOException { + writer.write( "&" ); + writer.write( name ); + writer.write( ";" ); + + lastOutputNodeType = Node.ENTITY_REFERENCE_NODE; + } + + protected void writeComment(String text) throws IOException { + if (format.isNewlines()) { + println(); + indent(); + } + writer.write( "" ); + + lastOutputNodeType = Node.COMMENT_NODE; + } + + /** Writes the attributes of the given element + * + */ + protected void writeAttributes( Element element ) throws IOException { + + // I do not yet handle the case where the same prefix maps to + // two different URIs. For attributes on the same element + // this is illegal; but as yet we don't throw an exception + // if someone tries to do this + for ( int i = 0, size = element.attributeCount(); i < size; i++ ) { + Attribute attribute = element.attribute(i); + Namespace ns = attribute.getNamespace(); + if (ns != null && ns != Namespace.NO_NAMESPACE && ns != Namespace.XML_NAMESPACE) { + String prefix = ns.getPrefix(); + String uri = namespaceStack.getURI(prefix); + if (!ns.getURI().equals(uri)) { // output a new namespace declaration + writeNamespace(ns); + namespaceStack.push(ns); + } + } + + // If the attribute is a namespace declaration, check if we have already + // written that declaration elsewhere (if that's the case, it must be + // in the namespace stack + String attName = attribute.getName(); + if (attName.startsWith("xmlns:")) { + String prefix = attName.substring(6); + if (namespaceStack.getNamespaceForPrefix(prefix) == null) { + String uri = attribute.getValue(); + namespaceStack.push(prefix, uri); + writeNamespace(prefix, uri); + } + } else if (attName.equals("xmlns")) { + if (namespaceStack.getDefaultNamespace() == null) { + String uri = attribute.getValue(); + namespaceStack.push(null, uri); + writeNamespace(null, uri); + } + } else { + char quote = format.getAttributeQuoteCharacter(); + writer.write(" "); + writer.write(attribute.getQualifiedName()); + writer.write("="); + writer.write(quote); + writeEscapeAttributeEntities(attribute.getValue()); + writer.write(quote); + } + } + } + + protected void writeAttribute(Attribute attribute) throws IOException { + writer.write(" "); + writer.write(attribute.getQualifiedName()); + writer.write("="); + + char quote = format.getAttributeQuoteCharacter(); + writer.write(quote); + + writeEscapeAttributeEntities(attribute.getValue()); + + writer.write(quote); + lastOutputNodeType = Node.ATTRIBUTE_NODE; + } + + protected void writeAttributes(Attributes attributes) throws IOException { + for (int i = 0, size = attributes.getLength(); i < size; i++) { + writeAttribute( attributes, i ); + } + } + + protected void writeAttribute(Attributes attributes, int index) throws IOException { + char quote = format.getAttributeQuoteCharacter(); + writer.write(" "); + writer.write(attributes.getQName(index)); + writer.write("="); + writer.write(quote); + writeEscapeAttributeEntities(attributes.getValue(index)); + writer.write(quote); + } + + + + protected void indent() throws IOException { + String indent = format.getIndent(); + if ( indent != null && indent.length() > 0 ) { + for ( int i = 0; i < indentLevel; i++ ) { + writer.write(indent); + } + } + } + + /** + *

+ * This will print a new line only if the newlines flag was set to true + *

+ */ + protected void writePrintln() throws IOException { + if (format.isNewlines()) { + writer.write( format.getLineSeparator() ); + } + } + + /** + * Get an OutputStreamWriter, use preferred encoding. + */ + protected Writer createWriter(OutputStream outStream, String encoding) throws UnsupportedEncodingException { + return new BufferedWriter( + new OutputStreamWriter( outStream, encoding ) + ); + } + + /** + *

+ * This will write the declaration to the given Writer. + * Assumes XML version 1.0 since we don't directly know. + *

+ */ + protected void writeDeclaration() throws IOException { + String encoding = format.getEncoding(); + + // Only print of declaration is not suppressed + if (! format.isSuppressDeclaration()) { + // Assume 1.0 version + if (encoding.equals("UTF8")) { + writer.write(""); + } else { + writer.write(""); + } + if (format.isNewLineAfterDeclaration()) { + println(); + } + } + } + + protected void writeClose(String qualifiedName) throws IOException { + writer.write(""); + } + + protected void writeEmptyElementClose(String qualifiedName) throws IOException { + // Simply close up + if (! format.isExpandEmptyElements()) { + writer.write("/>"); + } else { + writer.write(">"); + } + } + + protected boolean isExpandEmptyElements() { + return format.isExpandEmptyElements(); + } + + + /** This will take the pre-defined entities in XML 1.0 and + * convert their character representation to the appropriate + * entity reference, suitable for XML attributes. + */ + protected String escapeElementEntities(String text) { + char[] block = null; + int i, last = 0, size = text.length(); + for ( i = 0; i < size; i++ ) { + String entity = null; + char c = text.charAt(i); + switch( c ) { + case '<' : + entity = "<"; + break; + case '>' : + entity = ">"; + break; + case '&' : + entity = "&"; + break; + case '\t': case '\n': case '\r': + // don't encode standard whitespace characters + if (preserve) { + entity=String.valueOf(c); + } + break; + default: + if (c < 32 || shouldEncodeChar(c)) { + entity = "&#" + (int) c + ";"; + } + break; + } + if (entity != null) { + if ( block == null ) { + block = text.toCharArray(); + } + buffer.append(block, last, i - last); + buffer.append(entity); + last = i + 1; + } + } + if ( last == 0 ) { + return text; + } + if ( last < size ) { + if ( block == null ) { + block = text.toCharArray(); + } + buffer.append(block, last, i - last); + } + String answer = buffer.toString(); + buffer.setLength(0); + return answer; + } + + + protected void writeEscapeAttributeEntities(String text) throws IOException { + if ( text != null ) { + String escapedText = escapeAttributeEntities( text ); + writer.write( escapedText ); + } + } + /** This will take the pre-defined entities in XML 1.0 and + * convert their character representation to the appropriate + * entity reference, suitable for XML attributes. + */ + protected String escapeAttributeEntities(String text) { + char quote = format.getAttributeQuoteCharacter(); + + char[] block = null; + int i, last = 0, size = text.length(); + for ( i = 0; i < size; i++ ) { + String entity = null; + char c = text.charAt(i); + switch( c ) { + case '<' : + entity = "<"; + break; + case '>' : + entity = ">"; + break; + case '\'' : + if (quote == '\'') { + entity = "'"; + } + break; + case '\"' : + if (quote == '\"') { + entity = """; + } + break; + case '&' : + entity = "&"; + break; + case '\t': case '\n': case '\r': + // don't encode standard whitespace characters + break; + default: + if (c < 32 || shouldEncodeChar(c)) { + entity = "&#" + (int) c + ";"; + } + break; + } + if (entity != null) { + if ( block == null ) { + block = text.toCharArray(); + } + buffer.append(block, last, i - last); + buffer.append(entity); + last = i + 1; + } + } + if ( last == 0 ) { + return text; + } + if ( last < size ) { + if ( block == null ) { + block = text.toCharArray(); + } + buffer.append(block, last, i - last); + } + String answer = buffer.toString(); + buffer.setLength(0); + return answer; + } + + /** + * Should the given character be escaped. This depends on the + * encoding of the document. + * + * @return boolean + */ + protected boolean shouldEncodeChar(char c) { + int max = getMaximumAllowedCharacter(); + return max > 0 && c > max; + } + + /** + * Returns the maximum allowed character code that should be allowed + * unescaped which defaults to 127 in US-ASCII (7 bit) or + * 255 in ISO-* (8 bit). + */ + protected int defaultMaximumAllowedCharacter() { + String encoding = format.getEncoding(); + if (encoding != null) { + if (encoding.equals("US-ASCII")) { + return 127; + } + } + // no encoding for things like ISO-*, UTF-8 or UTF-16 + return -1; + } + + protected boolean isNamespaceDeclaration( Namespace ns ) { + if (ns != null && ns != Namespace.XML_NAMESPACE) { + String uri = ns.getURI(); + if ( uri != null ) { + if ( ! namespaceStack.contains( ns ) ) { + return true; + + } + } + } + return false; + + } + + protected void handleException(IOException e) throws SAXException { + throw new SAXException(e); + } + + //Laramie Crocker 4/8/2002 10:38AM + /** Lets subclasses get at the current format object, so they can call setTrimText, setNewLines, etc. + * Put in to support the HTMLWriter, in the way + * that it pushes the current newline/trim state onto a stack and overrides + * the state within preformatted tags. + */ + protected OutputFormat getOutputFormat() { + return format; + } + + public boolean resolveEntityRefs() { + return resolveEntityRefs; + } + + public void setResolveEntityRefs(boolean resolve) { + this.resolveEntityRefs = resolve; + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/log/ContextMap.java b/src/java/org/jivesoftware/util/log/ContextMap.java new file mode 100644 index 0000000..756b182 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/ContextMap.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The ContextMap contains non-hierarchical context information + * relevent to a particular LogEvent. It may include information + * such as; + *

+ *

    + *
  • user ->fred
  • + *
  • hostname ->helm.realityforge.org
  • + *
  • ipaddress ->1.2.3.4
  • + *
  • interface ->127.0.0.1
  • + *
  • caller ->com.biz.MyCaller.method(MyCaller.java:18)
  • + *
  • source ->1.6.3.2:33
  • + *
+ * The context is bound to a thread (and inherited by sub-threads) but + * it can also be added to by LogTargets. + * + * @author Peter Donald + */ +public final class ContextMap implements Serializable { + ///Thread local for holding instance of map associated with current thread + private static final ThreadLocal c_context = new InheritableThreadLocal(); + + private final ContextMap m_parent; + + ///Container to hold map of elements + private Map m_map = Collections.synchronizedMap(new HashMap()); + + ///Flag indicating whether this map should be readonly + private transient boolean m_readOnly; + + /** + * Get the Current ContextMap. + * This method returns a ContextMap associated with current thread. If the + * thread doesn't have a ContextMap associated with it then a new + * ContextMap is created. + * + * @return the current ContextMap + */ + public final static ContextMap getCurrentContext() { + return getCurrentContext(true); + } + + /** + * Get the Current ContextMap. + * This method returns a ContextMap associated with current thread. + * If the thread doesn't have a ContextMap associated with it and + * autocreate is true then a new ContextMap is created. + * + * @param autocreate true if a ContextMap is to be created if it doesn't exist + * @return the current ContextMap + */ + public final static ContextMap getCurrentContext(final boolean autocreate) { + //Check security permission here??? + ContextMap context = (ContextMap)c_context.get(); + + if (null == context && autocreate) { + context = new ContextMap(); + c_context.set(context); + } + + return context; + } + + /** + * Bind a particular ContextMap to current thread. + * + * @param context the context map (may be null) + */ + public final static void bind(final ContextMap context) { + //Check security permission here?? + c_context.set(context); + } + + /** + * Default constructor. + */ + public ContextMap() { + this(null); + } + + /** + * Constructor that sets parent contextMap. + * + * @param parent the parent ContextMap + */ + public ContextMap(final ContextMap parent) { + m_parent = parent; + } + + /** + * Make the context read-only. + * This makes it safe to allow untrusted code reference + * to ContextMap. + */ + public void makeReadOnly() { + m_readOnly = true; + } + + /** + * Determine if context is read-only. + * + * @return true if Context is read only, false otherwise + */ + public boolean isReadOnly() { + return m_readOnly; + } + + /** + * Empty the context map. + */ + public void clear() { + checkReadable(); + + m_map.clear(); + } + + /** + * Get an entry from the context. + * + * @param key the key to map + * @param defaultObject a default object to return if key does not exist + * @return the object in context + */ + public Object get(final String key, final Object defaultObject) { + final Object object = get(key); + + if (null != object) + return object; + else + return defaultObject; + } + + /** + * Get an entry from the context. + * + * @param key the key to map + * @return the object in context or null if none with specified key + */ + public Object get(final String key) { + final Object result = m_map.get(key); + + if (null == result && null != m_parent) { + return m_parent.get(key); + } + + return result; + } + + /** + * Set a value in context + * + * @param key the key + * @param value the value (may be null) + */ + public void set(final String key, final Object value) { + checkReadable(); + + if (value == null) { + m_map.remove(key); + } + else { + m_map.put(key, value); + } + } + + + /** + * Get the number of contexts in map. + * + * @return the number of contexts in map + */ + public int getSize() { + return m_map.size(); + } + + /** + * Helper method that sets context to read-only after de-serialization. + * + * @return the corrected object version + * @throws ObjectStreamException if an error occurs + */ + private Object readResolve() throws ObjectStreamException { + makeReadOnly(); + return this; + } + + /** + * Utility method to verify that Context is read-only. + */ + private void checkReadable() { + if (isReadOnly()) { + throw new IllegalStateException("ContextMap is read only and can not be modified"); + } + } +} diff --git a/src/java/org/jivesoftware/util/log/ErrorAware.java b/src/java/org/jivesoftware/util/log/ErrorAware.java new file mode 100644 index 0000000..e93eaaf --- /dev/null +++ b/src/java/org/jivesoftware/util/log/ErrorAware.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +/** + * Interface implemented by components that wish to + * delegate ErrorHandling to an ErrorHandler. + * + * @author Peter Donald + */ +public interface ErrorAware { + /** + * Provide component with ErrorHandler. + * + * @param errorHandler the errorHandler + */ + void setErrorHandler(ErrorHandler errorHandler); +} diff --git a/src/java/org/jivesoftware/util/log/ErrorHandler.java b/src/java/org/jivesoftware/util/log/ErrorHandler.java new file mode 100644 index 0000000..30cb10f --- /dev/null +++ b/src/java/org/jivesoftware/util/log/ErrorHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +/** + * Handle unrecoverable errors that occur during logging. + * Based on Log4js notion of ErrorHandlers. + * + * @author Peter Donald + */ +public interface ErrorHandler { + /** + * Log an unrecoverable error. + * + * @param message the error message + * @param throwable the exception associated with error (may be null) + * @param event the LogEvent that caused error, if any (may be null) + */ + void error(String message, Throwable throwable, LogEvent event); +} diff --git a/src/java/org/jivesoftware/util/log/FilterTarget.java b/src/java/org/jivesoftware/util/log/FilterTarget.java new file mode 100644 index 0000000..4ecad45 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/FilterTarget.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + + + +/** + * A Log target which will do filtering and then pass it + * onto targets further along in chain. + *

+ *

Filtering can mena that not all LogEvents get passed + * along chain or that the LogEvents passed alongare modified + * in some manner.

+ * + * @author Peter Donald + */ +public interface FilterTarget extends LogTarget { + + /** + * Add a target to output chain. + * + * @param target the log target + */ + void addTarget(LogTarget target); +} diff --git a/src/java/org/jivesoftware/util/log/Hierarchy.java b/src/java/org/jivesoftware/util/log/Hierarchy.java new file mode 100644 index 0000000..3a1989f --- /dev/null +++ b/src/java/org/jivesoftware/util/log/Hierarchy.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +import org.jivesoftware.util.log.format.PatternFormatter; +import org.jivesoftware.util.log.output.io.StreamTarget; +import org.jivesoftware.util.log.util.DefaultErrorHandler; + +/** + * This class encapsulates a basic independent log hierarchy. + * The hierarchy is essentially a safe wrapper around root logger. + * + * @author Peter Donald + */ +public class Hierarchy { + ///Format of default formatter + private static final String FORMAT = + "%7.7{priority} %5.5{time} [%8.8{category}] (%{context}): %{message}\\n%{throwable}"; + + ///The instance of default hierarchy + private static final Hierarchy c_hierarchy = new Hierarchy(); + + ///Error Handler associated with hierarchy + private ErrorHandler m_errorHandler; + + ///The root logger which contains all Loggers in this hierarchy + private Logger m_rootLogger; + + /** + * Retrieve the default hierarchy. + *

+ *

In most cases the default LogHierarchy is the only + * one used in an application. However when security is + * a concern or multiple independent applications will + * be running in same JVM it is advantageous to create + * new Hierarchies rather than reuse default.

+ * + * @return the default Hierarchy + */ + public static Hierarchy getDefaultHierarchy() { + return c_hierarchy; + } + + /** + * Create a hierarchy object. + * The default LogTarget writes to stdout. + */ + public Hierarchy() { + m_errorHandler = new DefaultErrorHandler(); + m_rootLogger = new Logger(new InnerErrorHandler(), "", null, null); + + //Setup default output target to print to console + final PatternFormatter formatter = new PatternFormatter(FORMAT); + final StreamTarget target = new StreamTarget(System.out, formatter); + + setDefaultLogTarget(target); + } + + /** + * Set the default log target for hierarchy. + * This is the target inherited by loggers if no other target is specified. + * + * @param target the default target + */ + public void setDefaultLogTarget(final LogTarget target) { + if (null == target) { + throw new IllegalArgumentException("Can not set DefaultLogTarget to null"); + } + + final LogTarget[] targets = new LogTarget[]{target}; + getRootLogger().setLogTargets(targets); + } + + /** + * Set the default log targets for this hierarchy. + * These are the targets inherited by loggers if no other targets are specified + * + * @param targets the default targets + */ + public void setDefaultLogTargets(final LogTarget[] targets) { + if (null == targets || 0 == targets.length) { + throw new IllegalArgumentException("Can not set DefaultLogTargets to null"); + } + + for (int i = 0; i < targets.length; i++) { + if (null == targets[i]) { + throw new IllegalArgumentException("Can not set DefaultLogTarget element to null"); + } + } + + getRootLogger().setLogTargets(targets); + } + + /** + * Set the default priority for hierarchy. + * This is the priority inherited by loggers if no other priority is specified. + * + * @param priority the default priority + */ + public void setDefaultPriority(final Priority priority) { + if (null == priority) { + throw new IllegalArgumentException("Can not set default Hierarchy Priority to null"); + } + + getRootLogger().setPriority(priority); + } + + /** + * Set the ErrorHandler associated with hierarchy. + * + * @param errorHandler the ErrorHandler + */ + public void setErrorHandler(final ErrorHandler errorHandler) { + if (null == errorHandler) { + throw new IllegalArgumentException("Can not set default Hierarchy ErrorHandler to null"); + } + + m_errorHandler = errorHandler; + } + + /** + * Retrieve a logger for named category. + * + * @param category the context + * @return the Logger + */ + public Logger getLoggerFor(final String category) { + return getRootLogger().getChildLogger(category); + } + +// /** +// * Logs an error message to error handler. +// * Default Error Handler is stderr. +// * +// * @param message a message to log +// * @param throwable a Throwable to log +// * @deprecated Logging components should use ErrorHandler rather than Hierarchy.log() +// */ +// public void log(final String message, final Throwable throwable) { +// m_errorHandler.error(message, throwable, null); +// } +// +// /** +// * Logs an error message to error handler. +// * Default Error Handler is stderr. +// * +// * @param message a message to log +// * @deprecated Logging components should use ErrorHandler rather than Hierarchy.log() +// */ +// public void log(final String message) { +// log(message, null); +// } + + private class InnerErrorHandler + implements ErrorHandler { + /** + * Log an unrecoverable error. + * + * @param message the error message + * @param throwable the exception associated with error (may be null) + * @param event the LogEvent that caused error, if any (may be null) + */ + public void error(final String message, + final Throwable throwable, + final LogEvent event) { + m_errorHandler.error(message, throwable, event); + } + } + + /** + * Utility method to retrieve logger for hierarchy. + * This method is intended for use by sub-classes + * which can take responsibility for manipulating + * Logger directly. + * + * @return the Logger + */ + protected final Logger getRootLogger() { + return m_rootLogger; + } +} diff --git a/src/java/org/jivesoftware/util/log/LogEvent.java b/src/java/org/jivesoftware/util/log/LogEvent.java new file mode 100644 index 0000000..79e4610 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/LogEvent.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ + +package org.jivesoftware.util.log; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * This class encapsulates each individual log event. + * LogEvents usually originate at a Logger and are routed + * to LogTargets. + * + * @author Peter Donald + */ +public final class LogEvent + implements Serializable { + //A Constant used when retrieving time relative to start of applicaiton start + private final static long START_TIME = System.currentTimeMillis(); + + ///The category that this LogEvent concerns. (Must not be null) + private String m_category; + + ///The message to be logged. (Must not be null) + private String m_message; + + ///The exception that caused LogEvent if any. (May be null) + private Throwable m_throwable; + + ///The time in millis that LogEvent occurred + private long m_time; + + ///The priority of LogEvent. (Must not be null) + private Priority m_priority; + + ///The context map associated with LogEvent. (May be null). + private ContextMap m_contextMap; + + /** + * Get Priority for LogEvent. + * + * @return the LogEvent Priority + */ + public final Priority getPriority() { + return m_priority; + } + + /** + * Set the priority of LogEvent. + * + * @param priority the new LogEvent priority + */ + public final void setPriority(final Priority priority) { + m_priority = priority; + } + + /** + * Get ContextMap associated with LogEvent + * + * @return the ContextMap + */ + public final ContextMap getContextMap() { + return m_contextMap; + } + + /** + * Set the ContextMap for this LogEvent. + * + * @param contextMap the context map + */ + public final void setContextMap(final ContextMap contextMap) { + m_contextMap = contextMap; + } + +// /** +// * Get ContextStack associated with LogEvent +// * +// * @return the ContextStack +// * @deprecated ContextStack has been deprecated and thus so has this method +// */ +// public final ContextStack getContextStack() +// { +// return m_contextStack; +// } + +// /** +// * Set the ContextStack for this LogEvent. +// * Note that if this LogEvent ever changes threads, the +// * ContextStack must be cloned. +// * +// * @param contextStack the context stack +// * @deprecated ContextStack has been deprecated and thus so has this method +// */ +// public final void setContextStack( final ContextStack contextStack ) +// { +// m_contextStack = contextStack; +// } + + /** + * Get the category that LogEvent relates to. + * + * @return the name of category + */ + public final String getCategory() { + return m_category; + } + + /** + * Get the message associated with event. + * + * @return the message + */ + public final String getMessage() { + return m_message; + } + + /** + * Get throwabe instance associated with event. + * + * @return the Throwable + */ + public final Throwable getThrowable() { + return m_throwable; + } + + /** + * Get the absolute time of the log event. + * + * @return the absolute time + */ + public final long getTime() { + return m_time; + } + + /** + * Get the time of the log event relative to start of application. + * + * @return the time + */ + public final long getRelativeTime() { + return m_time - START_TIME; + } + + /** + * Set the LogEvent category. + * + * @param category the category + */ + public final void setCategory(final String category) { + m_category = category; + } + + /** + * Set the message for LogEvent. + * + * @param message the message + */ + public final void setMessage(final String message) { + m_message = message; + } + + /** + * Set the throwable for LogEvent. + * + * @param throwable the instance of Throwable + */ + public final void setThrowable(final Throwable throwable) { + m_throwable = throwable; + } + + /** + * Set the absolute time of LogEvent. + * + * @param time the time + */ + public final void setTime(final long time) { + m_time = time; + } + + + /** + * Helper method that replaces deserialized priority with correct singleton. + * + * @return the singleton version of object + * @throws ObjectStreamException if an error occurs + */ + private Object readResolve() + throws ObjectStreamException { + if (null == m_category) m_category = ""; + if (null == m_message) m_message = ""; + + String priorityName = ""; + if (null != m_priority) { + priorityName = m_priority.getName(); + } + + m_priority = Priority.getPriorityForName(priorityName); + + return this; + } +} diff --git a/src/java/org/jivesoftware/util/log/LogTarget.java b/src/java/org/jivesoftware/util/log/LogTarget.java new file mode 100644 index 0000000..a3c1c91 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/LogTarget.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +/** + * LogTarget is a class to encapsulate outputting LogEvent's. + * This provides the base for all output and filter targets. + *

+ * Warning: If performance becomes a problem then this + * interface will be rewritten as a abstract class. + * + * @author Peter Donald + */ +public interface LogTarget { + /** + * Process a log event. + * In NO case should this method ever throw an exception/error. + * The reason is that logging is usually added for debugging/auditing + * purposes and it would be unnaceptable to have your debugging + * code cause more errors. + * + * @param event the event + */ + void processEvent(LogEvent event); +} diff --git a/src/java/org/jivesoftware/util/log/Logger.java b/src/java/org/jivesoftware/util/log/Logger.java new file mode 100644 index 0000000..fdf2d34 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/Logger.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +/** + * The object interacted with by client objects to perform logging. + * + * @author Peter Donald + */ +public class Logger { + ///Separator character use to separate different categories + public final static char CATEGORY_SEPARATOR = '.'; + + ///The ErrorHandler associated with Logger + private final ErrorHandler m_errorHandler; + + ///Logger to inherit logtargets and priorities from + private final Logger m_parent; + + ///the fully qualified name of category + private final String m_category; + + ///The list of child loggers associated with this logger + private Logger[] m_children; + + ///The log-targets this logger writes to + private LogTarget[] m_logTargets; + + ///Indicate that logTargets were set with setLogTargets() rather than inherited + private boolean m_logTargetsForceSet; + + ///The priority threshold associated with logger + private Priority m_priority; + + ///Indicate that priority was set with setPriority() rather than inherited + private boolean m_priorityForceSet; + + /** + * True means LogEvents will be sent to parents LogTargets + * aswell as the ones set for this Logger. + */ + private boolean m_additivity; + + /** + * Protected constructor for use inside the logging toolkit. + * You should not be using this constructor directly. + * + * @param errorHandler the ErrorHandler logger uses to log errors + * @param category the fully qualified name of category + * @param logTargets the LogTargets associated with logger + * @param parent the parent logger (used for inheriting from) + */ + Logger(final ErrorHandler errorHandler, + final String category, + final LogTarget[] logTargets, + final Logger parent) { + m_errorHandler = errorHandler; + m_category = category; + m_logTargets = logTargets; + m_parent = parent; + + if (null == m_logTargets) { + unsetLogTargets(); + } + + unsetPriority(); + } + + /** + * Determine if messages of priority DEBUG will be logged. + * + * @return true if DEBUG messages will be logged + */ + public final boolean isDebugEnabled() { + return m_priority.isLowerOrEqual(Priority.DEBUG); + } + + /** + * Log a debug priority event. + * + * @param message the message + * @param throwable the throwable + */ + public final void debug(final String message, final Throwable throwable) { + if (isDebugEnabled()) { + output(Priority.DEBUG, message, throwable); + } + } + + /** + * Log a debug priority event. + * + * @param message the message + */ + public final void debug(final String message) { + if (isDebugEnabled()) { + output(Priority.DEBUG, message, null); + } + } + + /** + * Determine if messages of priority INFO will be logged. + * + * @return true if INFO messages will be logged + */ + public final boolean isInfoEnabled() { + return m_priority.isLowerOrEqual(Priority.INFO); + } + + /** + * Log a info priority event. + * + * @param message the message + */ + public final void info(final String message, final Throwable throwable) { + if (isInfoEnabled()) { + output(Priority.INFO, message, throwable); + } + } + + /** + * Log a info priority event. + * + * @param message the message + */ + public final void info(final String message) { + if (isInfoEnabled()) { + output(Priority.INFO, message, null); + } + } + + /** + * Determine if messages of priority WARN will be logged. + * + * @return true if WARN messages will be logged + */ + public final boolean isWarnEnabled() { + return m_priority.isLowerOrEqual(Priority.WARN); + } + + /** + * Log a warn priority event. + * + * @param message the message + * @param throwable the throwable + */ + public final void warn(final String message, final Throwable throwable) { + if (isWarnEnabled()) { + output(Priority.WARN, message, throwable); + } + } + + /** + * Log a warn priority event. + * + * @param message the message + */ + public final void warn(final String message) { + if (isWarnEnabled()) { + output(Priority.WARN, message, null); + } + } + + /** + * Determine if messages of priority ERROR will be logged. + * + * @return true if ERROR messages will be logged + */ + public final boolean isErrorEnabled() { + return m_priority.isLowerOrEqual(Priority.ERROR); + } + + /** + * Log a error priority event. + * + * @param message the message + * @param throwable the throwable + */ + public final void error(final String message, final Throwable throwable) { + if (isErrorEnabled()) { + output(Priority.ERROR, message, throwable); + } + } + + /** + * Log a error priority event. + * + * @param message the message + */ + public final void error(final String message) { + if (isErrorEnabled()) { + output(Priority.ERROR, message, null); + } + } + + /** + * Determine if messages of priority FATAL_ERROR will be logged. + * + * @return true if FATAL_ERROR messages will be logged + */ + public final boolean isFatalErrorEnabled() { + return m_priority.isLowerOrEqual(Priority.FATAL_ERROR); + } + + /** + * Log a fatalError priority event. + * + * @param message the message + * @param throwable the throwable + */ + public final void fatalError(final String message, final Throwable throwable) { + if (isFatalErrorEnabled()) { + output(Priority.FATAL_ERROR, message, throwable); + } + } + + /** + * Log a fatalError priority event. + * + * @param message the message + */ + public final void fatalError(final String message) { + if (isFatalErrorEnabled()) { + output(Priority.FATAL_ERROR, message, null); + } + } + + /** + * Make this logger additive, which means send all log events to parent + * loggers LogTargets regardless of whether or not the + * LogTargets have been overidden. + *

+ * This is derived from Log4js notion of Additivity. + * + * @param additivity true to make logger additive, false otherwise + */ + public final void setAdditivity(final boolean additivity) { + m_additivity = additivity; + } + + /** + * Determine if messages of priority will be logged. + * + * @return true if messages will be logged + */ + public final boolean isPriorityEnabled(final Priority priority) { + return m_priority.isLowerOrEqual(priority); + } + + /** + * Log a event at specific priority with a certain message and throwable. + * + * @param message the message + * @param priority the priority + * @param throwable the throwable + */ + public final void log(final Priority priority, + final String message, + final Throwable throwable) { + if (m_priority.isLowerOrEqual(priority)) { + output(priority, message, throwable); + } + } + + /** + * Log a event at specific priority with a certain message. + * + * @param message the message + * @param priority the priority + */ + public final void log(final Priority priority, final String message) { + log(priority, message, null); + } + + /** + * Set the priority for this logger. + * + * @param priority the priority + */ + public synchronized void setPriority(final Priority priority) { + m_priority = priority; + m_priorityForceSet = true; + resetChildPriorities(false); + } + + /** + * Unset the priority of Logger. + * (Thus it will use it's parent's priority or DEBUG if no parent. + */ + public synchronized void unsetPriority() { + unsetPriority(false); + } + + /** + * Unset the priority of Logger. + * (Thus it will use it's parent's priority or DEBUG if no parent. + * If recursive is true unset priorities of all child loggers. + * + * @param recursive true to unset priority of all child loggers + */ + public synchronized void unsetPriority(final boolean recursive) { + if (null != m_parent) + m_priority = m_parent.m_priority; + else + m_priority = Priority.DEBUG; + + m_priorityForceSet = false; + resetChildPriorities(recursive); + } + + /** + * Set the log targets for this logger. + * + * @param logTargets the Log Targets + */ + public synchronized void setLogTargets(final LogTarget[] logTargets) { + m_logTargets = logTargets; + setupErrorHandlers(); + m_logTargetsForceSet = true; + resetChildLogTargets(false); + } + + /** + * Unset the logtargets for this logger. + * This logger (and thus all child loggers who don't specify logtargets) will + * inherit from the parents LogTargets. + */ + public synchronized void unsetLogTargets() { + unsetLogTargets(false); + } + + /** + * Unset the logtargets for this logger and all child loggers if recursive is set. + * The loggers unset (and all child loggers who don't specify logtargets) will + * inherit from the parents LogTargets. + */ + public synchronized void unsetLogTargets(final boolean recursive) { + if (null != m_parent) + m_logTargets = m_parent.safeGetLogTargets(); + else + m_logTargets = null; + + m_logTargetsForceSet = false; + resetChildLogTargets(recursive); + } + + /** + * Get all the child Loggers of current logger. + * + * @return the child loggers + */ + public synchronized Logger[] getChildren() { + if (null == m_children) return new Logger[0]; + + final Logger[] children = new Logger[m_children.length]; + + for (int i = 0; i < children.length; i++) { + children[i] = m_children[i]; + } + + return children; + } + + /** + * Create a new child logger. + * The category of child logger is [current-category].subcategory + * + * @param subCategory the subcategory of this logger + * @return the new logger + * @throws IllegalArgumentException if subCategory has an empty element name + */ + public synchronized Logger getChildLogger(final String subCategory) + throws IllegalArgumentException { + final int end = subCategory.indexOf(CATEGORY_SEPARATOR); + + String nextCategory = null; + String remainder = null; + + if (-1 == end) + nextCategory = subCategory; + else { + if (end == 0) { + throw new IllegalArgumentException("Logger categories MUST not have empty elements"); + } + + nextCategory = subCategory.substring(0, end); + remainder = subCategory.substring(end + 1); + } + + //Get FQN for category + String category = null; + if (m_category.equals("")) + category = nextCategory; + else { + category = m_category + CATEGORY_SEPARATOR + nextCategory; + } + + //Check existing children to see if they + //contain next Logger for step in category + if (null != m_children) { + for (int i = 0; i < m_children.length; i++) { + if (m_children[i].m_category.equals(category)) { + if (null == remainder) + return m_children[i]; + else + return m_children[i].getChildLogger(remainder); + } + } + } + + //Create new logger + final Logger child = new Logger(m_errorHandler, category, null, this); + + //Add new logger to child list + if (null == m_children) { + m_children = new Logger[]{child}; + } + else { + final Logger[] children = new Logger[m_children.length + 1]; + System.arraycopy(m_children, 0, children, 0, m_children.length); + children[m_children.length] = child; + m_children = children; + } + + if (null == remainder) + return child; + else + return child.getChildLogger(remainder); + } + + /** + * Retrieve priority associated with Logger. + * + * @return the loggers priority + * @deprecated This method violates Inversion of Control principle. + * It will downgraded to protected access in a future + * release. When user needs to check priority it is advised + * that they use the is[Priority]Enabled() functions. + */ + public final Priority getPriority() { + return m_priority; + } + + /** + * Retrieve category associated with logger. + * + * @return the Category + * @deprecated This method violates Inversion of Control principle. + * If you are relying on its presence then there may be + * something wrong with the design of your system + */ + public final String getCategory() { + return m_category; + } + + /** + * Get a copy of log targets for this logger. + * + * @return the child loggers + */ + public LogTarget[] getLogTargets() { + // Jive change - we ignore the deprecated warning above and just return the log targets + // since it's a closed system for us anyways + return m_logTargets; + } + + /** + * Internal method to do actual outputting. + * + * @param priority the priority + * @param message the message + * @param throwable the throwable + */ + private final void output(final Priority priority, + final String message, + final Throwable throwable) { + final LogEvent event = new LogEvent(); + event.setCategory(m_category); +// event.setContextStack( ContextStack.getCurrentContext( false ) ); + event.setContextMap(ContextMap.getCurrentContext(false)); + + if (null != message) { + event.setMessage(message); + } + else { + event.setMessage(""); + } + + event.setThrowable(throwable); + event.setPriority(priority); + + //this next line can kill performance. It may be wise to + //disable it sometimes and use a more granular approach + event.setTime(System.currentTimeMillis()); + + output(event); + } + + private final void output(final LogEvent event) { + //cache a copy of targets for thread safety + //It is now possible for another thread + //to replace m_logTargets + final LogTarget[] targets = m_logTargets; + + if (null == targets) { + final String message = "LogTarget is null for category '" + m_category + "'"; + m_errorHandler.error(message, null, event); + } + else if (!m_additivity) { + fireEvent(event, targets); + } + else { + //If log targets were not inherited, additivity is true + //then fire an event to local targets + if (m_logTargetsForceSet) { + fireEvent(event, targets); + } + + //if we have a parent Logger then send log event to parent + if (null != m_parent) { + m_parent.output(event); + } + } + } + + private final void fireEvent(final LogEvent event, final LogTarget[] targets) { + for (int i = 0; i < targets.length; i++) { + //No need to clone array as addition of a log-target + //will result in changin whole array + targets[i].processEvent(event); + } + } + + /** + * Update priority of children if any. + */ + private synchronized void resetChildPriorities(final boolean recursive) { + if (null == m_children) return; + + final Logger[] children = m_children; + + for (int i = 0; i < children.length; i++) { + children[i].resetPriority(recursive); + } + } + + /** + * Update priority of this Logger. + * If this loggers priority was manually set then ignore + * otherwise get parents priority and update all children's priority. + */ + private synchronized void resetPriority(final boolean recursive) { + if (recursive) { + m_priorityForceSet = false; + } + else if (m_priorityForceSet) { + return; + } + + m_priority = m_parent.m_priority; + resetChildPriorities(recursive); + } + + /** + * Retrieve logtarget array contained in logger. + * This method is provided so that child Loggers can access a + * copy of parents LogTargets. + * + * @return the array of LogTargets + */ + private synchronized LogTarget[] safeGetLogTargets() { + if (null == m_logTargets) { + if (null == m_parent) + return new LogTarget[0]; + else + return m_parent.safeGetLogTargets(); + } + else { + final LogTarget[] logTargets = new LogTarget[m_logTargets.length]; + + for (int i = 0; i < logTargets.length; i++) { + logTargets[i] = m_logTargets[i]; + } + + return logTargets; + } + } + + /** + * Update logTargets of children if any. + */ + private synchronized void resetChildLogTargets(final boolean recursive) { + if (null == m_children) return; + + for (int i = 0; i < m_children.length; i++) { + m_children[i].resetLogTargets(recursive); + } + } + + /** + * Set ErrorHandlers of LogTargets if necessary. + */ + private synchronized void setupErrorHandlers() { + if (null == m_logTargets) return; + + for (int i = 0; i < m_logTargets.length; i++) { + final LogTarget target = m_logTargets[i]; + if (target instanceof ErrorAware) { + ((ErrorAware)target).setErrorHandler(m_errorHandler); + } + } + } + + /** + * Update logTarget of this Logger. + * If this loggers logTarget was manually set then ignore + * otherwise get parents logTarget and update all children's logTarget. + */ + private synchronized void resetLogTargets(final boolean recursive) { + if (recursive) { + m_logTargetsForceSet = false; + } + else if (m_logTargetsForceSet) { + return; + } + + m_logTargets = m_parent.safeGetLogTargets(); + resetChildLogTargets(recursive); + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/log/Priority.java b/src/java/org/jivesoftware/util/log/Priority.java new file mode 100644 index 0000000..7848442 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/Priority.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * Class representing and holding constants for priority. + * + * @author Peter Donald + */ +public final class Priority implements Serializable { + + /** + * Developer orientated messages, usually used during development of product. + */ + public final static Priority DEBUG = new Priority("DEBUG", 5); + + /** + * Useful information messages such as state changes, client connection, user login etc. + */ + public final static Priority INFO = new Priority("INFO", 10); + + /** + * A problem or conflict has occurred but it may be recoverable, then + * again it could be the start of the system failing. + */ + public final static Priority WARN = new Priority("WARN", 15); + + /** + * A problem has occurred but it is not fatal. The system will still function. + */ + public final static Priority ERROR = new Priority("ERROR", 20); + + /** + * Something caused whole system to fail. This indicates that an administrator + * should restart the system and try to fix the problem that caused the failure. + */ + public final static Priority FATAL_ERROR = new Priority("FATAL_ERROR", 25); + + private final String m_name; + private final int m_priority; + + /** + * Retrieve a Priority object for the name parameter. + * + * @param priority the priority name + * @return the Priority for name + */ + public static Priority getPriorityForName(final String priority) { + if (Priority.DEBUG.getName().equals(priority)) + return Priority.DEBUG; + else if (Priority.INFO.getName().equals(priority)) + return Priority.INFO; + else if (Priority.WARN.getName().equals(priority)) + return Priority.WARN; + else if (Priority.ERROR.getName().equals(priority)) + return Priority.ERROR; + else if (Priority.FATAL_ERROR.getName().equals(priority)) + return Priority.FATAL_ERROR; + else + return Priority.DEBUG; + } + + /** + * Private Constructor to block instantiation outside class. + * + * @param name the string name of priority + * @param priority the numerical code of priority + */ + private Priority(final String name, final int priority) { + m_name = name; + m_priority = priority; + } + + /** + * Overidden string to display Priority in human readable form. + * + * @return the string describing priority + */ + public String toString() { + return "Priority[" + getName() + "/" + getValue() + "]"; + } + + /** + * Get numerical value associated with priority. + * + * @return the numerical value + */ + public int getValue() { + return m_priority; + } + + /** + * Get name of priority. + * + * @return the priorities name + */ + public String getName() { + return m_name; + } + + /** + * Test whether this priority is greater than other priority. + * + * @param other the other Priority + */ + public boolean isGreater(final Priority other) { + return m_priority > other.getValue(); + } + + /** + * Test whether this priority is lower than other priority. + * + * @param other the other Priority + */ + public boolean isLower(final Priority other) { + return m_priority < other.getValue(); + } + + /** + * Test whether this priority is lower or equal to other priority. + * + * @param other the other Priority + */ + public boolean isLowerOrEqual(final Priority other) { + return m_priority <= other.getValue(); + } + + /** + * Helper method that replaces deserialized object with correct singleton. + * + * @return the singleton version of object + * @throws ObjectStreamException if an error occurs + */ + private Object readResolve() + throws ObjectStreamException { + return getPriorityForName(m_name); + } +} diff --git a/src/java/org/jivesoftware/util/log/filter/AbstractFilterTarget.java b/src/java/org/jivesoftware/util/log/filter/AbstractFilterTarget.java new file mode 100644 index 0000000..99c7df5 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/filter/AbstractFilterTarget.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.filter; + +import org.jivesoftware.util.log.FilterTarget; +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.LogTarget; + +/** + * Abstract implementation of FilterTarget. + * A concrete implementation has to implement filter method. + * + * @author Peter Donald + */ +public abstract class AbstractFilterTarget + implements FilterTarget, LogTarget { + //Log targets in filter chain + private LogTarget m_targets[]; + + /** + * Add a new target to output chain. + * + * @param target the target + */ + public void addTarget(final LogTarget target) { + if (null == m_targets) { + m_targets = new LogTarget[]{target}; + } + else { + final LogTarget oldTargets[] = m_targets; + m_targets = new LogTarget[oldTargets.length + 1]; + System.arraycopy(oldTargets, 0, m_targets, 0, oldTargets.length); + m_targets[m_targets.length - 1] = target; + } + } + + /** + * Filter the log event. + * + * @param event the event + * @return return true to discard event, false otherwise + */ + protected abstract boolean filter(LogEvent event); + + /** + * Process a log event + * + * @param event the log event + */ + public void processEvent(final LogEvent event) { + if (null == m_targets || filter(event)) + return; + else { + for (int i = 0; i < m_targets.length; i++) { + m_targets[i].processEvent(event); + } + } + } +} diff --git a/src/java/org/jivesoftware/util/log/filter/PriorityFilter.java b/src/java/org/jivesoftware/util/log/filter/PriorityFilter.java new file mode 100644 index 0000000..c9c8474 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/filter/PriorityFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.filter; + +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.Priority; + +/** + * Filters log events based on priority. + * + * @author Peter Donald + */ +public class PriorityFilter extends AbstractFilterTarget { + + ///Priority to filter against + private Priority m_priority; + + /** + * Constructor that sets the priority that is filtered against. + * + * @param priority the Priority + */ + public PriorityFilter(final Priority priority) { + m_priority = priority; + } + + /** + * Set priority used to filter. + * + * @param priority the priority to filter on + */ + public void setPriority(final Priority priority) { + m_priority = priority; + } + + /** + * Filter the log event based on priority. + *

+ * If LogEvent has a Lower priroity then discard it. + * + * @param event the event + * @return return true to discard event, false otherwise + */ + protected boolean filter(final LogEvent event) { + return (!m_priority.isLower(event.getPriority())); + } +} diff --git a/src/java/org/jivesoftware/util/log/format/ExtendedPatternFormatter.java b/src/java/org/jivesoftware/util/log/format/ExtendedPatternFormatter.java new file mode 100644 index 0000000..3cd3369 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/format/ExtendedPatternFormatter.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.format; + +import org.jivesoftware.util.Log; +import org.jivesoftware.util.log.ContextMap; +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.util.StackIntrospector; + +/** + * Formatter especially designed for debugging applications. + *

+ * This formatter extends the standard PatternFormatter to add + * two new possible expansions. These expansions are %{method} + * and %{thread}. In both cases the context map is first checked + * for values with specified key. This is to facilitate passing + * information about caller/thread when threads change (as in + * AsyncLogTarget). They then attempt to determine appropriate + * information dynamically. + * + * @author Peter Donald + * @version CVS $Revision: 37 $ $Date: 2004-10-21 03:08:43 -0300 (Thu, 21 Oct 2004) $ + */ +public class ExtendedPatternFormatter extends PatternFormatter { + private final static int TYPE_METHOD = MAX_TYPE + 1; + private final static int TYPE_THREAD = MAX_TYPE + 2; + + private final static String TYPE_METHOD_STR = "method"; + private final static String TYPE_THREAD_STR = "thread"; + + public ExtendedPatternFormatter(final String format) { + super(format); + } + + /** + * Retrieve the type-id for a particular string. + * + * @param type the string + * @return the type-id + */ + protected int getTypeIdFor(final String type) { + if (type.equalsIgnoreCase(TYPE_METHOD_STR)) + return TYPE_METHOD; + else if (type.equalsIgnoreCase(TYPE_THREAD_STR)) + return TYPE_THREAD; + else { + return super.getTypeIdFor(type); + } + } + + /** + * Formats a single pattern run (can be extended in subclasses). + * + * @param run the pattern run to format. + * @return the formatted result. + */ + protected String formatPatternRun(final LogEvent event, final PatternRun run) { + switch (run.m_type) { + case TYPE_METHOD: + return getMethod(event, run.m_format); + case TYPE_THREAD: + return getThread(event, run.m_format); + default: + return super.formatPatternRun(event, run); + } + } + + /** + * Utility method to format category. + * + * @param event + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + private String getMethod(final LogEvent event, final String format) { + final ContextMap map = event.getContextMap(); + if (null != map) { + final Object object = map.get("method"); + if (null != object) { + return object.toString(); + } + } + +// final String result = StackIntrospector.getCallerMethod(Logger.class); + final String result = StackIntrospector.getCallerMethod(Log.class); + if (null == result) { + return "UnknownMethod"; + } + return result; + } + + /** + * Utility thread to format category. + * + * @param event + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + private String getThread(final LogEvent event, final String format) { + final ContextMap map = event.getContextMap(); + if (null != map) { + final Object object = map.get("thread"); + if (null != object) { + return object.toString(); + } + } + + return Thread.currentThread().getName(); + } +} diff --git a/src/java/org/jivesoftware/util/log/format/Formatter.java b/src/java/org/jivesoftware/util/log/format/Formatter.java new file mode 100644 index 0000000..6601050 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/format/Formatter.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.format; + +import org.jivesoftware.util.log.LogEvent; + +/** + * This defines the interface for components that wish to serialize + * LogEvents into Strings. + * + * @author Peter Donald + */ +public interface Formatter { + /** + * Serialize log event into string. + * + * @param event the event + * @return the formatted string + */ + String format(LogEvent event); +} diff --git a/src/java/org/jivesoftware/util/log/format/PatternFormatter.java b/src/java/org/jivesoftware/util/log/format/PatternFormatter.java new file mode 100644 index 0000000..0960c32 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/format/PatternFormatter.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.format; + +import org.jivesoftware.util.FastDateFormat; +import org.jivesoftware.util.log.ContextMap; +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.Priority; + +import java.io.StringWriter; +import java.util.Date; +import java.util.Stack; + +/** + * This formater formats the LogEvents according to a input pattern + * string. + *

+ * The format of each pattern element can be %[+|-][#[.#]]{field:subformat}. + *

+ *
    + *
  • The +|- indicates left or right justify. + *
  • + *
  • The #.# indicates the minimum and maximum size of output.
    + * You may omit the values and the field will be formatted without size + * restriction.
    + * You may specify '#', or '#.' to define an minimum size, only.
    + * You may specify '.#' to define an maximum size only. + *
  • + *
  • + * 'field' indicates which field is to be output and must be one of + * properties of LogEvent.
    + * Currently following fields are supported: + *
    + *
    category
    + *
    Category value of the logging event.
    + *
    context
    + *
    Context value of the logging event.
    + *
    message
    + *
    Message value of the logging event.
    + *
    time
    + *
    Time value of the logging event.
    + *
    rtime
    + *
    Relative time value of the logging event.
    + *
    throwable
    + *
    Throwable value of the logging event.
    + *
    priority
    + *
    Priority value of the logging event.
    + *
    + *
  • + *
  • 'subformat' indicates a particular subformat and is currently only used + * for category context to specify the context map parameter name. + *
  • + *
+ *

A simple example of a typical PatternFormatter format: + *

+ *
%{time} %5.5{priority}[%-10.10{category}]: %{message}
+ * 
+ *

+ * This format string will format a log event printing first time value of + * of log event with out size restriction, next priority with minum and maximum size 5, + * next category right justified having minmum and maximum size of 10, + * at last the message of the log event without size restriction. + *

+ *

A formatted sample message of the above pattern format: + *

+ *
1000928827905 DEBUG [     junit]: Sample message
+ * 
+ * + * @author Peter Donald + * @author Sylvain Wallez + * @version CVS $Revision: 1747 $ $Date: 2005-08-04 18:36:36 -0300 (Thu, 04 Aug 2005) $ + */ +public class PatternFormatter implements Formatter { + private final static int TYPE_TEXT = 1; + private final static int TYPE_CATEGORY = 2; + private final static int TYPE_CONTEXT = 3; + private final static int TYPE_MESSAGE = 4; + private final static int TYPE_TIME = 5; + private final static int TYPE_RELATIVE_TIME = 6; + private final static int TYPE_THROWABLE = 7; + private final static int TYPE_PRIORITY = 8; + + /** + * The maximum value used for TYPEs. Subclasses can define their own TYPEs + * starting at MAX_TYPE + 1. + */ + protected final static int MAX_TYPE = TYPE_PRIORITY; + + private final static String TYPE_CATEGORY_STR = "category"; + private final static String TYPE_CONTEXT_STR = "context"; + private final static String TYPE_MESSAGE_STR = "message"; + private final static String TYPE_TIME_STR = "time"; + private final static String TYPE_RELATIVE_TIME_STR = "rtime"; + private final static String TYPE_THROWABLE_STR = "throwable"; + private final static String TYPE_PRIORITY_STR = "priority"; + + private final static String SPACE_16 = " "; + private final static String SPACE_8 = " "; + private final static String SPACE_4 = " "; + private final static String SPACE_2 = " "; + private final static String SPACE_1 = " "; + + private final static String EOL = System.getProperty("line.separator", "\n"); + + protected static class PatternRun { + public String m_data; + public boolean m_rightJustify; + public int m_minSize; + public int m_maxSize; + public int m_type; + public String m_format; + } + + private PatternRun m_formatSpecification[]; + + private FastDateFormat m_simpleDateFormat; + private final Date m_date = new Date(); + + /** + * @deprecated Use constructor PatternFormatter(String pattern) as this does not + * correctly initialize object + */ + public PatternFormatter() { + } + + public PatternFormatter(final String pattern) { + parse(pattern); + } + + /** + * Extract and build a pattern from input string. + * + * @param stack the stack on which to place patterns + * @param pattern the input string + * @param index the start of pattern run + * @return the number of characters in pattern run + */ + private int addPatternRun(final Stack stack, + final char pattern[], + int index) { + final PatternRun run = new PatternRun(); + final int start = index++; + + //first check for a +|- sign + if ('+' == pattern[index]) + index++; + else if ('-' == pattern[index]) { + run.m_rightJustify = true; + index++; + } + + if (Character.isDigit(pattern[index])) { + int total = 0; + while (Character.isDigit(pattern[index])) { + total = total * 10 + (pattern[index] - '0'); + index++; + } + run.m_minSize = total; + } + + //check for . sign indicating a maximum is to follow + if (index < pattern.length && '.' == pattern[index]) { + index++; + + if (Character.isDigit(pattern[index])) { + int total = 0; + while (Character.isDigit(pattern[index])) { + total = total * 10 + (pattern[index] - '0'); + index++; + } + run.m_maxSize = total; + } + } + + if (index >= pattern.length || '{' != pattern[index]) { + throw + new IllegalArgumentException("Badly formed pattern at character " + + index); + } + + int typeStart = index; + + while (index < pattern.length && + pattern[index] != ':' && pattern[index] != '}') { + index++; + } + + int typeEnd = index - 1; + + final String type = + new String(pattern, typeStart + 1, typeEnd - typeStart); + + run.m_type = getTypeIdFor(type); + + if (index < pattern.length && pattern[index] == ':') { + index++; + while (index < pattern.length && pattern[index] != '}') index++; + + final int length = index - typeEnd - 2; + + if (0 != length) { + run.m_format = new String(pattern, typeEnd + 2, length); + } + } + + if (index >= pattern.length || '}' != pattern[index]) { + throw new + IllegalArgumentException("Unterminated type in pattern at character " + + index); + } + + index++; + + stack.push(run); + + return index - start; + } + + /** + * Extract and build a text run from input string. + * It does special handling of '\n' and '\t' replaceing + * them with newline and tab. + * + * @param stack the stack on which to place runs + * @param pattern the input string + * @param index the start of the text run + * @return the number of characters in run + */ + private int addTextRun(final Stack stack, + final char pattern[], + int index) { + final PatternRun run = new PatternRun(); + final int start = index; + boolean escapeMode = false; + + if ('%' == pattern[index]) index++; + + final StringBuffer sb = new StringBuffer(); + + while (index < pattern.length && pattern[index] != '%') { + if (escapeMode) { + if ('n' == pattern[index]) + sb.append(EOL); + else if ('t' == pattern[index]) + sb.append('\t'); + else + sb.append(pattern[index]); + escapeMode = false; + } + else if ('\\' == pattern[index]) + escapeMode = true; + else + sb.append(pattern[index]); + index++; + } + + run.m_data = sb.toString(); + run.m_type = TYPE_TEXT; + + stack.push(run); + + return index - start; + } + + /** + * Utility to append a string to buffer given certain constraints. + * + * @param sb the StringBuffer + * @param minSize the minimum size of output (0 to ignore) + * @param maxSize the maximum size of output (0 to ignore) + * @param rightJustify true if the string is to be right justified in it's box. + * @param output the input string + */ + private void append(final StringBuffer sb, + final int minSize, + final int maxSize, + final boolean rightJustify, + final String output) { + final int size = output.length(); + + if (size < minSize) { + //assert( minSize > 0 ); + if (rightJustify) { + appendWhiteSpace(sb, minSize - size); + sb.append(output); + } + else { + sb.append(output); + appendWhiteSpace(sb, minSize - size); + } + } + else if (maxSize > 0 && maxSize < size) { + if (rightJustify) { + sb.append(output.substring(size - maxSize)); + } + else { + sb.append(output.substring(0, maxSize)); + } + } + else { + sb.append(output); + } + } + + /** + * Append a certain number of whitespace characters to a StringBuffer. + * + * @param sb the StringBuffer + * @param length the number of spaces to append + */ + private void appendWhiteSpace(final StringBuffer sb, int length) { + while (length >= 16) { + sb.append(SPACE_16); + length -= 16; + } + + if (length >= 8) { + sb.append(SPACE_8); + length -= 8; + } + + if (length >= 4) { + sb.append(SPACE_4); + length -= 4; + } + + if (length >= 2) { + sb.append(SPACE_2); + length -= 2; + } + + if (length >= 1) { + sb.append(SPACE_1); + length -= 1; + } + } + + /** + * Format the event according to the pattern. + * + * @param event the event + * @return the formatted output + */ + public String format(final LogEvent event) { + final StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < m_formatSpecification.length; i++) { + final PatternRun run = m_formatSpecification[i]; + + //treat text differently as it doesn't need min/max padding + if (run.m_type == TYPE_TEXT) { + sb.append(run.m_data); + } + else { + final String data = formatPatternRun(event, run); + + if (null != data) { + append(sb, run.m_minSize, run.m_maxSize, run.m_rightJustify, data); + } + } + } + + return sb.toString(); + } + + /** + * Formats a single pattern run (can be extended in subclasses). + * + * @param run the pattern run to format. + * @return the formatted result. + */ + protected String formatPatternRun(final LogEvent event, final PatternRun run) { + switch (run.m_type) { + case TYPE_RELATIVE_TIME: + return getRTime(event.getRelativeTime(), run.m_format); + case TYPE_TIME: + return getTime(event.getTime(), run.m_format); + case TYPE_THROWABLE: + return getStackTrace(event.getThrowable(), run.m_format); + case TYPE_MESSAGE: + return getMessage(event.getMessage(), run.m_format); + case TYPE_CATEGORY: + return getCategory(event.getCategory(), run.m_format); + case TYPE_PRIORITY: + return getPriority(event.getPriority(), run.m_format); + + case TYPE_CONTEXT: +// if( null == run.m_format || +// run.m_format.startsWith( "stack" ) ) +// { +// //Print a warning out to stderr here +// //to indicate you are using a deprecated feature? +// return getContext( event.getContextStack(), run.m_format ); +// } +// else +// { + return getContextMap(event.getContextMap(), run.m_format); +// } + + default: + throw new IllegalStateException("Unknown Pattern specification." + run.m_type); + } + } + + /** + * Utility method to format category. + * + * @param category the category string + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getCategory(final String category, final String format) { + return category; + } + + /** + * Get formatted priority string. + */ + protected String getPriority(final Priority priority, final String format) { + return priority.getName(); + } + +// /** +// * Utility method to format context. +// * +// * @param context the context string +// * @param format ancilliary format parameter - allowed to be null +// * @return the formatted string +// * @deprecated Use getContextStack rather than this method +// */ +// protected String getContext( final ContextStack stack, final String format ) +// { +// return getContextStack( stack, format ); +// } + +// /** +// * Utility method to format context. +// * +// * @param context the context string +// * @param format ancilliary format parameter - allowed to be null +// * @return the formatted string +// */ +// protected String getContextStack( final ContextStack stack, final String format ) +// { +// if( null == stack ) return ""; +// return stack.toString( Integer.MAX_VALUE ); +// } + + /** + * Utility method to format context map. + * + * @param map the context map + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getContextMap(final ContextMap map, final String format) { + if (null == map) return ""; + return map.get(format, "").toString(); + } + + /** + * Utility method to format message. + * + * @param message the message string + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getMessage(final String message, final String format) { + return message; + } + + /** + * Utility method to format stack trace. + * + * @param throwable the throwable instance + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getStackTrace(final Throwable throwable, final String format) { + if (null == throwable) return ""; + final StringWriter sw = new StringWriter(); + throwable.printStackTrace(new java.io.PrintWriter(sw)); + return sw.toString(); + } + + /** + * Utility method to format relative time. + * + * @param time the time + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getRTime(final long time, final String format) { + return getTime(time, format); + } + + /** + * Utility method to format time. + * + * @param time the time + * @param format ancilliary format parameter - allowed to be null + * @return the formatted string + */ + protected String getTime(final long time, final String format) { + if (null == format) { + return Long.toString(time); + } + else { + synchronized (m_date) { + if (null == m_simpleDateFormat) { + m_simpleDateFormat = FastDateFormat.getInstance(format); + } + m_date.setTime(time); + return m_simpleDateFormat.format(m_date); + } + } + } + + /** + * Retrieve the type-id for a particular string. + * + * @param type the string + * @return the type-id + */ + protected int getTypeIdFor(final String type) { + if (type.equalsIgnoreCase(TYPE_CATEGORY_STR)) + return TYPE_CATEGORY; + else if (type.equalsIgnoreCase(TYPE_CONTEXT_STR)) + return TYPE_CONTEXT; + else if (type.equalsIgnoreCase(TYPE_MESSAGE_STR)) + return TYPE_MESSAGE; + else if (type.equalsIgnoreCase(TYPE_PRIORITY_STR)) + return TYPE_PRIORITY; + else if (type.equalsIgnoreCase(TYPE_TIME_STR)) + return TYPE_TIME; + else if (type.equalsIgnoreCase(TYPE_RELATIVE_TIME_STR)) + return TYPE_RELATIVE_TIME; + else if (type.equalsIgnoreCase(TYPE_THROWABLE_STR)) { + return TYPE_THROWABLE; + } + else { + throw new IllegalArgumentException("Unknown Type in pattern - " + + type); + } + } + + /** + * Parse the input pattern and build internal data structures. + * + * @param patternString the pattern + */ + protected final void parse(final String patternString) { + final Stack stack = new Stack(); + final int size = patternString.length(); + final char pattern[] = new char[size]; + int index = 0; + + patternString.getChars(0, size, pattern, 0); + + while (index < size) { + if (pattern[index] == '%' && + !(index != size - 1 && pattern[index + 1] == '%')) { + index += addPatternRun(stack, pattern, index); + } + else { + index += addTextRun(stack, pattern, index); + } + } + + final int elementCount = stack.size(); + + m_formatSpecification = new PatternRun[elementCount]; + + for (int i = 0; i < elementCount; i++) { + m_formatSpecification[i] = (PatternRun)stack.elementAt(i); + } + } + + /** + * Set the string description that the format is extracted from. + * + * @param format the string format + * @deprecated Parse format in via constructor rather than use this method + */ + public void setFormat(final String format) { + parse(format); + } +} diff --git a/src/java/org/jivesoftware/util/log/output/AbstractOutputTarget.java b/src/java/org/jivesoftware/util/log/output/AbstractOutputTarget.java new file mode 100644 index 0000000..b289146 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/AbstractOutputTarget.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output; + +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.format.Formatter; + +/** + * Abstract output target. + * Any new output target that is writing to a single connected + * resource should extend this class directly or indirectly. + * + * @author Peter Donald + */ +public abstract class AbstractOutputTarget + extends AbstractTarget { + /** + * Formatter for target. + */ + private Formatter m_formatter; + + /** + * Parameterless constructor. + */ + public AbstractOutputTarget() { + } + + public AbstractOutputTarget(final Formatter formatter) { + m_formatter = formatter; + } + + /** + * Retrieve the associated formatter. + * + * @return the formatter + * @deprecated Access to formatter is not advised and this method will be removed + * in future iterations. It remains only for backwards compatability. + */ + public synchronized Formatter getFormatter() { + return m_formatter; + } + + /** + * Set the formatter. + * + * @param formatter the formatter + * @deprecated In future this method will become protected access. + */ + public synchronized void setFormatter(final Formatter formatter) { + writeTail(); + m_formatter = formatter; + writeHead(); + } + + /** + * Abstract method to send data. + * + * @param data the data to be output + */ + protected void write(final String data) { + output(data); + } + + /** + * Abstract method that will output event. + * + * @param data the data to be output + * @deprecated User should overide send() instead of output(). Output exists + * for backwards compatability and will be removed in future. + */ + protected void output(final String data) { + } + + protected void doProcessEvent(LogEvent event) { + final String data = format(event); + write(data); + } + + /** + * Startup log session. + */ + protected synchronized void open() { + if (!isOpen()) { + super.open(); + writeHead(); + } + } + + /** + * Shutdown target. + * Attempting to send to target after close() will cause errors to be logged. + */ + public synchronized void close() { + if (isOpen()) { + writeTail(); + super.close(); + } + } + + /** + * Helper method to format an event into a string, using the formatter if available. + * + * @param event the LogEvent + * @return the formatted string + */ + private String format(final LogEvent event) { + if (null != m_formatter) { + return m_formatter.format(event); + } + else { + return event.toString(); + } + } + + /** + * Helper method to send out log head. + * The head initiates a session of logging. + */ + private void writeHead() { + if (!isOpen()) return; + + final String head = getHead(); + if (null != head) { + write(head); + } + } + + /** + * Helper method to send out log tail. + * The tail completes a session of logging. + */ + private void writeTail() { + if (!isOpen()) return; + + final String tail = getTail(); + if (null != tail) { + write(tail); + } + } + + /** + * Helper method to retrieve head for log session. + * TODO: Extract from formatter + * + * @return the head string + */ + private String getHead() { + return null; + } + + /** + * Helper method to retrieve tail for log session. + * TODO: Extract from formatter + * + * @return the head string + */ + private String getTail() { + return null; + } +} diff --git a/src/java/org/jivesoftware/util/log/output/AbstractTarget.java b/src/java/org/jivesoftware/util/log/output/AbstractTarget.java new file mode 100644 index 0000000..b900c77 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/AbstractTarget.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output; + +import org.jivesoftware.util.log.ErrorAware; +import org.jivesoftware.util.log.ErrorHandler; +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.LogTarget; + +/** + * Abstract target. + * + * @author Peter Donald + */ +public abstract class AbstractTarget implements LogTarget, ErrorAware { + + ///ErrorHandler used by target to delegate Error handling + private ErrorHandler m_errorHandler; + + ///Flag indicating that log session is finished (aka target has been closed) + private boolean m_isOpen; + + /** + * Provide component with ErrorHandler. + * + * @param errorHandler the errorHandler + */ + public synchronized void setErrorHandler(final ErrorHandler errorHandler) { + m_errorHandler = errorHandler; + } + + protected synchronized boolean isOpen() { + return m_isOpen; + } + + /** + * Startup log session. + */ + protected synchronized void open() { + if (!isOpen()) { + m_isOpen = true; + } + } + + /** + * Process a log event, via formatting and outputting it. + * + * @param event the log event + */ + public synchronized void processEvent(final LogEvent event) { + if (!isOpen()) { + getErrorHandler().error("Writing event to closed stream.", null, event); + return; + } + + try { + doProcessEvent(event); + } + catch (final Throwable throwable) { + getErrorHandler().error("Unknown error writing event.", throwable, event); + } + } + + /** + * Process a log event, via formatting and outputting it. + * This should be overidden by subclasses. + * + * @param event the log event + */ + protected abstract void doProcessEvent(LogEvent event) + throws Exception; + + /** + * Shutdown target. + * Attempting to send to target after close() will cause errors to be logged. + */ + public synchronized void close() { + if (isOpen()) { + m_isOpen = false; + } + } + + /** + * Helper method to retrieve ErrorHandler for subclasses. + * + * @return the ErrorHandler + */ + protected final ErrorHandler getErrorHandler() { + return m_errorHandler; + } + + /** + * Helper method to send error messages to error handler. + * + * @param message the error message + * @param throwable the exception if any + * @deprecated Use getErrorHandler().error(...) directly + */ + protected final void error(final String message, final Throwable throwable) { + getErrorHandler().error(message, throwable, null); + } +} diff --git a/src/java/org/jivesoftware/util/log/output/AsyncLogTarget.java b/src/java/org/jivesoftware/util/log/output/AsyncLogTarget.java new file mode 100644 index 0000000..5106ed2 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/AsyncLogTarget.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output; + +import org.jivesoftware.util.log.ErrorAware; +import org.jivesoftware.util.log.ErrorHandler; +import org.jivesoftware.util.log.LogEvent; +import org.jivesoftware.util.log.LogTarget; +import java.util.LinkedList; + +/** + * An asynchronous LogTarget that sends entries on in another thread. + * It is the responsibility of the user of this class to start + * the thread etc. + *

+ *

+ * LogTarget mySlowTarget = ...;
+ * AsyncLogTarget asyncTarget = new AsyncLogTarget( mySlowTarget );
+ * Thread thread = new Thread( asyncTarget );
+ * thread.setPriority( Thread.MIN_PRIORITY );
+ * thread.start();
+ * 

+ * logger.setLogTargets( new LogTarget[] { asyncTarget } ); + *

+ * + * @author Peter Donald + */ +public class AsyncLogTarget extends AbstractTarget implements Runnable { + + private final LinkedList m_list; + private final int m_queueSize; + private final LogTarget m_logTarget; + + public AsyncLogTarget(final LogTarget logTarget) { + this(logTarget, 15); + } + + public AsyncLogTarget(final LogTarget logTarget, final int queueSize) { + m_logTarget = logTarget; + m_list = new LinkedList(); + m_queueSize = queueSize; + open(); + } + + /** + * Provide component with ErrorHandler. + * + * @param errorHandler the errorHandler + */ + public synchronized void setErrorHandler(final ErrorHandler errorHandler) { + super.setErrorHandler(errorHandler); + + if (m_logTarget instanceof ErrorAware) { + ((ErrorAware)m_logTarget).setErrorHandler(errorHandler); + } + } + + /** + * Process a log event by adding it to queue. + * + * @param event the log event + */ + public void doProcessEvent(final LogEvent event) { + synchronized (m_list) { + final int size = m_list.size(); + while (m_queueSize <= size) { + try { + m_list.wait(); + } + catch (final InterruptedException ie) { + //This really should not occur ... + //Maybe we should log it though for + //now lets ignore it + } + } + + m_list.addFirst(event); + + if (size == 0) { + //tell the "server" thread to wake up + //if it is waiting for a queue to contain some items + m_list.notify(); + } + } + } + + public void run() { + //set this variable when thread is interupted + //so we know we can shutdown thread soon. + boolean interupted = false; + + while (true) { + LogEvent event = null; + + synchronized (m_list) { + while (null == event) { + final int size = m_list.size(); + + if (size > 0) { + event = (LogEvent)m_list.removeLast(); + + if (size == m_queueSize) { + //tell the "client" thread to wake up + //if it is waiting for a queue position to open up + m_list.notify(); + } + + } + else if (interupted || Thread.interrupted()) { + //ie there is nothing in queue and thread is interrupted + //thus we stop thread + return; + } + else { + try { + m_list.wait(); + } + catch (final InterruptedException ie) { + //Ignore this and let it be dealt in next loop + //Need to set variable as the exception throw cleared status + interupted = true; + } + } + } + } + + + try { + //actually process an event + m_logTarget.processEvent(event); + } + catch (final Throwable throwable) { + getErrorHandler().error("Unknown error writing event.", throwable, event); + } + } + } +} diff --git a/src/java/org/jivesoftware/util/log/output/io/FileTarget.java b/src/java/org/jivesoftware/util/log/output/io/FileTarget.java new file mode 100644 index 0000000..c30fb88 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/FileTarget.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io; + +import org.jivesoftware.util.log.format.Formatter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * A basic target that writes to a File. + * + * @author Peter Donald + */ +public class FileTarget extends StreamTarget { + + ///File we are writing to + private File m_file; + + ///Flag indicating whether or not file should be appended to + private boolean m_append; + + /** + * Construct file target to send to a file with a formatter. + * + * @param file the file to send to + * @param append true if file is to be appended to, false otherwise + * @param formatter the Formatter + * @throws IOException if an error occurs + */ + public FileTarget(final File file, final boolean append, final Formatter formatter) + throws IOException { + super(null, formatter); + + if (null != file) { + setFile(file, append); + openFile(); + } + } + + /** + * Set the file for this target. + * + * @param file the file to send to + * @param append true if file is to be appended to, false otherwise + * @throws IOException if directories can not be created or file can not be opened + */ + protected synchronized void setFile(final File file, final boolean append) + throws IOException { + if (null == file) { + throw new NullPointerException("file property must not be null"); + } + + if (isOpen()) { + throw new IOException("target must be closed before " + + "file property can be set"); + } + + m_append = append; + m_file = file; + } + + /** + * Open underlying file and allocate resources. + * This method will attempt to create directories below file and + * append to it if specified. + */ + protected synchronized void openFile() + throws IOException { + if (isOpen()) close(); + + final File file = getFile().getCanonicalFile(); + + final File parent = file.getParentFile(); + if (null != parent && !parent.exists()) { + parent.mkdir(); + } + + final FileOutputStream outputStream = + new FileOutputStream(file.getPath(), m_append); + + setOutputStream(outputStream); + open(); + } + + /** + * Retrieve file associated with target. + * This allows subclasses to access file object. + * + * @return the output File + */ + protected synchronized File getFile() { + return m_file; + } +} diff --git a/src/java/org/jivesoftware/util/log/output/io/StreamTarget.java b/src/java/org/jivesoftware/util/log/output/io/StreamTarget.java new file mode 100644 index 0000000..4e5a135 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/StreamTarget.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io; + +import org.jivesoftware.util.log.format.Formatter; +import org.jivesoftware.util.log.output.AbstractOutputTarget; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A basic target that writes to an OutputStream. + * + * @author Peter Donald + */ +public class StreamTarget extends AbstractOutputTarget { + ///OutputStream we are writing to + private OutputStream m_outputStream; + + /** + * Constructor that writes to a stream and uses a particular formatter. + * + * @param outputStream the OutputStream to send to + * @param formatter the Formatter to use + */ + public StreamTarget(final OutputStream outputStream, final Formatter formatter) { + super(formatter); + + if (null != outputStream) { + setOutputStream(outputStream); + open(); + } + } + + /** + * Set the output stream. + * Close down old stream and send tail if appropriate. + * + * @param outputStream the new OutputStream + */ + protected synchronized void setOutputStream(final OutputStream outputStream) { + if (null == outputStream) { + throw new NullPointerException("outputStream property must not be null"); + } + + m_outputStream = outputStream; + } + + /** + * Abstract method that will output event. + * + * @param data the data to be output + */ + protected synchronized void write(final String data) { + //Cache method local version + //so that can be replaced in another thread + final OutputStream outputStream = m_outputStream; + + if (null == outputStream) { + final String message = "Attempted to send data '" + data + "' to Null OutputStream"; + getErrorHandler().error(message, null, null); + return; + } + + try { + //TODO: We should be able to specify encoding??? + outputStream.write(data.getBytes("UTF-8")); + outputStream.flush(); + } + catch (final IOException ioe) { + final String message = "Error writing data '" + data + "' to OutputStream"; + getErrorHandler().error(message, ioe, null); + } + } + + /** + * Shutdown target. + * Attempting to send to target after close() will cause errors to be logged. + */ + public synchronized void close() { + super.close(); + shutdownStream(); + } + + /** + * Shutdown output stream. + */ + protected synchronized void shutdownStream() { + final OutputStream outputStream = m_outputStream; + m_outputStream = null; + + try { + if (null != outputStream) { + outputStream.close(); + } + } + catch (final IOException ioe) { + getErrorHandler().error("Error closing OutputStream", ioe, null); + } + } +} diff --git a/src/java/org/jivesoftware/util/log/output/io/WriterTarget.java b/src/java/org/jivesoftware/util/log/output/io/WriterTarget.java new file mode 100644 index 0000000..75ec78a --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/WriterTarget.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io; + +import org.jivesoftware.util.log.format.Formatter; +import org.jivesoftware.util.log.output.AbstractOutputTarget; +import java.io.IOException; +import java.io.Writer; + +/** + * This target outputs to a writer. + * + * @author Peter Donald + */ +public class WriterTarget extends AbstractOutputTarget { + + private Writer m_output; + + /** + * Construct target with a specific writer and formatter. + * + * @param writer the writer + * @param formatter the formatter + */ + public WriterTarget(final Writer writer, final Formatter formatter) { + super(formatter); + + if (null != writer) { + setWriter(writer); + open(); + } + } + + /** + * Set the writer. + * Close down writer and send tail if appropriate. + * + * @param writer the new writer + */ + protected synchronized void setWriter(final Writer writer) { + if (null == writer) { + throw new NullPointerException("writer property must not be null"); + } + + m_output = writer; + } + + /** + * Concrete implementation of output that writes out to underlying writer. + * + * @param data the data to output + */ + protected void write(final String data) { + try { + m_output.write(data); + m_output.flush(); + } + catch (final IOException ioe) { + getErrorHandler().error("Caught an IOException", ioe, null); + } + } + + /** + * Shutdown target. + * Attempting to send to target after close() will cause errors to be logged. + */ + public synchronized void close() { + super.close(); + shutdownWriter(); + } + + /** + * Shutdown Writer. + */ + protected synchronized void shutdownWriter() { + final Writer writer = m_output; + m_output = null; + + try { + if (null != writer) { + writer.close(); + } + } + catch (final IOException ioe) { + getErrorHandler().error("Error closing Writer", ioe, null); + } + } +} diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/ExpandingFileStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/ExpandingFileStrategy.java new file mode 100644 index 0000000..d4f62b8 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/ExpandingFileStrategy.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * strategy for naming log files based on appending revolving suffix. + *

+ * Heavily odified by Bruce Ritchie (Jive Software) to rotate along + * the following strategy: + *

+ * current log file will always be the base File name + * the next oldest file will be the _1 file + * the next oldest file will be the _2 file + * etc. + * + * @author Bernhard Huber + */ +public class ExpandingFileStrategy implements FileStrategy { + + ///the base file name. + private String baseFileName; + + public ExpandingFileStrategy(final String baseFileName) { + + this.baseFileName = baseFileName; + } + + public File currentFile() { + return new File(baseFileName); + } + + /** + * Calculate the real file name from the base filename. + * + * @return File the calculated file name + */ + public File nextFile() { + // go through all the possible filenames and delete/rename as necessary + for (int i = 0; true; i++) { + File test = new File(baseFileName.substring(0, baseFileName.lastIndexOf('.')) + + "_" + i + baseFileName.substring(baseFileName.lastIndexOf('.'))); + + if (test.exists()) { + continue; + } + else { + return test; + } + } + } +} + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/FileStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/FileStrategy.java new file mode 100644 index 0000000..957eef8 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/FileStrategy.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * Strategy for naming log files. + * For a given base file name an implementation calculates + * the real file name. + * + * @author Bernhard Huber + * @author Peter Donald + */ +public interface FileStrategy { + + /** + * Get the current logfile + */ + File currentFile(); + + /** + * Get the next log file to rotate to. + * + * @return the file to rotate to + */ + File nextFile(); +} + + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/OrRotateStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/OrRotateStrategy.java new file mode 100644 index 0000000..20f094e --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/OrRotateStrategy.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * Hierarchical Rotation stragety. + * This object is initialised with several rotation strategy objects. + * The isRotationNeeded method checks the first rotation + * strategy object. If a rotation is needed, this result is returned. + * If not the next rotation strategy object is asked and so on. + * + * @author Carsten Ziegeler + */ +public class OrRotateStrategy + implements RotateStrategy { + private RotateStrategy[] m_strategies; + + /** + * The rotation strategy used. This marker is required for the reset() + * method. + */ + private int m_usedRotation = -1; + + /** + * Constructor + */ + public OrRotateStrategy(final RotateStrategy[] strategies) { + this.m_strategies = strategies; + } + + /** + * reset. + */ + public void reset() { + if (-1 != m_usedRotation) { + m_strategies[m_usedRotation].reset(); + m_usedRotation = -1; + } + } + + /** + * check if now a log rotation is neccessary. + * This object is initialised with several rotation strategy objects. + * The isRotationNeeded method checks the first rotation + * strategy object. If a rotation is needed, this result is returned. + * If not the next rotation strategy object is asked and so on. + * + * @param data the last message written to the log system + * @return boolean return true if log rotation is neccessary, else false + */ + public boolean isRotationNeeded(final String data, final File file) { + m_usedRotation = -1; + + if (null != m_strategies) { + final int length = m_strategies.length; + for (int i = 0; i < length; i++) { + if (true == m_strategies[i].isRotationNeeded(data, file)) { + m_usedRotation = i; + return true; + } + } + } + + return false; + } +} + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/RevolvingFileStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/RevolvingFileStrategy.java new file mode 100644 index 0000000..bd04661 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/RevolvingFileStrategy.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * strategy for naming log files based on appending revolving suffix. + *

+ * Heavily odified by Bruce Ritchie (Jive Software) to rotate along + * the following strategy: + *

+ * current log file will always be the base File name + * the next oldest file will be the _1 file + * the next oldest file will be the _2 file + * etc. + * + * @author Bernhard Huber + */ +public class RevolvingFileStrategy implements FileStrategy { + + ///max file prefix count + private int maxCount; + + ///the base file name. + private String baseFileName; + + public RevolvingFileStrategy(final String baseFileName, final int maxCount) { + + this.baseFileName = baseFileName; + this.maxCount = maxCount; + + if (-1 == this.maxCount) { + this.maxCount = 5; + } + } + + public File currentFile() { + return new File(baseFileName); + } + + /** + * Calculate the real file name from the base filename. + * + * @return File the calculated file name + */ + public File nextFile() { + // go through all the possible filenames and delete/rename as necessary + for (int i = maxCount; i > 0; i--) { + File test = new File(baseFileName.substring(0, baseFileName.lastIndexOf('.')) + + "_" + i + baseFileName.substring(baseFileName.lastIndexOf('.'))); + + if (i == maxCount && test.exists()) { + test.delete(); + } + + if (test.exists()) { + File r = new File(baseFileName.substring(0, baseFileName.lastIndexOf('.')) + + "_" + (i + 1) + baseFileName.substring(baseFileName.lastIndexOf('.'))); + test.renameTo(r); + } + } + + // rename the current file + File current = new File(baseFileName); + File first = new File(baseFileName.substring(0, baseFileName.lastIndexOf('.')) + + "_1" + baseFileName.substring(baseFileName.lastIndexOf('.'))); + current.renameTo(first); + + // return the base filename + return new File(baseFileName); + } +} + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategy.java new file mode 100644 index 0000000..3605fbf --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * Strategy that checks condition under which file rotation is needed. + * + * @author Bernhard Huber + */ +public interface RotateStrategy { + /** + * reset cumulative rotation history data. + * Called after rotation. + */ + void reset(); + + /** + * Check if a log rotation is neccessary at this time. + * + * @param data the serialized version of last message written to the log system + * @param file the File that we are writing to + * @return boolean return true if log rotation is neccessary, else false + */ + boolean isRotationNeeded(String data, File file); +} + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyBySize.java b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyBySize.java new file mode 100644 index 0000000..f68db41 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyBySize.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * Rotation stragety based on size written to log file. + * + * @author Bernhard Huber + */ +public class RotateStrategyBySize + implements RotateStrategy { + private long m_maxSize; + private long m_currentSize; + + /** + * Rotate logs by size. + * By default do log rotation after writing approx. 1MB of messages + */ + public RotateStrategyBySize() { + this(1024 * 1024); + } + + /** + * Rotate logs by size. + * + * @param maxSize rotate after writing max_size [byte] of messages + */ + public RotateStrategyBySize(final long maxSize) { + m_currentSize = 0; + m_maxSize = maxSize; + } + + /** + * reset log size written so far. + */ + public void reset() { + m_currentSize = 0; + } + + /** + * Check if now a log rotation is neccessary. + * + * @param data the last message written to the log system + * @return boolean return true if log rotation is neccessary, else false + */ + public boolean isRotationNeeded(final String data, final File file) { + m_currentSize += data.length(); + if (m_currentSize >= m_maxSize) { + m_currentSize = 0; + return true; + } + else { + return false; + } + } +} + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyByTime.java b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyByTime.java new file mode 100644 index 0000000..f81f37e --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/RotateStrategyByTime.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import java.io.File; + +/** + * rotation stragety based when log writting started. + * + * @author Bernhard Huber + */ +public class RotateStrategyByTime + implements RotateStrategy { + ///time interval when rotation is triggered. + private long m_timeInterval; + + ///time when logging started. + private long m_startingTime; + + ///rotation count. + private long m_currentRotation; + + /** + * Rotate logs by time. + * By default do log rotation every 24 hours + */ + public RotateStrategyByTime() { + this(1000 * 60 * 60 * 24); + } + + /** + * Rotate logs by time. + * + * @param timeInterval rotate after time-interval [ms] has expired + */ + public RotateStrategyByTime(final long timeInterval) { + m_startingTime = System.currentTimeMillis(); + m_currentRotation = 0; + m_timeInterval = timeInterval; + } + + /** + * reset interval history counters. + */ + public void reset() { + m_startingTime = System.currentTimeMillis(); + m_currentRotation = 0; + } + + /** + * Check if now a log rotation is neccessary. + * If + * (current_time - m_startingTime) / m_timeInterval > m_currentRotation + * rotation is needed. + * + * @param data the last message written to the log system + * @return boolean return true if log rotation is neccessary, else false + */ + public boolean isRotationNeeded(final String data, final File file) { + final long newRotation = + (System.currentTimeMillis() - m_startingTime) / m_timeInterval; + + if (newRotation > m_currentRotation) { + m_currentRotation = newRotation; + return true; + } + else { + return false; + } + } +} + + diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/RotatingFileTarget.java b/src/java/org/jivesoftware/util/log/output/io/rotate/RotatingFileTarget.java new file mode 100644 index 0000000..312f128 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/RotatingFileTarget.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import org.jivesoftware.util.log.format.Formatter; +import org.jivesoftware.util.log.output.io.FileTarget; +import java.io.File; +import java.io.IOException; + +/** + * This is a basic Output log target that writes to rotating files. + * + * @author Peter Donald + * @author Stephen McConnell + * @author Bernhard Huber + */ +public class RotatingFileTarget extends FileTarget { + + ///The rotation strategy to be used. + private RotateStrategy m_rotateStrategy; + + ///The file strategy to be used. + private FileStrategy m_fileStrategy; + + /** + * Construct RotatingFileTarget object. + * + * @param formatter Formatter to be used + */ + public RotatingFileTarget(final Formatter formatter, + final RotateStrategy rotateStrategy, + final FileStrategy fileStrategy) + throws IOException { + super(null, false, formatter); + + m_rotateStrategy = rotateStrategy; + m_fileStrategy = fileStrategy; + + getInitialFile(); + } + + public synchronized void rotate() + throws IOException { + close(); + + final File file = m_fileStrategy.nextFile(); + setFile(file, false); + openFile(); + } + + /** + * Output the log message, and check if rotation is needed. + */ + public synchronized void write(final String data) { + // send the log message + super.write(data); + + // if rotation is needed, close old File, create new File + final boolean rotate = + m_rotateStrategy.isRotationNeeded(data, getFile()); + if (rotate) { + try { + rotate(); + } + catch (final IOException ioe) { + getErrorHandler().error("Error rotating file", ioe, null); + } + } + } + + private void getInitialFile() throws IOException { + close(); + + boolean rotate = m_rotateStrategy.isRotationNeeded("", m_fileStrategy.currentFile()); + + if (rotate) { + setFile(m_fileStrategy.nextFile(), false); + } + else { + setFile(m_fileStrategy.currentFile(), true); + } + + openFile(); + } +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/util/log/output/io/rotate/UniqueFileStrategy.java b/src/java/org/jivesoftware/util/log/output/io/rotate/UniqueFileStrategy.java new file mode 100644 index 0000000..cee3b43 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/output/io/rotate/UniqueFileStrategy.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.output.io.rotate; + +import org.jivesoftware.util.FastDateFormat; + +import java.io.File; +import java.util.Date; + +/** + * Strategy for naming log files based on appending time suffix. + * A file name can be based on simply appending the number of miliseconds + * since (not really sure) 1/1/1970. + * Other constructors accept a pattern of a SimpleDateFormat + * to form the appended string to the base file name as well as a suffix + * which should be appended last. + *

+ * A new UniqueFileStrategy( new File("foo.", "yyyy-MM-dd", ".log" ) + * object will return File objects with file names like + * foo.2001-12-24.log + * + * @author Bernhard Huber + * @author Giacomo Pati + */ +public class UniqueFileStrategy + implements FileStrategy { + private File m_baseFile; + private File m_currentFile; + + private FastDateFormat m_formatter; + + private String m_suffix; + + public UniqueFileStrategy(final File baseFile) { + m_baseFile = baseFile; + } + + public UniqueFileStrategy(final File baseFile, String pattern) { + this(baseFile); + m_formatter = FastDateFormat.getInstance(pattern); + } + + public UniqueFileStrategy(final File baseFile, String pattern, String suffix) { + this(baseFile, pattern); + m_suffix = suffix; + } + + public File currentFile() { + return m_currentFile; + } + + /** + * Calculate the real file name from the base filename. + * + * @return File the calculated file name + */ + public File nextFile() { + final StringBuilder sb = new StringBuilder(); + sb.append(m_baseFile); + if (m_formatter == null) { + sb.append(System.currentTimeMillis()); + } + else { + final String dateString = m_formatter.format(new Date()); + sb.append(dateString); + } + + if (m_suffix != null) { + sb.append(m_suffix); + } + + m_currentFile = new File(sb.toString()); + return m_currentFile; + } +} + diff --git a/src/java/org/jivesoftware/util/log/util/DefaultErrorHandler.java b/src/java/org/jivesoftware/util/log/util/DefaultErrorHandler.java new file mode 100644 index 0000000..7706811 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/util/DefaultErrorHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.util; + +import org.jivesoftware.util.log.ErrorHandler; +import org.jivesoftware.util.log.LogEvent; + +/** + * Handle unrecoverable errors that occur during logging by + * writing to standard error. + * + * @author Peter Donald + */ +public class DefaultErrorHandler + implements ErrorHandler { + /** + * Log an unrecoverable error. + * + * @param message the error message + * @param throwable the exception associated with error (may be null) + * @param event the LogEvent that caused error, if any (may be null) + */ + public void error(final String message, + final Throwable throwable, + final LogEvent event) { + System.err.println("Logging Error: " + message); + if (null != throwable) { + throwable.printStackTrace(); + } + } +} diff --git a/src/java/org/jivesoftware/util/log/util/LoggerOutputStream.java b/src/java/org/jivesoftware/util/log/util/LoggerOutputStream.java new file mode 100644 index 0000000..906437e --- /dev/null +++ b/src/java/org/jivesoftware/util/log/util/LoggerOutputStream.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.util; + +import org.jivesoftware.util.log.Logger; +import org.jivesoftware.util.log.Priority; +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Redirect an output stream to a logger. + * This class is useful to redirect standard output or + * standard error to a Logger. An example use is + *

+ *

+ * final LoggerOutputStream outputStream =
+ *     new LoggerOutputStream( logger, Priority.DEBUG );
+ * final PrintStream output = new PrintStream( outputStream, true );
+ * 

+ * System.setOut( output ); + *

+ * + * @author Peter Donald + */ +public class LoggerOutputStream + extends OutputStream { + ///Logger that we log to + private final Logger m_logger; + + ///Log level we log to + private final Priority m_priority; + + ///The buffered output so far + private final StringBuffer m_output = new StringBuffer(); + + ///Flag set to true once stream closed + private boolean m_closed; + + /** + * Construct OutputStreamLogger to send to a particular logger at a particular priority. + * + * @param logger the logger to send to + * @param priority the priority at which to log + */ + public LoggerOutputStream(final Logger logger, + final Priority priority) { + m_logger = logger; + m_priority = priority; + } + + /** + * Shutdown stream. + */ + public void close() + throws IOException { + flush(); + super.close(); + m_closed = true; + } + + /** + * Write a single byte of data to output stream. + * + * @param data the byte of data + * @throws IOException if an error occurs + */ + public void write(final int data) + throws IOException { + checkValid(); + + //Should we properly convert char using locales etc?? + m_output.append((char)data); + + if ('\n' == data) { + flush(); + } + } + + /** + * Flush data to underlying logger. + * + * @throws IOException if an error occurs + */ + public synchronized void flush() + throws IOException { + checkValid(); + + m_logger.log(m_priority, m_output.toString()); + m_output.setLength(0); + } + + /** + * Make sure stream is valid. + * + * @throws IOException if an error occurs + */ + private void checkValid() + throws IOException { + if (true == m_closed) { + throw new EOFException("OutputStreamLogger closed"); + } + } +} diff --git a/src/java/org/jivesoftware/util/log/util/OutputStreamLogger.java b/src/java/org/jivesoftware/util/log/util/OutputStreamLogger.java new file mode 100644 index 0000000..92c2561 --- /dev/null +++ b/src/java/org/jivesoftware/util/log/util/OutputStreamLogger.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.util; + +import org.jivesoftware.util.log.Logger; +import org.jivesoftware.util.log.Priority; + +/** + * Redirect an output stream to a logger. + * This class is useful to redirect standard output or + * standard error to a Logger. An example use is + *

+ *

+ * final OutputStreamLogger outputStream =
+ *     new OutputStreamLogger( logger, Priority.DEBUG );
+ * final PrintStream output = new PrintStream( outputStream, true );
+ * 

+ * System.setOut( output ); + *

+ * + * @author Peter Donald + * @deprecated Use LoggerOutputStream as this class was misnamed. + */ +public class OutputStreamLogger + extends LoggerOutputStream { + + /** + * Construct logger to send to a particular logger at a particular priority. + * + * @param logger the logger to send to + * @param priority the priority at which to log + * @deprecated Use LoggerOutputStream as this class was misnamed. + */ + public OutputStreamLogger(final Logger logger, + final Priority priority) { + super(logger, priority); + } +} diff --git a/src/java/org/jivesoftware/util/log/util/StackIntrospector.java b/src/java/org/jivesoftware/util/log/util/StackIntrospector.java new file mode 100644 index 0000000..3dfc44d --- /dev/null +++ b/src/java/org/jivesoftware/util/log/util/StackIntrospector.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE file. + */ +package org.jivesoftware.util.log.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * A set of utilities to inspect current stack frame. + * + * @author Sylvain Wallez + * @author Stuart Roebuck + */ +public final class StackIntrospector { + /** + * Hack to get the call stack as an array of classes. The + * SecurityManager class provides it as a protected method, so + * change it to public through a new method ! + */ + private final static class CallStack + extends SecurityManager { + /** + * Returns the current execution stack as an array of classes. + * The length of the array is the number of methods on the execution + * stack. The element at index 0 is the class of the currently executing + * method, the element at index 1 is the class of that method's caller, + * and so on. + */ + public Class[] get() { + return getClassContext(); + } + } + + ///Method to cache CallStack hack as needed + private static CallStack c_callStack; + + /** + * Private constructor to block instantiation. + */ + private StackIntrospector() { + } + + /** + * Create Hack SecurityManager to get CallStack. + * + * @return the CallStack object + * @throws SecurityException if an existing SecurityManager disallows construction + * of another SecurityManager + */ + private synchronized static CallStack getCallStack() + throws SecurityException { + if (null == c_callStack) { + //Lazily create CallStack accessor as appropriate + c_callStack = new CallStack(); + } + + return c_callStack; + } + + /** + * Find the caller of the passed in Class. + * May return null if caller not found on execution stack + * + * @param clazz the Class to search for on stack to find caller of + * @return the Class of object that called parrameter class + * @throws SecurityException if an existing SecurityManager disallows construction + * of another SecurityManager and thus blocks method results + */ + public final static Class getCallerClass(final Class clazz) + throws SecurityException { + final Class[] stack = getCallStack().get(); + + // Traverse the call stack in reverse order until we find clazz + for (int i = stack.length - 1; i >= 0; i--) { + if (clazz.isAssignableFrom(stack[i])) { + // Found : the caller is the previous stack element + return stack[i + 1]; + } + } + + //Unable to locate class in call stack + return null; + } + + /** + * Get the method path name for the method from which the LogEvent was + * created, this includes the path name and the source filename and line + * number if the source was compiled with debugging on. + * + * @return The method path name in the form "the.package.path.Method" + */ + public final static String getCallerMethod(final Class clazz) { + final String className = clazz.getName(); + + //Extract stack into a StringBuffer + final StringWriter sw = new StringWriter(); + final Throwable throwable = new Throwable(); + throwable.printStackTrace(new PrintWriter(sw, true)); + final StringBuffer buffer = sw.getBuffer(); + + //Cache vars used in loop + final StringBuffer line = new StringBuffer(); + final int length = buffer.length(); + + //setup state + boolean found = false; + int state = 0; + + //parse line + for (int i = 0; i < length; i++) { + final char ch = buffer.charAt(i); + + switch (state) { + case 0: + //Strip the first line from input + if ('\n' == ch) state = 1; + break; + + case 1: + //strip 't' from 'at' + if ('t' == ch) state = 2; + break; + + case 2: + //Strip space after 'at' + line.setLength(0); + state = 3; + break; + + case 3: + //accumulate all characters to end of line + if ('\n' != ch) + line.append(ch); + else { + //At this stage you have the line that looks like + //com.biz.SomeClass.someMethod(SomeClass.java:22) + final String method = line.toString(); + + ///Determine if line is a match for class + final boolean match = method.startsWith(className); + if (!found && match) { + //If this is the first time we cound class then + //set found to true and look for caller into class + found = true; + } + else if (found && !match) { + //We have now located caller of Clazz + return method; + } + + //start parsing from start of line again + state = 1; + } + } + } + + return ""; + } + + /** + * Return the current call stack as a String, starting with the first call + * in the stack after a reference to the clazz class, and then + * display entries entries. + *

+ *

This can be useful for debugging code to determine where calls to a + * method are coming from.

+ * + * @param clazz the last class on the stack you are not interested in! + * @param entries the number of stack lines to return. + * @return The method path name in the form "the.package.path.Method" + */ + public final static String getRecentStack(final Class clazz, int entries) { + final String className = clazz.getName(); + + //Extract stack into a StringBuffer + final StringWriter sw = new StringWriter(); + final Throwable throwable = new Throwable(); + throwable.printStackTrace(new PrintWriter(sw, true)); + final StringBuffer buffer = sw.getBuffer(); + + //Cache vars used in loop + final StringBuffer line = new StringBuffer(); + final StringBuffer stack = new StringBuffer(); + final int length = buffer.length(); + + //setup state + boolean found = false; + int state = 0; + + //parse line + for (int i = 0; i < length; i++) { + final char ch = buffer.charAt(i); + + switch (state) { + case 0: + //Strip the first line from input + if ('\n' == ch) state = 1; + break; + + case 1: + //strip 't' from 'at' + if ('t' == ch) state = 2; + break; + + case 2: + //Strip space after 'at' + line.setLength(0); + state = 3; + break; + + case 3: + //accumulate all characters to end of line + if ('\n' != ch) + line.append(ch); + else { + //At this stage you have the line that looks like + //com.biz.SomeClass.someMethod(SomeClass.java:22) + final String method = line.toString(); + + ///Determine if line is a match for class + final boolean match = method.startsWith(className); + if (!found && match) { + //If this is the first time we cound class then + //set found to true and look for caller into class + found = true; + } + else if (found && !match) { + //We are looking at the callers of Clazz + stack.append(method); + entries--; + if (entries == 0) return stack.toString(); + stack.append("\n"); + } + + //start parsing from start of line again + state = 1; + } + } + } + + return ""; + } +} + diff --git a/src/java/overview.html b/src/java/overview.html new file mode 100644 index 0000000..b87ea5e --- /dev/null +++ b/src/java/overview.html @@ -0,0 +1,3 @@ + +Connection Manager lets XMPP clients connect to XMPP servers by multiplexing connections to the server. + \ No newline at end of file diff --git a/src/security/keystore b/src/security/keystore new file mode 100644 index 0000000..695e338 --- /dev/null +++ b/src/security/keystore Binary files differ diff --git a/src/security/truststore b/src/security/truststore new file mode 100644 index 0000000..43f2f5d --- /dev/null +++ b/src/security/truststore Binary files differ diff --git a/src/tools/anttask/org/jivesoftware/ant/SubDirInfoTask.java b/src/tools/anttask/org/jivesoftware/ant/SubDirInfoTask.java new file mode 100644 index 0000000..8f1e190 --- /dev/null +++ b/src/tools/anttask/org/jivesoftware/ant/SubDirInfoTask.java @@ -0,0 +1,121 @@ +/** + * $RCSfile$ + * $Revision: 1106 $ + * $Date: 2005-03-07 23:09:06 -0300 (Mon, 07 Mar 2005) $ + * + * Copyright (C) 2004 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution. + */ + +package org.jivesoftware.ant; + +import org.apache.tools.ant.Task; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; + +import java.io.File; + +/** + * A simple ant task to return the sub directories of a given dir as a comma delimited string. + * + * This class does not need jdk 1.5 to compile. + */ +public class SubDirInfoTask extends Task { + + public static final String DEFAULT_DELIM = ","; + + private File dir; + private String property; + private String delimiter; + private String ifexists; + private String except; + + public SubDirInfoTask() { + } + + public File getDir() { + return dir; + } + + public void setDir(File dir) { + this.dir = dir; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + + public String getDelimiter() { + if (delimiter == null) { + return DEFAULT_DELIM; + } + return delimiter; + } + + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + public String getIfexists() { + return ifexists; + } + + public void setIfexists(String ifexists) { + this.ifexists = ifexists; + } + + public String getExcept() { + return except; + } + + public void setExcept(String except) { + this.except = except; + } + + public void execute() throws BuildException { + // Get the siblings of the given directory, add sub directory names to the property + File[] subdirs = dir.listFiles(); + StringBuffer buf = new StringBuffer(); + String value = null; + String sep = ""; + if (subdirs != null) { + for (int i=0; i 0) { + value = buf.toString(); + } + if (value == null) { + log("No tokens found.", Project.MSG_DEBUG); + } + else { + log("Setting property '" + property + "' to " + value, Project.MSG_DEBUG); + if (buf.length() >= 0) { + getProject().setProperty(property, value); + } + } + } +}