diff --git a/ConnectionManager.iml b/ConnectionManager.iml
new file mode 100644
index 0000000..4a5435f
--- /dev/null
+++ b/ConnectionManager.iml
@@ -0,0 +1,80 @@
+
+
+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 @@ + + + + +
+
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 @@ + + + + +
+3.0.0 -- June 29, 2006 +
+ +
+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 @@ + + + +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
+ +tar -xzvf cmanager_3_0_0.tar.gz
+ mv cmanager /opt
Note: the .tar.gz and .zip builds do not contain a bundled Java runtime (JRE). Therefore, you must have JDK or JRE 1.5.0 (Java 5) or later installed on your system. You can check your java version by typing "java -version" at the command line and (if necessary) upgrade your Java installation by visiting http://java.sun.com.
+ +To complete the installation of Connection Manager, you'll need to +perform each of the following steps:
+This document also includes information on:
+ + + +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 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. +
+ +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.
+
Use the cmanager.bat batch file to start the connection manager. The file is located +in the bin/ directory of your Connection Manager installation. + +
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+ + +
+
+
+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
+ *
+ +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. 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. + +
+ +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 +
+ Important! -- the Connection Manager build tool needs to know + where Java is installed on your system. You must configure the "JAVA_HOME" + environment variable to point to the correct directory. Instructions on + how to set this variable for various platforms are as follows: +
+
+ export JAVA_HOME=/usr/local/jdk1.5 + +
+ The value "/usr/local/jdk1.5" should be replaced with your actual + Java directory. Be sure there are no spaces after the end of + the directory name. Do not add an extra slash after the directory name. +
+ source .profile + +
+ The JAVA_HOME variable should now be configured correctly. +
+
+
+ set JAVA_HOME=c:\jdk1.5 + +
+ The value "c:\jdk1.5" should be replaced with your actual + Java directory. Be sure there are no spaces between + the "=" sign or after the end of the directory name. Do + not add an extra slash after the directory name. +
+ +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. + +
+
+ +
+
+Linux/Unix users only:You must make the ant script
+executable. From the build directory, type:
+
+chmod u+x ant
+
+ |
+Now, invoke the build tool to compile the Connection Manager source code + +
+Windows: ant
+Unix/Linux: ./ant
+
+ +If the build tool is invoked correctly and Connection Manager compiles, you've correctly +configured your copy of the Connection Manager source distribution. + +
Finished!
+
+
+ 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 +
+
+ ant
+
+
Description:
+ +Equivalent of calling "ant jar javadoc". + +jar +
+
+ ant jar
+
+
Description:
+ +Builds Connection Manager into the target directory. You can then launch +Connection Manager using the scripts in target/bin/. +clean +
+
+ ant clean
+
+
Description:
+ +Cleans all artifacts of the build process by deleting the target/ +directory.+ +
+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. +
++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). +
++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: +
++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. +
++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 +
++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. +
++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. +
++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
++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
++Open the conf/manager.xml file in your favorite +editor and add or change the following system properties: +
++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. + +
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".
+ +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 ++ +
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!
+ +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.
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
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
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 inInputStream
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
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 inInputStream
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
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}
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 Mapsocket.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. + *
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("+ * + * 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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.
+ *
+ *
+ * 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
+ *
+ * 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 Maptrue
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("
+ *
+ * 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("
+ *
+ * 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 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: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.
+ *
+ *
+ * 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:
+ *
+ *
+ * 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.
+ *
+ * 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+ * + * 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
+ * + * 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
+ *
+ * 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 Type | Extra 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 |
+ *
+ * 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
+ * This will print the 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. Writes the Print out a {@link String}, Perfoms
+ * the necessary entity escaping and whitespace stripping. Writes the opening tag of an {@link Element},
+ * including its {@link Attribute}s
+ * but without its content. Writes the closing tag of an {@link Element}
+ * This will print a new line only if the newlines flag was set to true
+ *
+ * This will write the declaration to the given Writer.
+ * Assumes XML version 1.0 since we don't directly know.
+ * Filtering can mena that not all LogEvents get passed
+ * along chain or that the LogEvents passed alongare modified
+ * in some manner. 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.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
+ * <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
+ * <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
+ * <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, ListAttribute
to output.
+ */
+ public void write(Attribute attribute) throws IOException {
+ writeAttribute(attribute);
+
+ if ( autoFlush ) {
+ flush();
+ }
+ }
+
+
+ /** Document
to the current Writer.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();
+ }
+ }
+
+ /** {@link org.dom4j.Element}
, including
+ * its {@link Attribute}
s, and its value, and all
+ * its content (child nodes) to the current Writer.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();
+ }
+ }
+
+ /** 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 );
+ }
+ }
+
+
+ /** Element
to output.
+ */
+ public void writeOpen(Element element) throws IOException {
+ writer.write("<");
+ writer.write( element.getQualifiedName() );
+ writeAttributes(element);
+ writer.write(">");
+ }
+
+ /** 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("");
+ writer.write(target);
+ writer.write(" ");
+ writer.write(data);
+ 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("");
+ writer.write(qualifiedName);
+ 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( "" );
+ writer.write( processingInstruction.getName() );
+ writer.write( " " );
+ writer.write( processingInstruction.getText() );
+ 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);
+ }
+ }
+ }
+
+ /**
+ *
+ *
+ * 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.
+ *
+ *
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);
+ }
+ }
+ }
+}