First step in cluster integration
authorfhanik <fhanik@13f79535-47bb-0310-9956-ffa450edef68>
Tue, 22 Aug 2006 17:28:09 +0000 (17:28 +0000)
committerfhanik <fhanik@13f79535-47bb-0310-9956-ffa450edef68>
Tue, 22 Aug 2006 17:28:09 +0000 (17:28 +0000)
git-svn-id: https://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk@433703 13f79535-47bb-0310-9956-ffa450edef68

137 files changed:
build.xml
java/org/apache/catalina/ha/CatalinaCluster.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterDeployer.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterListener.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterManager.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterMessage.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterMessageBase.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterRuleSet.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterSession.java [new file with mode: 0644]
java/org/apache/catalina/ha/ClusterValve.java [new file with mode: 0644]
java/org/apache/catalina/ha/Constants.java [new file with mode: 0644]
java/org/apache/catalina/ha/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/ha/context/ReplicatedContext.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/FarmWarDeployer.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/FileChangeListener.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/FileMessage.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/FileMessageFactory.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/UndeployMessage.java [new file with mode: 0644]
java/org/apache/catalina/ha/deploy/WarWatcher.java [new file with mode: 0644]
java/org/apache/catalina/ha/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/ha/package.html [new file with mode: 0644]
java/org/apache/catalina/ha/session/BackupManager.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/ClusterManagerBase.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/ClusterSessionListener.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/Constants.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/DeltaManager.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/DeltaRequest.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/DeltaSession.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/JvmRouteBinderValve.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/ha/session/ReplicatedSession.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/SerializablePrincipal.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/SessionIDMessage.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/SessionMessage.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/SessionMessageImpl.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java [new file with mode: 0644]
java/org/apache/catalina/ha/session/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/Constants.java [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/ReplicationValve.java [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/SendMessageData.java [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java [new file with mode: 0644]
java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/ha/util/IDynamicProperty.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ByteMessage.java [new file with mode: 0644]
java/org/apache/catalina/tribes/Channel.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelException.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelListener.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelMessage.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelReceiver.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ChannelSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/Constants.java [new file with mode: 0644]
java/org/apache/catalina/tribes/ErrorHandler.java [new file with mode: 0644]
java/org/apache/catalina/tribes/Heartbeat.java [new file with mode: 0644]
java/org/apache/catalina/tribes/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/tribes/ManagedChannel.java [new file with mode: 0644]
java/org/apache/catalina/tribes/Member.java [new file with mode: 0644]
java/org/apache/catalina/tribes/MembershipListener.java [new file with mode: 0644]
java/org/apache/catalina/tribes/MembershipService.java [new file with mode: 0644]
java/org/apache/catalina/tribes/MessageListener.java [new file with mode: 0644]
java/org/apache/catalina/tribes/RemoteProcessException.java [new file with mode: 0644]
java/org/apache/catalina/tribes/UniqueId.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/AbsoluteOrder.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/ChannelCoordinator.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/GroupChannel.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/InterceptorPayload.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/Response.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/RpcCallback.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/RpcChannel.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/RpcMessage.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/OrderInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/BufferPool.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/BufferPool14Impl.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/BufferPool15Impl.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/ChannelData.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/DirectByteArrayOutputStream.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/ListenCallback.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/ObjectReader.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/ReplicationStream.java [new file with mode: 0644]
java/org/apache/catalina/tribes/io/XByteBuffer.java [new file with mode: 0644]
java/org/apache/catalina/tribes/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/Constants.java [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/McastService.java [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/McastServiceImpl.java [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/MemberImpl.java [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/Membership.java [new file with mode: 0644]
java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/tribes/package.html [new file with mode: 0644]
java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java [new file with mode: 0644]
java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java [new file with mode: 0644]
java/org/apache/catalina/tribes/tipis/ReplicatedMap.java [new file with mode: 0644]
java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java [new file with mode: 0644]
java/org/apache/catalina/tribes/tipis/Streamable.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/AbstractSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/Constants.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/DataSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/LocalStrings.properties [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/MultiPointSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/PooledSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/ReceiverBase.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/SenderState.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/ThreadPool.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/WorkerThread.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/BioReceiver.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/BioSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/MultipointBioSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/PooledMultiSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/nio/NioReceiver.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/nio/NioSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java [new file with mode: 0644]
java/org/apache/catalina/tribes/util/Arrays.java [new file with mode: 0644]
java/org/apache/catalina/tribes/util/Logs.java [new file with mode: 0644]
java/org/apache/catalina/tribes/util/StringManager.java [new file with mode: 0644]
java/org/apache/catalina/tribes/util/UUIDGenerator.java [new file with mode: 0644]

index 2d6185c..9657b1a 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -42,6 +42,8 @@
   <property name="jsp-api.jar" value="${tomcat.build}/lib/jsp-api.jar"/>
   <property name="el-api.jar" value="${tomcat.build}/lib/el-api.jar"/>
   <property name="catalina.jar" value="${tomcat.build}/lib/catalina.jar"/>
+  <property name="catalina-tribes.jar" value="${tomcat.build}/lib/catalina-tribes.jar"/>
+  <property name="catalina-ha.jar" value="${tomcat.build}/lib/catalina-ha.jar"/>
   <property name="catalina-ant.jar" value="${tomcat.build}/lib/catalina-ant.jar"/>
   <property name="catalina-ant-jmx.jar" value="${tomcat.build}/lib/catalina-ant-jmx.jar"/>
   <property name="tomcat-coyote.jar" value="${tomcat.build}/lib/tomcat-coyote.jar"/>
         <!-- Modules -->
         <exclude name="org/apache/catalina/ant/**" />
         <exclude name="org/apache/catalina/cluster/**" />
+        <exclude name="org/apache/catalina/ha/**" />
+        <exclude name="org/apache/catalina/tribes/**" />
         <exclude name="org/apache/catalina/launcher/**" />
         <exclude name="org/apache/catalina/storeconfig/**" />
       </fileset>
     </jar>
+    
+    <!-- Catalina GroupCom/Tribes JAR File -->
+    <jar jarfile="${catalina-tribes.jar}">
+      <fileset dir="${tomcat.classes}">
+        <exclude name="**/package.html" />
+        <exclude name="**/LocalStrings_*" />
+        <!-- Modules -->
+        <include name="org/apache/catalina/tribes/**" />
+      </fileset>
+    </jar>
+    <!-- Catalina Cluster/HA JAR File -->
+    <jar jarfile="${catalina-ha.jar}">
+      <fileset dir="${tomcat.classes}">
+        <exclude name="**/package.html" />
+        <exclude name="**/LocalStrings_*" />
+        <!-- Modules -->
+        <include name="org/apache/catalina/ha/**" />
+      </fileset>
+    </jar>
 
     <!-- Catalina Ant Tasks JAR File -->
     <jar jarfile="${catalina-ant.jar}">
diff --git a/java/org/apache/catalina/ha/CatalinaCluster.java b/java/org/apache/catalina/ha/CatalinaCluster.java
new file mode 100644 (file)
index 0000000..d4a9e6e
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha;
+
+import java.util.Map;
+
+import org.apache.catalina.Cluster;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Manager;
+import org.apache.catalina.Valve;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.Member;
+import org.apache.commons.logging.Log;
+
+
+
+/**
+ * A <b>CatalinaCluster</b> interface allows to plug in and out the 
+ * different cluster implementations
+ *
+ * @author Filip Hanik
+ * @version $Revision: 379550 $, $Date: 2006-02-21 12:06:35 -0600 (Tue, 21 Feb 2006) $
+ */
+
+public interface CatalinaCluster extends Cluster {
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * Descriptive information about this component implementation.
+     */
+    public String info = "CatalinaCluster/2.0";
+    
+    /**
+     * Start the cluster, the owning container will invoke this
+     * @throws Exception - if failure to start cluster
+     */
+    public void start() throws Exception;
+    
+    /**
+     * Stops the cluster, the owning container will invoke this
+     * @throws LifecycleException
+     */
+    public void stop() throws LifecycleException;
+    
+    /**
+     * Returns the associates logger with this cluster.
+     *
+     * @return Log
+     */
+    public Log getLogger();
+    
+    /**
+     * Sends a message to all the members in the cluster
+     * @param msg ClusterMessage
+     */
+    public void send(ClusterMessage msg);
+    
+    /**
+     * Sends a message to a specific member in the cluster.
+     *
+     * @param msg ClusterMessage
+     * @param dest Member
+     */
+    public void send(ClusterMessage msg, Member dest);
+    
+    /**
+     * Sends a message to a all members at local cluster domain
+     *
+     * @param msg ClusterMessage
+     */
+    public void sendClusterDomain(ClusterMessage msg);
+
+    /**
+     * Returns that cluster has members.
+     */
+    public boolean hasMembers();
+
+    /**
+     * Returns all the members currently participating in the cluster.
+     *
+     * @return Member[]
+     */
+    public Member[] getMembers();
+    
+    /**
+     * Return the member that represents this node.
+     *
+     * @return Member
+     */
+    public Member getLocalMember();
+    
+    public void addValve(Valve valve);
+    
+    public void addClusterListener(ClusterListener listener);
+    
+    public void removeClusterListener(ClusterListener listener);
+    
+    public void setClusterDeployer(ClusterDeployer deployer);
+    
+    public ClusterDeployer getClusterDeployer();
+    
+    /**
+     * @return The map of managers
+     */
+    public Map getManagers();
+
+    public Manager getManager(String name);
+    public void removeManager(String name,Manager manager);
+    public void addManager(String name,Manager manager);
+    public String getManagerName(String name, Manager manager);
+    public Valve[] getValves();
+    
+    public void setChannel(Channel channel);
+    public Channel getChannel();
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterDeployer.java b/java/org/apache/catalina/ha/ClusterDeployer.java
new file mode 100644 (file)
index 0000000..39c9322
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha;
+
+/**
+ * A <b>ClusterDeployer</b> interface allows to plug in and out the
+ * different deployment implementations
+ *
+ * @author Filip Hanik
+ * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $
+ */
+import org.apache.catalina.LifecycleException;
+import java.io.IOException;
+import java.net.URL;
+import org.apache.catalina.tribes.ChannelListener;
+
+public interface ClusterDeployer extends ChannelListener {
+    /**
+     * Descriptive information about this component implementation.
+     */
+    public String info = "ClusterDeployer/1.0";
+    /**
+     * Start the cluster deployer, the owning container will invoke this
+     * @throws Exception - if failure to start cluster
+     */
+    public void start() throws Exception;
+
+    /**
+     * Stops the cluster deployer, the owning container will invoke this
+     * @throws LifecycleException
+     */
+    public void stop() throws LifecycleException;
+
+    /**
+     * Sets the deployer for this cluster deployer to use.
+     * @param deployer Deployer
+     */
+    // FIXME
+    //public void setDeployer(Deployer deployer);
+
+    /**
+     * Install a new web application, whose web application archive is at the
+     * specified URL, into this container and all the other
+     * members of the cluster with the specified context path.
+     * A context path of "" (the empty string) should be used for the root
+     * application for this container.  Otherwise, the context path must
+     * start with a slash.
+     * <p>
+     * If this application is successfully installed locally, 
+     * a ContainerEvent of type
+     * <code>INSTALL_EVENT</code> will be sent to all registered listeners,
+     * with the newly created <code>Context</code> as an argument.
+     *
+     * @param contextPath The context path to which this application should
+     *  be installed (must be unique)
+     * @param war A URL of type "jar:" that points to a WAR file, or type
+     *  "file:" that points to an unpacked directory structure containing
+     *  the web application to be installed
+     *
+     * @exception IllegalArgumentException if the specified context path
+     *  is malformed (it must be "" or start with a slash)
+     * @exception IllegalStateException if the specified context path
+     *  is already attached to an existing web application
+     * @exception IOException if an input/output error was encountered
+     *  during installation
+     */
+    public void install(String contextPath, URL war) throws IOException;
+
+    /**
+     * Remove an existing web application, attached to the specified context
+     * path.  If this application is successfully removed, a
+     * ContainerEvent of type <code>REMOVE_EVENT</code> will be sent to all
+     * registered listeners, with the removed <code>Context</code> as
+     * an argument. Deletes the web application war file and/or directory
+     * if they exist in the Host's appBase.
+     *
+     * @param contextPath The context path of the application to be removed
+     * @param undeploy boolean flag to remove web application from server
+     *
+     * @exception IllegalArgumentException if the specified context path
+     *  is malformed (it must be "" or start with a slash)
+     * @exception IllegalArgumentException if the specified context path does
+     *  not identify a currently installed web application
+     * @exception IOException if an input/output error occurs during
+     *  removal
+     */
+    public void remove(String contextPath, boolean undeploy) throws IOException;
+
+    /**
+     * call from container Background Process
+     */
+    public void backgroundProcess();
+    
+    /**
+     * Returns the cluster the cluster deployer is associated with
+     * @return CatalinaCluster
+     */
+    public CatalinaCluster getCluster();
+
+    /**
+     * Associates the cluster deployer with a cluster
+     * @param cluster CatalinaCluster
+     */
+    public void setCluster(CatalinaCluster cluster);
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterListener.java b/java/org/apache/catalina/ha/ClusterListener.java
new file mode 100644 (file)
index 0000000..6d7aa60
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha;
+
+
+
+
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.util.StringManager;
+import java.io.Serializable;
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * Receive SessionID cluster change from other backup node after primary session
+ * node is failed.
+ * 
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public abstract class ClusterListener implements ChannelListener {
+
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ClusterListener.class);
+
+
+    //--Instance Variables--------------------------------------
+
+    /**
+     * The string manager for this package.
+     */
+    protected StringManager sm = StringManager.getManager(Constants.Package);
+
+    protected CatalinaCluster cluster = null;
+
+    //--Constructor---------------------------------------------
+
+    public ClusterListener() {
+    }
+    
+    //--Instance Getters/Setters--------------------------------
+    
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(CatalinaCluster cluster) {
+        if (log.isDebugEnabled()) {
+            if (cluster != null)
+                log.debug("add ClusterListener " + this.toString() + " to cluster" + cluster);
+            else
+                log.debug("remove ClusterListener " + this.toString() + " from cluster");
+        }
+        this.cluster = cluster;
+    }
+
+    public boolean equals(Object listener) {
+        return super.equals(listener);
+    }
+
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    //--Logic---------------------------------------------------
+
+    public final void messageReceived(Serializable msg, Member member) {
+        if ( msg instanceof ClusterMessage ) messageReceived((ClusterMessage)msg);
+    }
+    public final boolean accept(Serializable msg, Member member) {
+        if ( msg instanceof ClusterMessage ) return true;
+        return false;
+    }
+
+
+
+    /**
+     * Callback from the cluster, when a message is received, The cluster will
+     * broadcast it invoking the messageReceived on the receiver.
+     * 
+     * @param msg
+     *            ClusterMessage - the message received from the cluster
+     */
+    public abstract void messageReceived(ClusterMessage msg) ;
+    
+
+    /**
+     * Accept only SessionIDMessages
+     * 
+     * @param msg
+     *            ClusterMessage
+     * @return boolean - returns true to indicate that messageReceived should be
+     *         invoked. If false is returned, the messageReceived method will
+     *         not be invoked.
+     */
+    public abstract boolean accept(ClusterMessage msg) ;
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterManager.java b/java/org/apache/catalina/ha/ClusterManager.java
new file mode 100644 (file)
index 0000000..67e662d
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha;
+
+
+import org.apache.catalina.Manager;
+import java.io.IOException;
+import org.apache.catalina.tribes.io.ReplicationStream;
+
+
+/**
+ * The common interface used by all cluster manager.
+ * This is so that we can have a more pluggable way
+ * of swapping session managers for different algorithms.
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ */
+public interface ClusterManager extends Manager {
+
+   /**
+    * A message was received from another node, this
+    * is the callback method to implement if you are interested in
+    * receiving replication messages.
+    * @param msg - the message received.
+    */
+   public void messageDataReceived(ClusterMessage msg);
+
+   /**
+    * When the request has been completed, the replication valve
+    * will notify the manager, and the manager will decide whether
+    * any replication is needed or not.
+    * If there is a need for replication, the manager will
+    * create a session message and that will be replicated.
+    * The cluster determines where it gets sent.
+    * @param sessionId - the sessionId that just completed.
+    * @return a SessionMessage to be sent.
+    */
+   public ClusterMessage requestCompleted(String sessionId);
+
+   /**
+    * When the manager expires session not tied to a request.
+    * The cluster will periodically ask for a list of sessions
+    * that should expire and that should be sent across the wire.
+    * @return String[] The invalidated sessions
+    */
+   public String[] getInvalidatedSessions();
+   
+   /**
+    * Return the name of the manager, at host /context name and at engine hostname+/context.
+    * @return String
+    * @since 5.5.10
+    */
+   public String getName();
+   
+   /**
+    * Set the name of the manager, at host /context name and at engine hostname+/context
+    * @param name
+    * @since 5.5.10
+    */
+   public void setName(String name);
+         
+   public CatalinaCluster getCluster();
+
+   public void setCluster(CatalinaCluster cluster);
+   
+   /**
+    * @return Manager send only to same cluster domain.
+    * @since 5.5.10
+    */
+   public boolean isSendClusterDomainOnly();
+
+   /**
+    * @param sendClusterDomainOnly Flag value.
+    * @since 5.5.10
+    */
+   public void setSendClusterDomainOnly(boolean sendClusterDomainOnly);
+
+   /**
+    * @param mode The mode
+    * @since 5.5.10
+    */
+   public void setDefaultMode(boolean mode);
+
+   /**
+    * @since 5.5.10
+    */
+   public boolean isDefaultMode();
+   
+   public ReplicationStream getReplicationStream(byte[] data) throws IOException;
+
+   public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException;
+   
+   public boolean isNotifyListenersOnReplication();
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterMessage.java b/java/org/apache/catalina/ha/ClusterMessage.java
new file mode 100644 (file)
index 0000000..d33f9e8
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999,2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha;
+
+import java.io.Serializable;
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * @author Filip Hanik
+ * 
+ */
+public interface ClusterMessage extends Serializable {
+    public Member getAddress();
+    public void setAddress(Member member);
+    public String getUniqueId();
+    public void setUniqueId(String id);
+    public long getTimestamp();
+    public void setTimestamp(long timestamp);
+}
diff --git a/java/org/apache/catalina/ha/ClusterMessageBase.java b/java/org/apache/catalina/ha/ClusterMessageBase.java
new file mode 100644 (file)
index 0000000..63be811
--- /dev/null
@@ -0,0 +1,61 @@
+package org.apache.catalina.ha;
+
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class ClusterMessageBase implements ClusterMessage {
+    
+    protected transient Member address;
+    private String uniqueId;
+    private long timestamp;
+    public ClusterMessageBase() {
+    }
+
+    /**
+     * getAddress
+     *
+     * @return Member
+     * @todo Implement this org.apache.catalina.ha.ClusterMessage method
+     */
+    public Member getAddress() {
+        return address;
+    }
+
+    public String getUniqueId() {
+        return uniqueId;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    /**
+     * setAddress
+     *
+     * @param member Member
+     * @todo Implement this org.apache.catalina.ha.ClusterMessage method
+     */
+    public void setAddress(Member member) {
+        this.address = member;
+    }
+
+    public void setUniqueId(String uniqueId) {
+        this.uniqueId = uniqueId;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/ClusterRuleSet.java b/java/org/apache/catalina/ha/ClusterRuleSet.java
new file mode 100644 (file)
index 0000000..1f5ab4a
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Copyright 1999-2001,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha;
+
+
+import org.apache.tomcat.util.digester.Digester;
+import org.apache.tomcat.util.digester.RuleSetBase;
+
+
+/**
+ * <p><strong>RuleSet</strong> for processing the contents of a
+ * Cluster definition element.  </p>
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 387285 $ $Date: 2006-03-20 13:30:50 -0600 (Mon, 20 Mar 2006) $
+ */
+
+public class ClusterRuleSet extends RuleSetBase {
+
+
+    // ----------------------------------------------------- Instance Variables
+
+
+    /**
+     * The matching pattern prefix to use for recognizing our elements.
+     */
+    protected String prefix = null;
+
+
+    // ------------------------------------------------------------ Constructor
+
+
+    /**
+     * Construct an instance of this <code>RuleSet</code> with the default
+     * matching pattern prefix.
+     */
+    public ClusterRuleSet() {
+
+        this("");
+
+    }
+
+
+    /**
+     * Construct an instance of this <code>RuleSet</code> with the specified
+     * matching pattern prefix.
+     *
+     * @param prefix Prefix for matching pattern rules (including the
+     *  trailing slash character)
+     */
+    public ClusterRuleSet(String prefix) {
+        super();
+        this.namespaceURI = null;
+        this.prefix = prefix;
+    }
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /**
+     * <p>Add the set of Rule instances defined in this RuleSet to the
+     * specified <code>Digester</code> instance, associating them with
+     * our namespace URI (if any).  This method should only be called
+     * by a Digester instance.</p>
+     *
+     * @param digester Digester instance to which the new Rule instances
+     *  should be added.
+     */
+    public void addRuleInstances(Digester digester) {
+        //Cluster configuration start
+        
+        digester.addObjectCreate(prefix + "Channel",
+                                 null, // MUST be specified in the element
+                                 "className");
+        digester.addSetProperties(prefix + "Channel");
+        digester.addSetNext(prefix + "Channel",
+                            "setChannel",
+                            "org.apache.catalina.tribes.Channel");
+
+
+        String channelPrefix = prefix + "Channel/";
+        { //channel properties
+            digester.addObjectCreate(channelPrefix + "Membership",
+                                     null, // MUST be specified in the element
+                                     "className");
+            digester.addSetProperties(channelPrefix + "Membership");
+            digester.addSetNext(channelPrefix + "Membership",
+                                "setMembershipService",
+                                "org.apache.catalina.tribes.MembershipService");
+
+            digester.addObjectCreate(channelPrefix + "Sender",
+                                     null, // MUST be specified in the element
+                                     "className");
+            digester.addSetProperties(channelPrefix + "Sender");
+            digester.addSetNext(channelPrefix + "Sender",
+                                "setChannelSender",
+                                "org.apache.catalina.tribes.ChannelSender");
+
+            digester.addObjectCreate(channelPrefix + "Sender/Transport",
+                                     null, // MUST be specified in the element
+                                     "className");
+            digester.addSetProperties(channelPrefix + "Sender/Transport");
+            digester.addSetNext(channelPrefix + "Sender/Transport",
+                                "setTransport",
+                                "org.apache.catalina.tribes.tcp.MultiPointSender");
+
+
+            digester.addObjectCreate(channelPrefix + "Receiver",
+                                     null, // MUST be specified in the element
+                                     "className");
+            digester.addSetProperties(channelPrefix + "Receiver");
+            digester.addSetNext(channelPrefix + "Receiver",
+                                "setChannelReceiver",
+                                "org.apache.catalina.tribes.ChannelReceiver");
+
+            digester.addObjectCreate(channelPrefix + "Interceptor",
+                                     null, // MUST be specified in the element
+                                     "className");
+            digester.addSetProperties(channelPrefix + "Interceptor");
+            digester.addSetNext(channelPrefix + "Interceptor",
+                                "addInterceptor",
+                                "org.apache.catalina.tribes.ChannelInterceptor");
+        }
+
+        digester.addObjectCreate(prefix + "Valve",
+                                 null, // MUST be specified in the element
+                                 "className");
+        digester.addSetProperties(prefix + "Valve");
+        digester.addSetNext(prefix + "Valve",
+                            "addValve",
+                            "org.apache.catalina.Valve");
+        
+        digester.addObjectCreate(prefix + "Deployer",
+                                 null, // MUST be specified in the element
+                                 "className");
+        digester.addSetProperties(prefix + "Deployer");
+        digester.addSetNext(prefix + "Deployer",
+                            "setClusterDeployer",
+                            "org.apache.catalina.ha.ClusterDeployer");
+        
+        digester.addObjectCreate(prefix + "Listener",
+                null, // MUST be specified in the element
+                "className");
+        digester.addSetProperties(prefix + "Listener");
+        digester.addSetNext(prefix + "Listener",
+                            "addLifecycleListener",
+                            "org.apache.catalina.LifecycleListener");
+        
+        digester.addObjectCreate(prefix + "ClusterListener",
+                null, // MUST be specified in the element
+                "className");
+        digester.addSetProperties(prefix + "ClusterListener");
+        digester.addSetNext(prefix + "ClusterListener",
+                            "addClusterListener",
+                            "org.apache.catalina.ha.ClusterListener");
+        //Cluster configuration end
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterSession.java b/java/org/apache/catalina/ha/ClusterSession.java
new file mode 100644 (file)
index 0000000..8eb8978
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha;
+
+import org.apache.catalina.Session;
+import javax.servlet.http.HttpSession;
+
+public interface ClusterSession extends Session, HttpSession {
+   /**
+    * returns true if this session is the primary session, if that is the
+    * case, the manager can expire it upon timeout.
+    * @return True if this session is primary
+    */
+   public boolean isPrimarySession();
+
+   /**
+    * Sets whether this is the primary session or not.
+    * @param primarySession Flag value
+    */
+   public void setPrimarySession(boolean primarySession);
+   
+   
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterValve.java b/java/org/apache/catalina/ha/ClusterValve.java
new file mode 100644 (file)
index 0000000..d82f6df
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha;
+
+/**
+ * Cluster Valve Interface to mark all Cluster Valves 
+ * Only those Valve can'be configured as Cluster Valves
+ * @author Peter Rossbach
+ * @version $Revision: 303842 $, $Date: 2005-04-10 11:20:46 -0500 (Sun, 10 Apr 2005) $
+ */
+public interface ClusterValve {
+    /**
+     * Returns the cluster the cluster deployer is associated with
+     * @return CatalinaCluster
+     */
+    public CatalinaCluster getCluster();
+
+    /**
+     * Associates the cluster deployer with a cluster
+     * @param cluster CatalinaCluster
+     */
+    public void setCluster(CatalinaCluster cluster);
+}
diff --git a/java/org/apache/catalina/ha/Constants.java b/java/org/apache/catalina/ha/Constants.java
new file mode 100644 (file)
index 0000000..7b7f041
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha;
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.ha</code>
+ * package.
+ *
+ * @author Bip Thelin
+ * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $
+ */
+
+public final class Constants {
+    public static final String Package = "org.apache.catalina.ha";
+}
diff --git a/java/org/apache/catalina/ha/LocalStrings.properties b/java/org/apache/catalina/ha/LocalStrings.properties
new file mode 100644 (file)
index 0000000..0dafa46
--- /dev/null
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/ha/context/ReplicatedContext.java b/java/org/apache/catalina/ha/context/ReplicatedContext.java
new file mode 100644 (file)
index 0000000..680fae0
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.context;
+
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.tribes.tipis.ReplicatedMap;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.Loader;
+import org.apache.catalina.core.ApplicationContext;
+import org.apache.catalina.Globals;
+import javax.servlet.ServletContext;
+import java.util.HashMap;
+import org.apache.catalina.tribes.tipis.LazyReplicatedMap;
+
+/**
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class ReplicatedContext extends StandardContext {
+    private int mapSendOptions = Channel.SEND_OPTIONS_DEFAULT;
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( ReplicatedContext.class );
+
+    protected static long DEFAULT_REPL_TIMEOUT = 15000;//15 seconds
+    
+
+
+    public synchronized void start() throws LifecycleException {
+        if ( this.started ) return;
+        try {
+            CatalinaCluster catclust = (CatalinaCluster)this.getCluster();
+            if (this.context == null) this.context = new ReplApplContext(this.getBasePath(), this);
+            if ( catclust != null ) {
+                ReplicatedMap map = new ReplicatedMap(this,catclust.getChannel(),DEFAULT_REPL_TIMEOUT,
+                                                      getName(),getClassLoaders());
+                map.setChannelSendOptions(mapSendOptions);
+                ((ReplApplContext)this.context).setAttributeMap(map);
+                if (getAltDDName() != null) context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName());
+            }
+            super.start();
+        }  catch ( Exception x ) {
+            log.error("Unable to start ReplicatedContext",x);
+            throw new LifecycleException("Failed to start ReplicatedContext",x);
+        }
+    }
+    
+    public synchronized void stop() throws LifecycleException
+    {
+        ReplicatedMap map = (ReplicatedMap)((ReplApplContext)this.context).getAttributeMap();
+        if ( map!=null ) {
+            map.breakdown();
+        }
+        if ( !this.started ) return;
+        try {
+        } catch ( Exception x ){
+            log.error("Unable to stop ReplicatedContext",x);
+            throw new LifecycleException("Failed to stop ReplicatedContext",x);
+        } finally {
+            super.stop();
+        }
+
+    }
+
+
+    public void setMapSendOptions(int mapSendOptions) {
+        this.mapSendOptions = mapSendOptions;
+    }
+
+    public int getMapSendOptions() {
+        return mapSendOptions;
+    }
+    
+    public ClassLoader[] getClassLoaders() {
+        Loader loader = null;
+        ClassLoader classLoader = null;
+        loader = this.getLoader();
+        if (loader != null) classLoader = loader.getClassLoader();
+        if ( classLoader == null ) classLoader = Thread.currentThread().getContextClassLoader();
+        if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+            return new ClassLoader[] {classLoader};
+        } else {
+            return new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()};
+        }
+    }
+    
+    public ServletContext getServletContext() {
+        return ((ReplApplContext)context).getFacade();
+
+    }
+
+    
+    protected static class ReplApplContext extends ApplicationContext {
+        public ReplApplContext(String basePath, StandardContext context) {
+            super(basePath,context);
+        }
+        
+         protected ServletContext getFacade() {
+             return super.getFacade();
+        }
+        
+        public HashMap getAttributeMap() {
+            return (HashMap)this.attributes;
+        }
+        public void setAttributeMap(HashMap map) {
+            this.attributes = map;
+        }
+
+    }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java b/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java
new file mode 100644 (file)
index 0000000..98b6b44
--- /dev/null
@@ -0,0 +1,741 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Host;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterDeployer;
+import org.apache.catalina.ha.ClusterListener;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.tomcat.util.modeler.Registry;
+
+
+/**
+ * <p>
+ * A farm war deployer is a class that is able to deploy/undeploy web
+ * applications in WAR form within the cluster.
+ * </p>
+ * Any host can act as the admin, and will have three directories
+ * <ul>
+ * <li>deployDir - the directory where we watch for changes</li>
+ * <li>applicationDir - the directory where we install applications</li>
+ * <li>tempDir - a temporaryDirectory to store binary data when downloading a
+ * war from the cluster</li>
+ * </ul>
+ * Currently we only support deployment of WAR files since they are easier to
+ * send across the wire.
+ * 
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 390639 $
+ */
+public class FarmWarDeployer extends ClusterListener implements ClusterDeployer, FileChangeListener {
+    /*--Static Variables----------------------------------------*/
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+            .getLog(FarmWarDeployer.class);
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "FarmWarDeployer/1.2";
+
+    /*--Instance Variables--------------------------------------*/
+    protected CatalinaCluster cluster = null;
+
+    protected boolean started = false; //default 5 seconds
+
+    protected HashMap fileFactories = new HashMap();
+
+    protected String deployDir;
+
+    protected String tempDir;
+
+    protected String watchDir;
+
+    protected boolean watchEnabled = false;
+
+    protected WarWatcher watcher = null;
+
+    /**
+     * Iteration count for background processing.
+     */
+    private int count = 0;
+
+    /**
+     * Frequency of the Farm watchDir check. Cluster wide deployment will be
+     * done once for the specified amount of backgrondProcess calls (ie, the
+     * lower the amount, the most often the checks will occur).
+     */
+    protected int processDeployFrequency = 2;
+
+    /**
+     * Path where context descriptors should be deployed.
+     */
+    protected File configBase = null;
+
+    /**
+     * The associated host.
+     */
+    protected Host host = null;
+
+    /**
+     * The host appBase.
+     */
+    protected File appBase = null;
+
+    /**
+     * MBean server.
+     */
+    protected MBeanServer mBeanServer = null;
+
+    /**
+     * The associated deployer ObjectName.
+     */
+    protected ObjectName oname = null;
+
+    /*--Constructor---------------------------------------------*/
+    public FarmWarDeployer() {
+    }
+
+    /**
+     * Return descriptive information about this deployer implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+
+    /*--Logic---------------------------------------------------*/
+    public void start() throws Exception {
+        if (started)
+            return;
+        getCluster().addClusterListener(this);
+        if (watchEnabled) {
+            watcher = new WarWatcher(this, new File(getWatchDir()));
+            if (log.isInfoEnabled())
+                log.info("Cluster deployment is watching " + getWatchDir()
+                        + " for changes.");
+        }
+
+        // Check to correct engine and host setup
+        host = (Host) getCluster().getContainer();
+        Engine engine = (Engine) host.getParent();
+        try {
+            oname = new ObjectName(engine.getName() + ":type=Deployer,host="
+                    + host.getName());
+        } catch (Exception e) {
+            log.error("Can't construct MBean object name" + e);
+        }
+        configBase = new File(System.getProperty("catalina.base"), "conf");
+        if (engine != null) {
+            configBase = new File(configBase, engine.getName());
+        }
+        if (host != null) {
+            configBase = new File(configBase, host.getName());
+        }
+
+        // Retrieve the MBean server
+        mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
+
+        started = true;
+        count = 0;
+        if (log.isInfoEnabled())
+            log.info("Cluster FarmWarDeployer started.");
+    }
+
+    /*
+     * stop cluster wide deployments
+     * 
+     * @see org.apache.catalina.cluster.ClusterDeployer#stop()
+     */
+    public void stop() throws LifecycleException {
+        started = false;
+        getCluster().removeClusterListener(this);
+        count = 0;
+        if (watcher != null) {
+            watcher.clear();
+            watcher = null;
+
+        }
+        if (log.isInfoEnabled())
+            log.info("Cluster FarmWarDeployer stopped.");
+    }
+
+    public void cleanDeployDir() {
+        throw new java.lang.UnsupportedOperationException(
+                "Method cleanDeployDir() not yet implemented.");
+    }
+
+    /**
+     * Callback from the cluster, when a message is received, The cluster will
+     * broadcast it invoking the messageReceived on the receiver.
+     * 
+     * @param msg
+     *            ClusterMessage - the message received from the cluster
+     */
+    public void messageReceived(ClusterMessage msg) {
+        try {
+            if (msg instanceof FileMessage && msg != null) {
+                FileMessage fmsg = (FileMessage) msg;
+                if (log.isDebugEnabled())
+                    log.debug("receive cluster deployment [ path: "
+                            + fmsg.getContextPath() + " war:  "
+                            + fmsg.getFileName() + " ]");
+                FileMessageFactory factory = getFactory(fmsg);
+                // TODO correct second try after app is in service!
+                if (factory.writeMessage(fmsg)) {
+                    //last message received war file is completed
+                    String name = factory.getFile().getName();
+                    if (!name.endsWith(".war"))
+                        name = name + ".war";
+                    File deployable = new File(getDeployDir(), name);
+                    try {
+                        String path = fmsg.getContextPath();
+                        if (!isServiced(path)) {
+                            addServiced(path);
+                            try {
+                                remove(path);
+                                factory.getFile().renameTo(deployable);
+                                check(path);
+                            } finally {
+                                removeServiced(path);
+                            }
+                            if (log.isDebugEnabled())
+                                log.debug("deployment from " + path
+                                        + " finished.");
+                        } else
+                            log.error("Application " + path
+                                    + " in used. touch war file " + name
+                                    + " again!");
+                    } catch (Exception ex) {
+                        log.error(ex);
+                    } finally {
+                        removeFactory(fmsg);
+                    }
+                }
+            } else if (msg instanceof UndeployMessage && msg != null) {
+                try {
+                    UndeployMessage umsg = (UndeployMessage) msg;
+                    String path = umsg.getContextPath();
+                    if (log.isDebugEnabled())
+                        log.debug("receive cluster undeployment from " + path);
+                    if (!isServiced(path)) {
+                        addServiced(path);
+                        try {
+                            remove(path);
+                        } finally {
+                            removeServiced(path);
+                        }
+                        if (log.isDebugEnabled())
+                            log.debug("undeployment from " + path
+                                    + " finished.");
+                    } else
+                        log.error("Application "
+                            + path
+                            + " in used. Sorry not remove from backup cluster nodes!");
+                } catch (Exception ex) {
+                    log.error(ex);
+                }
+            }
+        } catch (java.io.IOException x) {
+            log.error("Unable to read farm deploy file message.", x);
+        }
+    }
+
+    /**
+     * create factory for all transported war files
+     * 
+     * @param msg
+     * @return Factory for all app message (war files)
+     * @throws java.io.FileNotFoundException
+     * @throws java.io.IOException
+     */
+    public synchronized FileMessageFactory getFactory(FileMessage msg)
+            throws java.io.FileNotFoundException, java.io.IOException {
+        File tmpFile = new File(msg.getFileName());
+        File writeToFile = new File(getTempDir(), tmpFile.getName());
+        FileMessageFactory factory = (FileMessageFactory) fileFactories.get(msg
+                .getFileName());
+        if (factory == null) {
+            factory = FileMessageFactory.getInstance(writeToFile, true);
+            fileFactories.put(msg.getFileName(), factory);
+        }
+        return factory;
+    }
+
+    /**
+     * Remove file (war) from messages)
+     * 
+     * @param msg
+     */
+    public void removeFactory(FileMessage msg) {
+        fileFactories.remove(msg.getFileName());
+    }
+
+    /**
+     * Before the cluster invokes messageReceived the cluster will ask the
+     * receiver to accept or decline the message, In the future, when messages
+     * get big, the accept method will only take a message header
+     * 
+     * @param msg
+     *            ClusterMessage
+     * @return boolean - returns true to indicate that messageReceived should be
+     *         invoked. If false is returned, the messageReceived method will
+     *         not be invoked.
+     */
+    public boolean accept(ClusterMessage msg) {
+        return (msg instanceof FileMessage) || (msg instanceof UndeployMessage);
+    }
+
+    /**
+     * Install a new web application, whose web application archive is at the
+     * specified URL, into this container and all the other members of the
+     * cluster with the specified context path. A context path of "" (the empty
+     * string) should be used for the root application for this container.
+     * Otherwise, the context path must start with a slash.
+     * <p>
+     * If this application is successfully installed locally, a ContainerEvent
+     * of type <code>INSTALL_EVENT</code> will be sent to all registered
+     * listeners, with the newly created <code>Context</code> as an argument.
+     * 
+     * @param contextPath
+     *            The context path to which this application should be installed
+     *            (must be unique)
+     * @param war
+     *            A URL of type "jar:" that points to a WAR file, or type
+     *            "file:" that points to an unpacked directory structure
+     *            containing the web application to be installed
+     * 
+     * @exception IllegalArgumentException
+     *                if the specified context path is malformed (it must be ""
+     *                or start with a slash)
+     * @exception IllegalStateException
+     *                if the specified context path is already attached to an
+     *                existing web application
+     * @exception IOException
+     *                if an input/output error was encountered during
+     *                installation
+     */
+    public void install(String contextPath, URL war) throws IOException {
+        Member[] members = getCluster().getMembers();
+        Member localMember = getCluster().getLocalMember();
+        FileMessageFactory factory = FileMessageFactory.getInstance(new File(
+                war.getFile()), false);
+        FileMessage msg = new FileMessage(localMember, war.getFile(),
+                contextPath);
+        if(log.isDebugEnabled())
+            log.debug("Send cluster war deployment [ path:"
+                    + contextPath + " war: " + war + " ] started.");
+        msg = factory.readMessage(msg);
+        while (msg != null) {
+            for (int i = 0; i < members.length; i++) {
+                if (log.isDebugEnabled())
+                    log.debug("Send cluster war fragment [ path: "
+                            + contextPath + " war: " + war + " to: " +  members[i] + " ]");
+                getCluster().send(msg, members[i]);
+            }
+            msg = factory.readMessage(msg);
+        }
+        if(log.isDebugEnabled())
+            log.debug("Send cluster war deployment [ path: "
+                    + contextPath + " war: " + war + " ] finished.");
+    }
+
+    /**
+     * Remove an existing web application, attached to the specified context
+     * path. If this application is successfully removed, a ContainerEvent of
+     * type <code>REMOVE_EVENT</code> will be sent to all registered
+     * listeners, with the removed <code>Context</code> as an argument.
+     * Deletes the web application war file and/or directory if they exist in
+     * the Host's appBase.
+     * 
+     * @param contextPath
+     *            The context path of the application to be removed
+     * @param undeploy
+     *            boolean flag to remove web application from server
+     * 
+     * @exception IllegalArgumentException
+     *                if the specified context path is malformed (it must be ""
+     *                or start with a slash)
+     * @exception IllegalArgumentException
+     *                if the specified context path does not identify a
+     *                currently installed web application
+     * @exception IOException
+     *                if an input/output error occurs during removal
+     */
+    public void remove(String contextPath, boolean undeploy) throws IOException {
+        if (log.isInfoEnabled())
+            log.info("Cluster wide remove of web app " + contextPath);
+        Member localMember = getCluster().getLocalMember();
+        UndeployMessage msg = new UndeployMessage(localMember, System
+                .currentTimeMillis(), "Undeploy:" + contextPath + ":"
+                + System.currentTimeMillis(), contextPath, undeploy);
+        if (log.isDebugEnabled())
+            log.debug("Send cluster wide undeployment from "
+                    + contextPath );
+        cluster.send(msg);
+        // remove locally
+        if (undeploy) {
+            try {
+                if (!isServiced(contextPath)) {
+                    addServiced(contextPath);
+                    try {
+                        remove(contextPath);
+                    } finally {
+                        removeServiced(contextPath);
+                    }
+                } else
+                    log.error("Local remove from " + contextPath
+                            + "failed, other manager has app in service!");
+
+            } catch (Exception ex) {
+                log.error("local remove from " + contextPath + " failed", ex);
+            }
+        }
+
+    }
+
+    /*
+     * Modifcation from watchDir war detected!
+     * 
+     * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileModified(java.io.File)
+     */
+    public void fileModified(File newWar) {
+        try {
+            File deployWar = new File(getDeployDir(), newWar.getName());
+            copy(newWar, deployWar);
+            String contextName = getContextName(deployWar);
+            if (log.isInfoEnabled())
+                log.info("Installing webapp[" + contextName + "] from "
+                        + deployWar.getAbsolutePath());
+            try {
+                remove(contextName, false);
+            } catch (Exception x) {
+                log.error("No removal", x);
+            }
+            install(contextName, deployWar.toURL());
+        } catch (Exception x) {
+            log.error("Unable to install WAR file", x);
+        }
+    }
+
+    /*
+     * War remvoe from watchDir
+     * 
+     * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileRemoved(java.io.File)
+     */
+    public void fileRemoved(File removeWar) {
+        try {
+            String contextName = getContextName(removeWar);
+            if (log.isInfoEnabled())
+                log.info("Removing webapp[" + contextName + "]");
+            remove(contextName, true);
+        } catch (Exception x) {
+            log.error("Unable to remove WAR file", x);
+        }
+    }
+
+    /**
+     * Create a context path from war
+     * @param war War filename
+     * @return '/filename' or if war name is ROOT.war context name is empty string '' 
+     */
+    protected String getContextName(File war) {
+        String contextName = "/"
+        + war.getName().substring(0,
+                war.getName().lastIndexOf(".war"));
+        if("/ROOT".equals(contextName))
+            contextName= "" ;
+        return contextName ;
+    }
+    
+    /**
+     * Given a context path, get the config file name.
+     */
+    protected String getConfigFile(String path) {
+        String basename = null;
+        if (path.equals("")) {
+            basename = "ROOT";
+        } else {
+            basename = path.substring(1).replace('/', '#');
+        }
+        return (basename);
+    }
+
+    /**
+     * Given a context path, get the config file name.
+     */
+    protected String getDocBase(String path) {
+        String basename = null;
+        if (path.equals("")) {
+            basename = "ROOT";
+        } else {
+            basename = path.substring(1);
+        }
+        return (basename);
+    }
+
+    /**
+     * Return a File object representing the "application root" directory for
+     * our associated Host.
+     */
+    protected File getAppBase() {
+
+        if (appBase != null) {
+            return appBase;
+        }
+
+        File file = new File(host.getAppBase());
+        if (!file.isAbsolute())
+            file = new File(System.getProperty("catalina.base"), host
+                    .getAppBase());
+        try {
+            appBase = file.getCanonicalFile();
+        } catch (IOException e) {
+            appBase = file;
+        }
+        return (appBase);
+
+    }
+
+    /**
+     * Invoke the remove method on the deployer.
+     */
+    protected void remove(String path) throws Exception {
+        // TODO Handle remove also work dir content !
+        // Stop the context first to be nicer
+        Context context = (Context) host.findChild(path);
+        if (context != null) {
+            if(log.isDebugEnabled())
+                log.debug("Undeploy local context " +path );
+            ((Lifecycle) context).stop();
+            File war = new File(getAppBase(), getDocBase(path) + ".war");
+            File dir = new File(getAppBase(), getDocBase(path));
+            File xml = new File(configBase, getConfigFile(path) + ".xml");
+            if (war.exists()) {
+                war.delete();
+            } else if (dir.exists()) {
+                undeployDir(dir);
+            } else {
+                xml.delete();
+            }
+            // Perform new deployment and remove internal HostConfig state
+            check(path);
+        }
+
+    }
+
+    /**
+     * Delete the specified directory, including all of its contents and
+     * subdirectories recursively.
+     * 
+     * @param dir
+     *            File object representing the directory to be deleted
+     */
+    protected void undeployDir(File dir) {
+
+        String files[] = dir.list();
+        if (files == null) {
+            files = new String[0];
+        }
+        for (int i = 0; i < files.length; i++) {
+            File file = new File(dir, files[i]);
+            if (file.isDirectory()) {
+                undeployDir(file);
+            } else {
+                file.delete();
+            }
+        }
+        dir.delete();
+
+    }
+
+    /*
+     * Call watcher to check for deploy changes
+     * 
+     * @see org.apache.catalina.cluster.ClusterDeployer#backgroundProcess()
+     */
+    public void backgroundProcess() {
+        if (started) {
+            count = (count + 1) % processDeployFrequency;
+            if (count == 0 && watchEnabled) {
+                watcher.check();
+            }
+        }
+
+    }
+
+    /*--Deployer Operations ------------------------------------*/
+
+    /**
+     * Invoke the check method on the deployer.
+     */
+    protected void check(String name) throws Exception {
+        String[] params = { name };
+        String[] signature = { "java.lang.String" };
+        mBeanServer.invoke(oname, "check", params, signature);
+    }
+
+    /**
+     * Invoke the check method on the deployer.
+     */
+    protected boolean isServiced(String name) throws Exception {
+        String[] params = { name };
+        String[] signature = { "java.lang.String" };
+        Boolean result = (Boolean) mBeanServer.invoke(oname, "isServiced",
+                params, signature);
+        return result.booleanValue();
+    }
+
+    /**
+     * Invoke the check method on the deployer.
+     */
+    protected void addServiced(String name) throws Exception {
+        String[] params = { name };
+        String[] signature = { "java.lang.String" };
+        mBeanServer.invoke(oname, "addServiced", params, signature);
+    }
+
+    /**
+     * Invoke the check method on the deployer.
+     */
+    protected void removeServiced(String name) throws Exception {
+        String[] params = { name };
+        String[] signature = { "java.lang.String" };
+        mBeanServer.invoke(oname, "removeServiced", params, signature);
+    }
+
+    /*--Instance Getters/Setters--------------------------------*/
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(CatalinaCluster cluster) {
+        this.cluster = cluster;
+    }
+
+    public boolean equals(Object listener) {
+        return super.equals(listener);
+    }
+
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    public String getDeployDir() {
+        return deployDir;
+    }
+
+    public void setDeployDir(String deployDir) {
+        this.deployDir = deployDir;
+    }
+
+    public String getTempDir() {
+        return tempDir;
+    }
+
+    public void setTempDir(String tempDir) {
+        this.tempDir = tempDir;
+    }
+
+    public String getWatchDir() {
+        return watchDir;
+    }
+
+    public void setWatchDir(String watchDir) {
+        this.watchDir = watchDir;
+    }
+
+    public boolean isWatchEnabled() {
+        return watchEnabled;
+    }
+
+    public boolean getWatchEnabled() {
+        return watchEnabled;
+    }
+
+    public void setWatchEnabled(boolean watchEnabled) {
+        this.watchEnabled = watchEnabled;
+    }
+
+    /**
+     * Return the frequency of watcher checks.
+     */
+    public int getProcessDeployFrequency() {
+
+        return (this.processDeployFrequency);
+
+    }
+
+    /**
+     * Set the watcher checks frequency.
+     * 
+     * @param processExpiresFrequency
+     *            the new manager checks frequency
+     */
+    public void setProcessDeployFrequency(int processExpiresFrequency) {
+
+        if (processExpiresFrequency <= 0) {
+            return;
+        }
+        this.processDeployFrequency = processExpiresFrequency;
+    }
+
+    /**
+     * Copy a file to the specified temp directory.
+     * @param from copy from temp
+     * @param to   to host appBase directory
+     * @return true, copy successful
+     */
+    protected boolean copy(File from, File to) {
+        try {
+            if (!to.exists())
+                to.createNewFile();
+            java.io.FileInputStream is = new java.io.FileInputStream(from);
+            java.io.FileOutputStream os = new java.io.FileOutputStream(to,
+                    false);
+            byte[] buf = new byte[4096];
+            while (true) {
+                int len = is.read(buf);
+                if (len < 0)
+                    break;
+                os.write(buf, 0, len);
+            }
+            is.close();
+            os.close();
+        } catch (IOException e) {
+            log.error("Unable to copy file from:" + from + " to:" + to, e);
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileChangeListener.java b/java/org/apache/catalina/ha/deploy/FileChangeListener.java
new file mode 100644 (file)
index 0000000..3468622
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+import java.io.File;
+
+public interface FileChangeListener {
+    public void fileModified(File f);
+    public void fileRemoved(File f);
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileMessage.java b/java/org/apache/catalina/ha/deploy/FileMessage.java
new file mode 100644 (file)
index 0000000..ab2847e
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+
+import java.io.Serializable;
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.ha.ClusterMessageBase;
+
+/**
+ * Contains the data for a file being transferred over TCP, this is 
+ * essentially a fragment of a file, read and written by the FileMessageFactory
+ * @author Filip Hanik
+ * @version 1.0
+ */
+
+public class FileMessage extends ClusterMessageBase implements ClusterMessage, Serializable {
+    private int messageNumber;
+    private byte[] data;
+    private int dataLength;
+    
+    private long totalLength;
+    private long totalNrOfMsgs;
+    private String fileName;
+    private String contextPath;
+    
+    public FileMessage(Member source,
+                       String fileName,
+                       String contextPath) {
+        this.address=source;
+        this.fileName=fileName;
+        this.contextPath=contextPath;
+    }
+    
+    /*
+    public void writeExternal(ObjectOutput out) throws IOException {
+                   
+    }
+    
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+                  
+    }
+    */
+   
+    public int getMessageNumber() {
+        return messageNumber;
+    }
+    public void setMessageNumber(int messageNumber) {
+        this.messageNumber = messageNumber;
+    }
+    public long getTotalNrOfMsgs() {
+        return totalNrOfMsgs;
+    }
+    public void setTotalNrOfMsgs(long totalNrOfMsgs) {
+        this.totalNrOfMsgs = totalNrOfMsgs;
+    }
+    public byte[] getData() {
+        return data;
+    }
+    public void setData(byte[] data, int length) {
+        this.data = data;
+        this.dataLength = length;
+    }
+    public int getDataLength() {
+        return dataLength;
+    }
+    public void setDataLength(int dataLength) {
+        this.dataLength = dataLength;
+    }
+    public long getTotalLength() {
+        return totalLength;
+    }
+    public void setTotalLength(long totalLength) {
+        this.totalLength = totalLength;
+    }
+
+    public String getUniqueId() {
+        StringBuffer result = new StringBuffer(getFileName());
+        result.append("#-#");
+        result.append(getMessageNumber());
+        result.append("#-#");
+        result.append(System.currentTimeMillis());
+        return result.toString();
+    }
+
+    
+    public String getFileName() {
+        return fileName;
+    }
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+    public String getContextPath() {
+        return contextPath;
+    }
+    
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileMessageFactory.java b/java/org/apache/catalina/ha/deploy/FileMessageFactory.java
new file mode 100644 (file)
index 0000000..43354ea
--- /dev/null
@@ -0,0 +1,311 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+
+/**
+ * This factory is used to read files and write files by splitting them up into
+ * smaller messages. So that entire files don't have to be read into memory.
+ * <BR>
+ * The factory can be used as a reader or writer but not both at the same time.
+ * When done reading or writing the factory will close the input or output
+ * streams and mark the factory as closed. It is not possible to use it after
+ * that. <BR>
+ * To force a cleanup, call cleanup() from the calling object. <BR>
+ * This class is not thread safe.
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class FileMessageFactory {
+    /*--Static Variables----------------------------------------*/
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+            .getLog(FileMessageFactory.class);
+
+    /**
+     * The number of bytes that we read from file
+     */
+    public static final int READ_SIZE = 1024 * 10; //10kb
+
+    /**
+     * The file that we are reading/writing
+     */
+    protected File file = null;
+
+    /**
+     * True means that we are writing with this factory. False means that we are
+     * reading with this factory
+     */
+    protected boolean openForWrite;
+
+    /**
+     * Once the factory is used, it can not be reused.
+     */
+    protected boolean closed = false;
+
+    /**
+     * When openForWrite=false, the input stream is held by this variable
+     */
+    protected FileInputStream in;
+
+    /**
+     * When openForWrite=true, the output stream is held by this variable
+     */
+    protected FileOutputStream out;
+
+    /**
+     * The number of messages we have read or written
+     */
+    protected int nrOfMessagesProcessed = 0;
+
+    /**
+     * The total size of the file
+     */
+    protected long size = 0;
+
+    /**
+     * The total number of packets that we split this file into
+     */
+    protected long totalNrOfMessages = 0;
+
+    /**
+     * The bytes that we hold the data in, not thread safe.
+     */
+    protected byte[] data = new byte[READ_SIZE];
+
+    /**
+     * Private constructor, either instantiates a factory to read or write. <BR>
+     * When openForWrite==true, then a the file, f, will be created and an
+     * output stream is opened to write to it. <BR>
+     * When openForWrite==false, an input stream is opened, the file has to
+     * exist.
+     * 
+     * @param f
+     *            File - the file to be read/written
+     * @param openForWrite
+     *            boolean - true means we are writing to the file, false means
+     *            we are reading from the file
+     * @throws FileNotFoundException -
+     *             if the file to be read doesn't exist
+     * @throws IOException -
+     *             if the system fails to open input/output streams to the file
+     *             or if it fails to create the file to be written to.
+     */
+    private FileMessageFactory(File f, boolean openForWrite)
+            throws FileNotFoundException, IOException {
+        this.file = f;
+        this.openForWrite = openForWrite;
+        if (log.isDebugEnabled())
+            log.debug("open file " + f + " write " + openForWrite);
+        if (openForWrite) {
+            if (!file.exists())
+                file.createNewFile();
+            out = new FileOutputStream(f);
+        } else {
+            size = file.length();
+            totalNrOfMessages = (size / READ_SIZE) + 1;
+            in = new FileInputStream(f);
+        }//end if
+
+    }
+
+    /**
+     * Creates a factory to read or write from a file. When opening for read,
+     * the readMessage can be invoked, and when opening for write the
+     * writeMessage can be invoked.
+     * 
+     * @param f
+     *            File - the file to be read or written
+     * @param openForWrite
+     *            boolean - true, means we are writing to the file, false means
+     *            we are reading from it
+     * @throws FileNotFoundException -
+     *             if the file to be read doesn't exist
+     * @throws IOException -
+     *             if it fails to create the file that is to be written
+     * @return FileMessageFactory
+     */
+    public static FileMessageFactory getInstance(File f, boolean openForWrite)
+            throws FileNotFoundException, IOException {
+        return new FileMessageFactory(f, openForWrite);
+    }
+
+    /**
+     * Reads file data into the file message and sets the size, totalLength,
+     * totalNrOfMsgs and the message number <BR>
+     * If EOF is reached, the factory returns null, and closes itself, otherwise
+     * the same message is returned as was passed in. This makes sure that not
+     * more memory is ever used. To remember, neither the file message or the
+     * factory are thread safe. dont hand off the message to one thread and read
+     * the same with another.
+     * 
+     * @param f
+     *            FileMessage - the message to be populated with file data
+     * @throws IllegalArgumentException -
+     *             if the factory is for writing or is closed
+     * @throws IOException -
+     *             if a file read exception occurs
+     * @return FileMessage - returns the same message passed in as a parameter,
+     *         or null if EOF
+     */
+    public FileMessage readMessage(FileMessage f)
+            throws IllegalArgumentException, IOException {
+        checkState(false);
+        int length = in.read(data);
+        if (length == -1) {
+            cleanup();
+            return null;
+        } else {
+            f.setData(data, length);
+            f.setTotalLength(size);
+            f.setTotalNrOfMsgs(totalNrOfMessages);
+            f.setMessageNumber(++nrOfMessagesProcessed);
+            return f;
+        }//end if
+    }
+
+    /**
+     * Writes a message to file. If (msg.getMessageNumber() ==
+     * msg.getTotalNrOfMsgs()) the output stream will be closed after writing.
+     * 
+     * @param msg
+     *            FileMessage - message containing data to be written
+     * @throws IllegalArgumentException -
+     *             if the factory is opened for read or closed
+     * @throws IOException -
+     *             if a file write error occurs
+     * @return returns true if the file is complete and outputstream is closed,
+     *         false otherwise.
+     */
+    public boolean writeMessage(FileMessage msg)
+            throws IllegalArgumentException, IOException {
+        if (!openForWrite)
+            throw new IllegalArgumentException(
+                    "Can't write message, this factory is reading.");
+        if (log.isDebugEnabled())
+            log.debug("Message " + msg + " data " + msg.getData()
+                    + " data length " + msg.getDataLength() + " out " + out);
+        if (out != null) {
+            out.write(msg.getData(), 0, msg.getDataLength());
+            nrOfMessagesProcessed++;
+            out.flush();
+            if (msg.getMessageNumber() == msg.getTotalNrOfMsgs()) {
+                out.close();
+                cleanup();
+                return true;
+            }//end if
+        } else {
+            if (log.isWarnEnabled())
+                log.warn("Receive Message again -- Sender ActTimeout to short [ path: "
+                                + msg.getContextPath()
+                                + " war: "
+                                + msg.getFileName()
+                                + " data: "
+                                + msg.getData()
+                                + " data length: " + msg.getDataLength() + " ]");
+        }
+        return false;
+    }//writeMessage
+
+    /**
+     * Closes the factory, its streams and sets all its references to null
+     */
+    public void cleanup() {
+        if (in != null)
+            try {
+                in.close();
+            } catch (Exception ignore) {
+            }
+        if (out != null)
+            try {
+                out.close();
+            } catch (Exception ignore) {
+            }
+        in = null;
+        out = null;
+        size = 0;
+        closed = true;
+        data = null;
+        nrOfMessagesProcessed = 0;
+        totalNrOfMessages = 0;
+    }
+
+    /**
+     * Check to make sure the factory is able to perform the function it is
+     * asked to do. Invoked by readMessage/writeMessage before those methods
+     * proceed.
+     * 
+     * @param openForWrite
+     *            boolean
+     * @throws IllegalArgumentException
+     */
+    protected void checkState(boolean openForWrite)
+            throws IllegalArgumentException {
+        if (this.openForWrite != openForWrite) {
+            cleanup();
+            if (openForWrite)
+                throw new IllegalArgumentException(
+                        "Can't write message, this factory is reading.");
+            else
+                throw new IllegalArgumentException(
+                        "Can't read message, this factory is writing.");
+        }
+        if (this.closed) {
+            cleanup();
+            throw new IllegalArgumentException("Factory has been closed.");
+        }
+    }
+
+    /**
+     * Example usage.
+     * 
+     * @param args
+     *            String[], args[0] - read from filename, args[1] write to
+     *            filename
+     * @throws Exception
+     */
+    public static void main(String[] args) throws Exception {
+
+        System.out
+                .println("Usage: FileMessageFactory fileToBeRead fileToBeWritten");
+        System.out
+                .println("Usage: This will make a copy of the file on the local file system");
+        FileMessageFactory read = getInstance(new File(args[0]), false);
+        FileMessageFactory write = getInstance(new File(args[1]), true);
+        FileMessage msg = new FileMessage(null, args[0], args[0]);
+        msg = read.readMessage(msg);
+        System.out.println("Expecting to write " + msg.getTotalNrOfMsgs()
+                + " messages.");
+        int cnt = 0;
+        while (msg != null) {
+            write.writeMessage(msg);
+            cnt++;
+            msg = read.readMessage(msg);
+        }//while
+        System.out.println("Actually wrote " + cnt + " messages.");
+    }///main
+
+    public File getFile() {
+        return file;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/deploy/UndeployMessage.java b/java/org/apache/catalina/ha/deploy/UndeployMessage.java
new file mode 100644 (file)
index 0000000..1003f3c
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import java.io.Serializable;
+public class UndeployMessage implements ClusterMessage,Serializable {
+    private Member address;
+    private long timestamp;
+    private String uniqueId;
+    private String contextPath;
+    private boolean undeploy;
+    private int resend = 0;
+    private int compress = 0;
+
+    public UndeployMessage() {} //for serialization
+    public UndeployMessage(Member address,
+                           long timestamp,
+                           String uniqueId,
+                           String contextPath,
+                           boolean undeploy) {
+        this.address  = address;
+        this.timestamp= timestamp;
+        this.undeploy = undeploy;
+        this.uniqueId = uniqueId;
+        this.undeploy = undeploy;
+        this.contextPath = contextPath;
+    }
+
+    public Member getAddress() {
+        return address;
+    }
+
+    public void setAddress(Member address) {
+        this.address = address;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public String getUniqueId() {
+        return uniqueId;
+    }
+
+    public void setUniqueId(String uniqueId) {
+        this.uniqueId = uniqueId;
+    }
+
+    public String getContextPath() {
+        return contextPath;
+    }
+
+    public void setContextPath(String contextPath) {
+        this.contextPath = contextPath;
+    }
+
+    public boolean getUndeploy() {
+        return undeploy;
+    }
+
+    public void setUndeploy(boolean undeploy) {
+        this.undeploy = undeploy;
+    }
+    /**
+     * @return Returns the compress.
+     * @since 5.5.10 
+     */
+    public int getCompress() {
+        return compress;
+    }
+    /**
+     * @param compress The compress to set.
+     * @since 5.5.10
+     */
+    public void setCompress(int compress) {
+        this.compress = compress;
+    }
+    /**
+     * @return Returns the resend.
+     * @since 5.5.10
+     */
+    public int getResend() {
+        return resend;
+    }
+    /**
+     * @param resend The resend to set.
+     * @since 5.5.10
+     */
+    public void setResend(int resend) {
+        this.resend = resend;
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/deploy/WarWatcher.java b/java/org/apache/catalina/ha/deploy/WarWatcher.java
new file mode 100644 (file)
index 0000000..f7aaa4c
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.deploy;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Iterator;
+
+/**
+ * <p>
+ * The <b>WarWatcher </b> watches the deployDir for changes made to the
+ * directory (adding new WAR files->deploy or remove WAR files->undeploy) And
+ * notifies a listener of the changes made
+ * </p>
+ * 
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version 1.1
+ */
+
+public class WarWatcher {
+
+    /*--Static Variables----------------------------------------*/
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+            .getLog(WarWatcher.class);
+
+    /*--Instance Variables--------------------------------------*/
+    /**
+     * Directory to watch for war files
+     */
+    protected File watchDir = null;
+
+    /**
+     * Parent to be notified of changes
+     */
+    protected FileChangeListener listener = null;
+
+    /**
+     * Currently deployed files
+     */
+    protected Map currentStatus = new HashMap();
+
+    /*--Constructor---------------------------------------------*/
+
+    public WarWatcher() {
+    }
+
+    public WarWatcher(FileChangeListener listener, File watchDir) {
+        this.listener = listener;
+        this.watchDir = watchDir;
+    }
+
+    /*--Logic---------------------------------------------------*/
+
+    /**
+     * check for modification and send notifcation to listener
+     */
+    public void check() {
+        if (log.isInfoEnabled())
+            log.info("check cluster wars at " + watchDir);
+        File[] list = watchDir.listFiles(new WarFilter());
+        if (list == null)
+            list = new File[0];
+        //first make sure all the files are listed in our current status
+        for (int i = 0; i < list.length; i++) {
+            addWarInfo(list[i]);
+        }
+
+        //check all the status codes and update the FarmDeployer
+        for (Iterator i = currentStatus.entrySet().iterator(); i.hasNext();) {
+            Map.Entry entry = (Map.Entry) i.next();
+            WarInfo info = (WarInfo) entry.getValue();
+            int check = info.check();
+            if (check == 1) {
+                listener.fileModified(info.getWar());
+            } else if (check == -1) {
+                listener.fileRemoved(info.getWar());
+                //no need to keep in memory
+                currentStatus.remove(info.getWar());
+            }
+        }
+
+    }
+
+    /**
+     * add cluster war to the watcher state
+     * @param warfile
+     */
+    protected void addWarInfo(File warfile) {
+        WarInfo info = (WarInfo) currentStatus.get(warfile.getAbsolutePath());
+        if (info == null) {
+            info = new WarInfo(warfile);
+            info.setLastState(-1); //assume file is non existent
+            currentStatus.put(warfile.getAbsolutePath(), info);
+        }
+    }
+
+    /**
+     * clear watcher state
+     */
+    public void clear() {
+        currentStatus.clear();
+    }
+
+    /**
+     * @return Returns the watchDir.
+     */
+    public File getWatchDir() {
+        return watchDir;
+    }
+
+    /**
+     * @param watchDir
+     *            The watchDir to set.
+     */
+    public void setWatchDir(File watchDir) {
+        this.watchDir = watchDir;
+    }
+
+    /**
+     * @return Returns the listener.
+     */
+    public FileChangeListener getListener() {
+        return listener;
+    }
+
+    /**
+     * @param listener
+     *            The listener to set.
+     */
+    public void setListener(FileChangeListener listener) {
+        this.listener = listener;
+    }
+
+    /*--Inner classes-------------------------------------------*/
+
+    /**
+     * File name filter for war files
+     */
+    protected class WarFilter implements java.io.FilenameFilter {
+        public boolean accept(File path, String name) {
+            if (name == null)
+                return false;
+            return name.endsWith(".war");
+        }
+    }
+
+    /**
+     * File information on existing WAR files
+     */
+    protected class WarInfo {
+        protected File war = null;
+
+        protected long lastChecked = 0;
+
+        protected long lastState = 0;
+
+        public WarInfo(File war) {
+            this.war = war;
+            this.lastChecked = war.lastModified();
+            if (!war.exists())
+                lastState = -1;
+        }
+
+        public boolean modified() {
+            return war.exists() && war.lastModified() > lastChecked;
+        }
+
+        public boolean exists() {
+            return war.exists();
+        }
+
+        /**
+         * Returns 1 if the file has been added/modified, 0 if the file is
+         * unchanged and -1 if the file has been removed
+         * 
+         * @return int 1=file added; 0=unchanged; -1=file removed
+         */
+        public int check() {
+            //file unchanged by default
+            int result = 0;
+
+            if (modified()) {
+                //file has changed - timestamp
+                result = 1;
+                lastState = result;
+            } else if ((!exists()) && (!(lastState == -1))) {
+                //file was removed
+                result = -1;
+                lastState = result;
+            } else if ((lastState == -1) && exists()) {
+                //file was added
+                result = 1;
+                lastState = result;
+            }
+            this.lastChecked = System.currentTimeMillis();
+            return result;
+        }
+
+        public File getWar() {
+            return war;
+        }
+
+        public int hashCode() {
+            return war.getAbsolutePath().hashCode();
+        }
+
+        public boolean equals(Object other) {
+            if (other instanceof WarInfo) {
+                WarInfo wo = (WarInfo) other;
+                return wo.getWar().equals(getWar());
+            } else {
+                return false;
+            }
+        }
+
+        protected void setLastState(int lastState) {
+            this.lastState = lastState;
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/mbeans-descriptors.xml b/java/org/apache/catalina/ha/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..7f54550
--- /dev/null
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<mbeans-descriptors>
+
+  <mbean         name="SimpleTcpCluster"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Tcp Cluster implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.ha.tcp.SimpleTcpCluster">
+
+    <attribute   name="protocolStack"
+          description="JavaGroups protocol stack selection"
+                 type="java.lang.String"/>
+
+  </mbean>
+
+
+  <mbean         name="SimpleTcpReplicationManager"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Clustered implementation of the Manager interface"
+               domain="Catalina"
+                group="Manager"
+                 type="org.apache.catalina.ha.tcp.SimpleTcpReplicationManager">
+
+    <attribute   name="algorithm"
+          description="The message digest algorithm to be used when generating
+                       session identifiers"
+                 type="java.lang.String"/>
+
+    <attribute   name="checkInterval"
+          description="The interval (in seconds) between checks for expired
+                       sessions"
+                 type="int"/>
+
+    <attribute   name="className"
+          description="Fully qualified class name of the managed object"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="distributable"
+          description="The distributable flag for Sessions created by this
+                       Manager"
+                 type="boolean"/>
+
+    <attribute   name="entropy"
+          description="A String initialization parameter used to increase the
+                       entropy of the initialization of our random number
+                       generator"
+                 type="java.lang.String"/>
+
+    <attribute   name="managedResource"
+          description="The managed resource this MBean is associated with"
+                 type="java.lang.Object"/>
+
+    <attribute   name="maxActiveSessions"
+          description="The maximum number of active Sessions allowed, or -1
+                       for no limit"
+                 type="int"/>
+
+    <attribute   name="maxInactiveInterval"
+          description="The default maximum inactive interval for Sessions
+                       created by this Manager"
+                 type="int"/>
+
+    <attribute   name="name"
+          description="The descriptive name of this Manager implementation
+                       (for logging)"
+                 type="java.lang.String"
+            writeable="false"/>
+
+  </mbean>
+
+
+
+<mbean         name="ReplicationValve"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Valve for simple tcp replication"
+               domain="Catalina"
+                group="Valve"
+                 type="org.apache.catalina.ha.tcp.ReplicationValve">
+
+    <attribute   name="className"
+          description="Fully qualified class name of the managed object"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="debug"
+          description="The debugging detail level for this component"
+                 type="int"/>
+
+  </mbean>
+
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/ha/package.html b/java/org/apache/catalina/ha/package.html
new file mode 100644 (file)
index 0000000..ce83a0d
--- /dev/null
@@ -0,0 +1,11 @@
+<body>
+
+<p>This package contains code for Clustering, the base class
+of a Cluster is <code>org.apache.catalina.Cluster</code> implementations
+of this class is done when implementing a new Cluster protocol</p>
+
+<p>The only Cluster protocol currently implemented is a JavaGroups based<br>
+&nbsp;&nbsp;&nbsp;&nbsp;<b>JGCluster.java</b>
+</p>
+
+</body>
diff --git a/java/org/apache/catalina/ha/session/BackupManager.java b/java/org/apache/catalina/ha/session/BackupManager.java
new file mode 100644 (file)
index 0000000..fcaa1cf
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Loader;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.session.StandardManager;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.tribes.tipis.LazyReplicatedMap;
+import org.apache.catalina.tribes.Channel;
+
+/**
+ *@author Filip Hanik
+ *@version 1.0
+ */
+public class BackupManager extends StandardManager implements ClusterManager
+{
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( BackupManager.class );
+
+    protected static long DEFAULT_REPL_TIMEOUT = 15000;//15 seconds
+
+    /** Set to true if we don't want the sessions to expire on shutdown */
+    protected boolean mExpireSessionsOnShutdown = true;
+    
+    /**
+     * The name of this manager
+     */
+    protected String name;
+
+    /**
+     * A reference to the cluster
+     */
+    protected CatalinaCluster cluster;
+    
+    /**
+     * Should listeners be notified?
+     */
+    private boolean notifyListenersOnReplication;
+    /**
+     * 
+     */
+    private int mapSendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK|Channel.SEND_OPTIONS_USE_ACK;
+
+    /**
+     * Constructor, just calls super()
+     *
+     */
+    public BackupManager() {
+        super();
+    }
+
+
+//******************************************************************************/
+//      ClusterManager Interface     
+//******************************************************************************/
+
+    public void messageDataReceived(ClusterMessage msg) {
+    }
+
+    public boolean isSendClusterDomainOnly() {
+        return false;
+    }
+
+    /**
+     * @param sendClusterDomainOnly The sendClusterDomainOnly to set.
+     */
+    public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) {
+    }
+
+    /**
+     * @return Returns the defaultMode.
+     */
+    public boolean isDefaultMode() {
+        return false;
+    }
+    /**
+     * @param defaultMode The defaultMode to set.
+     */
+    public void setDefaultMode(boolean defaultMode) {
+    }
+
+    public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown)
+    {
+        mExpireSessionsOnShutdown = expireSessionsOnShutdown;
+    }
+
+    public void setCluster(CatalinaCluster cluster) {
+        if(log.isDebugEnabled())
+            log.debug("Cluster associated with SimpleTcpReplicationManager");
+        this.cluster = cluster;
+    }
+
+    public boolean getExpireSessionsOnShutdown()
+    {
+        return mExpireSessionsOnShutdown;
+    }
+
+
+    /**
+     * Override persistence since they don't go hand in hand with replication for now.
+     */
+    public void unload() throws IOException {
+    }
+    
+    public ClusterMessage requestCompleted(String sessionId) {
+        if ( !this.started ) return null;
+        LazyReplicatedMap map = (LazyReplicatedMap)sessions;
+        map.replicate(sessionId,false);
+        return null;
+    }
+
+
+//=========================================================================
+// OVERRIDE THESE METHODS TO IMPLEMENT THE REPLICATION
+//=========================================================================
+
+    public Session createEmptySession() {
+        return new DeltaSession(this);
+    }
+    
+    public ClassLoader[] getClassLoaders() {
+        return ClusterManagerBase.getClassLoaders(this.container);
+    }
+
+    /**
+     * Open Stream and use correct ClassLoader (Container) Switch
+     * ThreadClassLoader
+     * 
+     * @param data
+     * @return The object input stream
+     * @throws IOException
+     */
+    public ReplicationStream getReplicationStream(byte[] data) throws IOException {
+        return getReplicationStream(data,0,data.length);
+    }
+
+    public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException {
+        ByteArrayInputStream fis = new ByteArrayInputStream(data, offset, length);
+        return new ReplicationStream(fis, getClassLoaders());
+    }    
+
+
+
+
+    public String getName() {
+        return this.name;
+    }
+    /**
+     * Prepare for the beginning of active use of the public methods of this
+     * component.  This method should be called after <code>configure()</code>,
+     * and before any of the public methods of the component are utilized.<BR>
+     * Starts the cluster communication channel, this will connect with the other nodes
+     * in the cluster, and request the current session state to be transferred to this node.
+     * @exception IllegalStateException if this component has already been
+     *  started
+     * @exception LifecycleException if this component detects a fatal error
+     *  that prevents this component from being used
+     */
+    public void start() throws LifecycleException {
+        if ( this.started ) return;
+        try {
+            CatalinaCluster catclust = (CatalinaCluster)cluster;
+            catclust.addManager(getName(), this);
+            LazyReplicatedMap map = new LazyReplicatedMap(this,
+                                                          catclust.getChannel(),
+                                                          DEFAULT_REPL_TIMEOUT,
+                                                          getMapName(),
+                                                          getClassLoaders());
+            map.setChannelSendOptions(mapSendOptions);
+            this.sessions = map;
+            super.start();
+        }  catch ( Exception x ) {
+            log.error("Unable to start BackupManager",x);
+            throw new LifecycleException("Failed to start BackupManager",x);
+        }
+    }
+    
+    public String getMapName() {
+        CatalinaCluster catclust = (CatalinaCluster)cluster;
+        String name = catclust.getManagerName(getName(),this)+"-"+"";
+        if ( log.isDebugEnabled() ) log.debug("Backup manager, Setting map name to:"+name);
+        return name;
+    }
+
+    /**
+     * Gracefully terminate the active use of the public methods of this
+     * component.  This method should be the last one called on a given
+     * instance of this component.<BR>
+     * This will disconnect the cluster communication channel and stop the listener thread.
+     * @exception IllegalStateException if this component has not been started
+     * @exception LifecycleException if this component detects a fatal error
+     *  that needs to be reported
+     */
+    public void stop() throws LifecycleException
+    {
+        
+        LazyReplicatedMap map = (LazyReplicatedMap)sessions;
+        if ( map!=null ) {
+            map.breakdown();
+        }
+        if ( !this.started ) return;
+        try {
+        } catch ( Exception x ){
+            log.error("Unable to stop BackupManager",x);
+            throw new LifecycleException("Failed to stop BackupManager",x);
+        } finally {
+            super.stop();
+        }
+        cluster.removeManager(getName(),this);
+
+    }
+
+    public void setDistributable(boolean dist) {
+        this.distributable = dist;
+    }
+
+    public boolean getDistributable() {
+        return distributable;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+    public boolean isNotifyListenersOnReplication() {
+        return notifyListenersOnReplication;
+    }
+    public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) {
+        this.notifyListenersOnReplication = notifyListenersOnReplication;
+    }
+
+    public void setMapSendOptions(int mapSendOptions) {
+        this.mapSendOptions = mapSendOptions;
+    }
+
+    /* 
+     * @see org.apache.catalina.ha.ClusterManager#getCluster()
+     */
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+
+    public int getMapSendOptions() {
+        return mapSendOptions;
+    }
+
+    public String[] getInvalidatedSessions() {
+        return new String[0];
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/ClusterManagerBase.java b/java/org/apache/catalina/ha/session/ClusterManagerBase.java
new file mode 100644 (file)
index 0000000..71e0403
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+import org.apache.catalina.ha.ClusterManager;
+import java.beans.PropertyChangeListener;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.session.ManagerBase;
+import org.apache.catalina.Loader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.Container;
+
+/**
+ * 
+ * @author Filip Hanik
+ * @version $Revision: 380100 $ $Date: 2006-02-23 06:08:14 -0600 (Thu, 23 Feb 2006) $
+ */
+
+public abstract class ClusterManagerBase extends ManagerBase implements Lifecycle, PropertyChangeListener, ClusterManager{
+    
+
+    public static ClassLoader[] getClassLoaders(Container container) {
+        Loader loader = null;
+        ClassLoader classLoader = null;
+        if (container != null) loader = container.getLoader();
+        if (loader != null) classLoader = loader.getClassLoader();
+        else classLoader = Thread.currentThread().getContextClassLoader();
+        if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+            return new ClassLoader[] {classLoader};
+        } else {
+            return new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()};
+        }
+    }
+
+
+    public ClassLoader[] getClassLoaders() {
+        return getClassLoaders(container);
+    }
+
+    /**
+     * Open Stream and use correct ClassLoader (Container) Switch
+     * ThreadClassLoader
+     * 
+     * @param data
+     * @return The object input stream
+     * @throws IOException
+     */
+    public ReplicationStream getReplicationStream(byte[] data) throws IOException {
+        return getReplicationStream(data,0,data.length);
+    }
+
+    public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException {
+        ByteArrayInputStream fis = new ByteArrayInputStream(data, offset, length);
+        return new ReplicationStream(fis, getClassLoaders());
+    }    
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/session/ClusterSessionListener.java b/java/org/apache/catalina/ha/session/ClusterSessionListener.java
new file mode 100644 (file)
index 0000000..3150ec9
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+import java.util.Map;
+
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.*;
+
+/**
+ * Receive replicated SessionMessage form other cluster node.
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public class ClusterSessionListener extends ClusterListener {
+    /**
+     * The descriptive information about this implementation.
+     */
+    protected static final String info = "org.apache.catalina.session.ClusterSessionListener/1.1";
+
+    //--Constructor---------------------------------------------
+
+    public ClusterSessionListener() {
+    }
+
+    //--Logic---------------------------------------------------
+
+    /**
+     * Return descriptive information about this implementation.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+
+    /**
+     * Callback from the cluster, when a message is received, The cluster will
+     * broadcast it invoking the messageReceived on the receiver.
+     * 
+     * @param myobj
+     *            ClusterMessage - the message received from the cluster
+     */
+    public void messageReceived(ClusterMessage myobj) {
+        if (myobj != null && myobj instanceof SessionMessage) {
+            SessionMessage msg = (SessionMessage) myobj;
+            String ctxname = msg.getContextName();
+            //check if the message is a EVT_GET_ALL_SESSIONS,
+            //if so, wait until we are fully started up
+            Map managers = cluster.getManagers() ;
+            if (ctxname == null) {
+                java.util.Iterator i = managers.keySet().iterator();
+                while (i.hasNext()) {
+                    String key = (String) i.next();
+                    ClusterManager mgr = (ClusterManager) managers.get(key);
+                    if (mgr != null)
+                        mgr.messageDataReceived(msg);
+                    else {
+                        //this happens a lot before the system has started
+                        // up
+                        if (log.isDebugEnabled())
+                            log.debug("Context manager doesn't exist:"
+                                    + key);
+                    }
+                }
+            } else {
+                ClusterManager mgr = (ClusterManager) managers.get(ctxname);
+                if (mgr != null)
+                    mgr.messageDataReceived(msg);
+                else if (log.isWarnEnabled())
+                    log.warn("Context manager doesn't exist:" + ctxname);
+            }
+        }
+        return;
+    }
+
+    /**
+     * Accept only SessionMessage
+     * 
+     * @param msg
+     *            ClusterMessage
+     * @return boolean - returns true to indicate that messageReceived should be
+     *         invoked. If false is returned, the messageReceived method will
+     *         not be invoked.
+     */
+    public boolean accept(ClusterMessage msg) {
+        return (msg instanceof SessionMessage);
+    }
+}
+
diff --git a/java/org/apache/catalina/ha/session/Constants.java b/java/org/apache/catalina/ha/session/Constants.java
new file mode 100644 (file)
index 0000000..9240308
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha.session;
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.ha.session</code>
+ * package.
+ *
+ * @author Peter Rossbach Pero
+ */
+
+public class Constants {
+
+    public static final String Package = "org.apache.catalina.ha.session";
+
+}
diff --git a/java/org/apache/catalina/ha/session/DeltaManager.java b/java/org/apache/catalina/ha/session/DeltaManager.java
new file mode 100644 (file)
index 0000000..545b637
--- /dev/null
@@ -0,0 +1,1499 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+import java.beans.PropertyChangeEvent;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+
+import org.apache.catalina.Cluster;
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Host;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Session;
+import org.apache.catalina.Valve;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.tcp.ReplicationValve;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.util.LifecycleSupport;
+import org.apache.catalina.util.StringManager;
+
+/**
+ * The DeltaManager manages replicated sessions by only replicating the deltas
+ * in data. For applications written to handle this, the DeltaManager is the
+ * optimal way of replicating data.
+ * 
+ * This code is almost identical to StandardManager with a difference in how it
+ * persists sessions and some modifications to it.
+ * 
+ * <b>IMPLEMENTATION NOTE </b>: Correct behavior of session storing and
+ * reloading depends upon external calls to the <code>start()</code> and
+ * <code>stop()</code> methods of this class at the correct times.
+ * 
+ * @author Filip Hanik
+ * @author Craig R. McClanahan
+ * @author Jean-Francois Arcand
+ * @author Peter Rossbach
+ * @version $Revision: 380100 $ $Date: 2006-02-23 06:08:14 -0600 (Thu, 23 Feb 2006) $
+ */
+
+public class DeltaManager extends ClusterManagerBase{
+
+    // ---------------------------------------------------- Security Classes
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(DeltaManager.class);
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm = StringManager.getManager(Constants.Package);
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "DeltaManager/2.1";
+
+    /**
+     * Has this component been started yet?
+     */
+    private boolean started = false;
+
+    /**
+     * The descriptive name of this Manager implementation (for logging).
+     */
+    protected static String managerName = "DeltaManager";
+    protected String name = null;
+    protected boolean defaultMode = false;
+    private CatalinaCluster cluster = null;
+
+    /**
+     * cached replication valve cluster container!
+     */
+    private ReplicationValve replicationValve = null ;
+    
+    /**
+     * The lifecycle event support for this component.
+     */
+    protected LifecycleSupport lifecycle = new LifecycleSupport(this);
+
+    /**
+     * The maximum number of active Sessions allowed, or -1 for no limit.
+     */
+    private int maxActiveSessions = -1;
+    private boolean expireSessionsOnShutdown = false;
+    private boolean notifyListenersOnReplication = true;
+    private boolean notifySessionListenersOnReplication = true;
+    private boolean stateTransfered = false ;
+    private int stateTransferTimeout = 60;
+    private boolean sendAllSessions = true;
+    private boolean sendClusterDomainOnly = true ;
+    private int sendAllSessionsSize = 1000 ;
+    
+    /**
+     * wait time between send session block (default 2 sec) 
+     */
+    private int sendAllSessionsWaitTime = 2 * 1000 ; 
+    private ArrayList receivedMessageQueue = new ArrayList() ;
+    private boolean receiverQueue = false ;
+    private boolean stateTimestampDrop = true ;
+    private long stateTransferCreateSendTime; 
+    
+    // ------------------------------------------------------------------ stats attributes
+    
+    int rejectedSessions = 0;
+    private long sessionReplaceCounter = 0 ;
+    long processingTime = 0;
+    private long counterReceive_EVT_GET_ALL_SESSIONS = 0 ;
+    private long counterSend_EVT_ALL_SESSION_DATA = 0 ;
+    private long counterReceive_EVT_ALL_SESSION_DATA = 0 ;
+    private long counterReceive_EVT_SESSION_CREATED = 0 ;
+    private long counterReceive_EVT_SESSION_EXPIRED = 0;
+    private long counterReceive_EVT_SESSION_ACCESSED = 0 ;
+    private long counterReceive_EVT_SESSION_DELTA = 0;
+    private long counterSend_EVT_GET_ALL_SESSIONS = 0 ;
+    private long counterSend_EVT_SESSION_CREATED = 0;
+    private long counterSend_EVT_SESSION_DELTA = 0 ;
+    private long counterSend_EVT_SESSION_ACCESSED = 0;
+    private long counterSend_EVT_SESSION_EXPIRED = 0;
+    private int counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0 ;
+    private int counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0 ;
+    private int counterNoStateTransfered = 0 ;
+
+
+    // ------------------------------------------------------------- Constructor
+    public DeltaManager() {
+        super();
+    }
+
+    // ------------------------------------------------------------- Properties
+    
+    /**
+     * Return descriptive information about this Manager implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+        return info;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Return the descriptive short name of this Manager implementation.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @return Returns the counterSend_EVT_GET_ALL_SESSIONS.
+     */
+    public long getCounterSend_EVT_GET_ALL_SESSIONS() {
+        return counterSend_EVT_GET_ALL_SESSIONS;
+    }
+    
+    /**
+     * @return Returns the counterSend_EVT_SESSION_ACCESSED.
+     */
+    public long getCounterSend_EVT_SESSION_ACCESSED() {
+        return counterSend_EVT_SESSION_ACCESSED;
+    }
+    
+    /**
+     * @return Returns the counterSend_EVT_SESSION_CREATED.
+     */
+    public long getCounterSend_EVT_SESSION_CREATED() {
+        return counterSend_EVT_SESSION_CREATED;
+    }
+
+    /**
+     * @return Returns the counterSend_EVT_SESSION_DELTA.
+     */
+    public long getCounterSend_EVT_SESSION_DELTA() {
+        return counterSend_EVT_SESSION_DELTA;
+    }
+
+    /**
+     * @return Returns the counterSend_EVT_SESSION_EXPIRED.
+     */
+    public long getCounterSend_EVT_SESSION_EXPIRED() {
+        return counterSend_EVT_SESSION_EXPIRED;
+    }
+    /**
+     * @return Returns the counterSend_EVT_ALL_SESSION_DATA.
+     */
+    public long getCounterSend_EVT_ALL_SESSION_DATA() {
+        return counterSend_EVT_ALL_SESSION_DATA;
+    }
+
+    /**
+     * @return Returns the counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE.
+     */
+    public int getCounterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE() {
+        return counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE;
+    }
+    /**
+     * @return Returns the counterReceive_EVT_ALL_SESSION_DATA.
+     */
+    public long getCounterReceive_EVT_ALL_SESSION_DATA() {
+        return counterReceive_EVT_ALL_SESSION_DATA;
+    }
+    
+    /**
+     * @return Returns the counterReceive_EVT_GET_ALL_SESSIONS.
+     */
+    public long getCounterReceive_EVT_GET_ALL_SESSIONS() {
+        return counterReceive_EVT_GET_ALL_SESSIONS;
+    }
+    
+    /**
+     * @return Returns the counterReceive_EVT_SESSION_ACCESSED.
+     */
+    public long getCounterReceive_EVT_SESSION_ACCESSED() {
+        return counterReceive_EVT_SESSION_ACCESSED;
+    }
+    
+    /**
+     * @return Returns the counterReceive_EVT_SESSION_CREATED.
+     */
+    public long getCounterReceive_EVT_SESSION_CREATED() {
+        return counterReceive_EVT_SESSION_CREATED;
+    }
+    
+    /**
+     * @return Returns the counterReceive_EVT_SESSION_DELTA.
+     */
+    public long getCounterReceive_EVT_SESSION_DELTA() {
+        return counterReceive_EVT_SESSION_DELTA;
+    }
+    
+    /**
+     * @return Returns the counterReceive_EVT_SESSION_EXPIRED.
+     */
+    public long getCounterReceive_EVT_SESSION_EXPIRED() {
+        return counterReceive_EVT_SESSION_EXPIRED;
+    }
+    
+    
+    /**
+     * @return Returns the counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE.
+     */
+    public int getCounterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE() {
+        return counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE;
+    }
+    
+    /**
+     * @return Returns the processingTime.
+     */
+    public long getProcessingTime() {
+        return processingTime;
+    }
+    /**
+     * @return Returns the sessionReplaceCounter.
+     */
+    public long getSessionReplaceCounter() {
+        return sessionReplaceCounter;
+    }
+    
+    /**
+     * Number of session creations that failed due to maxActiveSessions
+     * 
+     * @return The count
+     */
+    public int getRejectedSessions() {
+        return rejectedSessions;
+    }
+
+    public void setRejectedSessions(int rejectedSessions) {
+        this.rejectedSessions = rejectedSessions;
+    }
+
+    /**
+     * @return Returns the counterNoStateTransfered.
+     */
+    public int getCounterNoStateTransfered() {
+        return counterNoStateTransfered;
+    }
+    
+    public int getReceivedQueueSize() {
+        return receivedMessageQueue.size() ;
+    }
+    
+    /**
+     * @return Returns the stateTransferTimeout.
+     */
+    public int getStateTransferTimeout() {
+        return stateTransferTimeout;
+    }
+    /**
+     * @param timeoutAllSession The timeout
+     */
+    public void setStateTransferTimeout(int timeoutAllSession) {
+        this.stateTransferTimeout = timeoutAllSession;
+    }
+
+    /**
+     * is session state transfered complete?
+     * 
+     */
+    public boolean getStateTransfered() {
+        return stateTransfered;
+    }
+
+    /**
+     * set that state ist complete transfered  
+     * @param stateTransfered
+     */
+    public void setStateTransfered(boolean stateTransfered) {
+        this.stateTransfered = stateTransfered;
+    }
+    
+    /**
+     * @return Returns the sendAllSessionsWaitTime in msec
+     */
+    public int getSendAllSessionsWaitTime() {
+        return sendAllSessionsWaitTime;
+    }
+    
+    /**
+     * @param sendAllSessionsWaitTime The sendAllSessionsWaitTime to set at msec.
+     */
+    public void setSendAllSessionsWaitTime(int sendAllSessionsWaitTime) {
+        this.sendAllSessionsWaitTime = sendAllSessionsWaitTime;
+    }
+    
+    /**
+     * @return Returns the sendClusterDomainOnly.
+     */
+    public boolean isSendClusterDomainOnly() {
+        return sendClusterDomainOnly;
+    }
+    
+    /**
+     * @param sendClusterDomainOnly The sendClusterDomainOnly to set.
+     */
+    public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) {
+        this.sendClusterDomainOnly = sendClusterDomainOnly;
+    }
+
+    /**
+     * @return Returns the stateTimestampDrop.
+     */
+    public boolean isStateTimestampDrop() {
+        return stateTimestampDrop;
+    }
+    
+    /**
+     * @param isTimestampDrop The new flag value
+     */
+    public void setStateTimestampDrop(boolean isTimestampDrop) {
+        this.stateTimestampDrop = isTimestampDrop;
+    }
+    
+    /**
+     * Return the maximum number of active Sessions allowed, or -1 for no limit.
+     */
+    public int getMaxActiveSessions() {
+        return (this.maxActiveSessions);
+    }
+
+    /**
+     * Set the maximum number of actives Sessions allowed, or -1 for no limit.
+     * 
+     * @param max
+     *            The new maximum number of sessions
+     */
+    public void setMaxActiveSessions(int max) {
+        int oldMaxActiveSessions = this.maxActiveSessions;
+        this.maxActiveSessions = max;
+        support.firePropertyChange("maxActiveSessions", new Integer(oldMaxActiveSessions), new Integer(this.maxActiveSessions));
+    }
+    
+    /**
+     * 
+     * @return Returns the sendAllSessions.
+     */
+    public boolean isSendAllSessions() {
+        return sendAllSessions;
+    }
+    
+    /**
+     * @param sendAllSessions The sendAllSessions to set.
+     */
+    public void setSendAllSessions(boolean sendAllSessions) {
+        this.sendAllSessions = sendAllSessions;
+    }
+    
+    /**
+     * @return Returns the sendAllSessionsSize.
+     */
+    public int getSendAllSessionsSize() {
+        return sendAllSessionsSize;
+    }
+    
+    /**
+     * @param sendAllSessionsSize The sendAllSessionsSize to set.
+     */
+    public void setSendAllSessionsSize(int sendAllSessionsSize) {
+        this.sendAllSessionsSize = sendAllSessionsSize;
+    }
+    
+    /**
+     * @return Returns the notifySessionListenersOnReplication.
+     */
+    public boolean isNotifySessionListenersOnReplication() {
+        return notifySessionListenersOnReplication;
+    }
+    
+    /**
+     * @param notifyListenersCreateSessionOnReplication The notifySessionListenersOnReplication to set.
+     */
+    public void setNotifySessionListenersOnReplication(boolean notifyListenersCreateSessionOnReplication) {
+        this.notifySessionListenersOnReplication = notifyListenersCreateSessionOnReplication;
+    }
+    
+    
+    public boolean isExpireSessionsOnShutdown() {
+        return expireSessionsOnShutdown;
+    }
+
+    public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown) {
+        this.expireSessionsOnShutdown = expireSessionsOnShutdown;
+    }
+    
+    public boolean isNotifyListenersOnReplication() {
+        return notifyListenersOnReplication;
+    }
+
+    public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) {
+        this.notifyListenersOnReplication = notifyListenersOnReplication;
+    }
+
+    
+    /**
+     * @return Returns the defaultMode.
+     */
+    public boolean isDefaultMode() {
+        return defaultMode;
+    }
+    /**
+     * @param defaultMode The defaultMode to set.
+     */
+    public void setDefaultMode(boolean defaultMode) {
+        this.defaultMode = defaultMode;
+    }
+    
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(CatalinaCluster cluster) {
+        this.cluster = cluster;
+    }
+
+    /**
+     * Set the Container with which this Manager has been associated. If it is a
+     * Context (the usual case), listen for changes to the session timeout
+     * property.
+     * 
+     * @param container
+     *            The associated Container
+     */
+    public void setContainer(Container container) {
+        // De-register from the old Container (if any)
+        if ((this.container != null) && (this.container instanceof Context))
+            ((Context) this.container).removePropertyChangeListener(this);
+
+        // Default processing provided by our superclass
+        super.setContainer(container);
+
+        // Register with the new Container (if any)
+        if ((this.container != null) && (this.container instanceof Context)) {
+            setMaxInactiveInterval(((Context) this.container).getSessionTimeout() * 60);
+            ((Context) this.container).addPropertyChangeListener(this);
+        }
+
+    }
+    
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Construct and return a new session object, based on the default settings
+     * specified by this Manager's properties. The session id will be assigned
+     * by this method, and available via the getId() method of the returned
+     * session. If a new session cannot be created for any reason, return
+     * <code>null</code>.
+     * 
+     * @exception IllegalStateException
+     *                if a new session cannot be instantiated for any reason
+     * 
+     * Construct and return a new session object, based on the default settings
+     * specified by this Manager's properties. The session id will be assigned
+     * by this method, and available via the getId() method of the returned
+     * session. If a new session cannot be created for any reason, return
+     * <code>null</code>.
+     * 
+     * @exception IllegalStateException
+     *                if a new session cannot be instantiated for any reason
+     */
+    public Session createSession(String sessionId) {
+        return createSession(sessionId, true);
+    }
+
+    /**
+     * create new session with check maxActiveSessions and send session creation
+     * to other cluster nodes.
+     * 
+     * @param distribute
+     * @return The session
+     */
+    public Session createSession(String sessionId, boolean distribute) {
+        if ((maxActiveSessions >= 0) && (sessions.size() >= maxActiveSessions)) {
+            rejectedSessions++;
+            throw new IllegalStateException(sm.getString("deltaManager.createSession.ise"));
+        }
+        DeltaSession session = (DeltaSession) super.createSession(sessionId) ;
+        if (distribute) {
+            sendCreateSession(session.getId(), session);
+        }
+        if (log.isDebugEnabled())
+            log.debug(sm.getString("deltaManager.createSession.newSession",session.getId(), new Integer(sessions.size())));
+        return (session);
+
+    }
+
+    /**
+     * Send create session evt to all backup node
+     * @param sessionId
+     * @param session
+     */
+    protected void sendCreateSession(String sessionId, DeltaSession session) {
+        if(cluster.getMembers().length > 0 ) {
+            SessionMessage msg = 
+                new SessionMessageImpl(getName(),
+                                       SessionMessage.EVT_SESSION_CREATED, 
+                                       null, 
+                                       sessionId,
+                                       sessionId + "-" + System.currentTimeMillis());
+            if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.sendMessage.newSession",name, sessionId));
+            msg.setTimestamp(session.getCreationTime());
+            counterSend_EVT_SESSION_CREATED++;
+            send(msg);
+        }
+    }
+    
+    /**
+     * Send messages to other backup member (domain or all)
+     * @param msg Session message
+     */
+    protected void send(SessionMessage msg) {
+        if(cluster != null) {
+            if(isSendClusterDomainOnly())
+                cluster.sendClusterDomain(msg);
+            else
+                cluster.send(msg);
+        }
+    }
+
+    /**
+     * Create DeltaSession
+     * @see org.apache.catalina.Manager#createEmptySession()
+     */
+    public Session createEmptySession() {
+        return getNewDeltaSession() ;
+    }
+    
+    /**
+     * Get new session class to be used in the doLoad() method.
+     */
+    protected DeltaSession getNewDeltaSession() {
+        return new DeltaSession(this);
+    }
+
+    /**
+     * Load Deltarequest from external node
+     * Load the Class at container classloader
+     * @see DeltaRequest#readExternal(java.io.ObjectInput)
+     * @param session
+     * @param data message data
+     * @return The request
+     * @throws ClassNotFoundException
+     * @throws IOException
+     */
+    protected DeltaRequest deserializeDeltaRequest(DeltaSession session, byte[] data) throws ClassNotFoundException, IOException {
+        ReplicationStream ois = getReplicationStream(data);
+        session.getDeltaRequest().readExternal(ois);
+        ois.close();
+        return session.getDeltaRequest();
+    }
+
+    /**
+     * serialize DeltaRequest
+     * @see DeltaRequest#writeExternal(java.io.ObjectOutput)
+     * 
+     * @param deltaRequest
+     * @return serialized delta request
+     * @throws IOException
+     */
+    protected byte[] serializeDeltaRequest(DeltaRequest deltaRequest) throws IOException {
+        return deltaRequest.serialize();
+    }
+
+    /**
+     * Load sessions from other cluster node.
+     * FIXME replace currently sessions with same id without notifcation.
+     * FIXME SSO handling is not really correct with the session replacement!
+     * @exception ClassNotFoundException
+     *                if a serialized class cannot be found during the reload
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    protected void deserializeSessions(byte[] data) throws ClassNotFoundException,IOException {
+
+        // Initialize our internal data structures
+        //sessions.clear(); //should not do this
+        // Open an input stream to the specified pathname, if any
+        ClassLoader originalLoader = Thread.currentThread().getContextClassLoader();
+        ObjectInputStream ois = null;
+        // Load the previously unloaded active sessions
+        try {
+            ois = getReplicationStream(data);
+            Integer count = (Integer) ois.readObject();
+            int n = count.intValue();
+            for (int i = 0; i < n; i++) {
+                DeltaSession session = (DeltaSession) createEmptySession();
+                session.readObjectData(ois);
+                session.setManager(this);
+                session.setValid(true);
+                session.setPrimarySession(false);
+                //in case the nodes in the cluster are out of
+                //time synch, this will make sure that we have the
+                //correct timestamp, isValid returns true, cause
+                // accessCount=1
+                session.access();
+                //make sure that the session gets ready to expire if
+                // needed
+                session.setAccessCount(0);
+                session.resetDeltaRequest();
+                // FIXME How inform other session id cache like SingleSignOn
+                // increment sessionCounter to correct stats report
+                if (findSession(session.getIdInternal()) == null ) {
+                    sessionCounter++;
+                } else {
+                    sessionReplaceCounter++;
+                    // FIXME better is to grap this sessions again !
+                    if (log.isWarnEnabled()) log.warn(sm.getString("deltaManager.loading.existing.session",session.getIdInternal()));
+                }
+                add(session);
+            }
+        } catch (ClassNotFoundException e) {
+            log.error(sm.getString("deltaManager.loading.cnfe", e), e);
+            throw e;
+        } catch (IOException e) {
+            log.error(sm.getString("deltaManager.loading.ioe", e), e);
+            throw e;
+        } finally {
+            // Close the input stream
+            try {
+                if (ois != null) ois.close();
+            } catch (IOException f) {
+                // ignored
+            }
+            ois = null;
+            if (originalLoader != null) Thread.currentThread().setContextClassLoader(originalLoader);
+        }
+
+    }
+
+    
+
+    /**
+     * Save any currently active sessions in the appropriate persistence
+     * mechanism, if any. If persistence is not supported, this method returns
+     * without doing anything.
+     * 
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    protected byte[] serializeSessions(Session[] currentSessions) throws IOException {
+
+        // Open an output stream to the specified pathname, if any
+        ByteArrayOutputStream fos = null;
+        ObjectOutputStream oos = null;
+
+        try {
+            fos = new ByteArrayOutputStream();
+            oos = new ObjectOutputStream(new BufferedOutputStream(fos));
+            oos.writeObject(new Integer(currentSessions.length));
+            for(int i=0 ; i < currentSessions.length;i++) {
+                ((DeltaSession)currentSessions[i]).writeObjectData(oos);                
+            }
+            // Flush and close the output stream
+            oos.flush();
+        } catch (IOException e) {
+            log.error(sm.getString("deltaManager.unloading.ioe", e), e);
+            throw e;
+        } finally {
+            if (oos != null) {
+                try {
+                    oos.close();
+                } catch (IOException f) {
+                    ;
+                }
+                oos = null;
+            }
+        }
+        // send object data as byte[]
+        return fos.toByteArray();
+    }
+
+    // ------------------------------------------------------ Lifecycle Methods
+
+    /**
+     * Add a lifecycle event listener to this component.
+     * 
+     * @param listener
+     *            The listener to add
+     */
+    public void addLifecycleListener(LifecycleListener listener) {
+        lifecycle.addLifecycleListener(listener);
+    }
+
+    /**
+     * Get the lifecycle listeners associated with this lifecycle. If this
+     * Lifecycle has no listeners registered, a zero-length array is returned.
+     */
+    public LifecycleListener[] findLifecycleListeners() {
+        return lifecycle.findLifecycleListeners();
+    }
+
+    /**
+     * Remove a lifecycle event listener from this component.
+     * 
+     * @param listener
+     *            The listener to remove
+     */
+    public void removeLifecycleListener(LifecycleListener listener) {
+        lifecycle.removeLifecycleListener(listener);
+    }
+
+    /**
+     * Prepare for the beginning of active use of the public methods of this
+     * component. This method should be called after <code>configure()</code>,
+     * and before any of the public methods of the component are utilized.
+     * 
+     * @exception LifecycleException
+     *                if this component detects a fatal error that prevents this
+     *                component from being used
+     */
+    public void start() throws LifecycleException {
+        if (!initialized) init();
+
+        // Validate and update our current component state
+        if (started) {
+            return;
+        }
+        started = true;
+        lifecycle.fireLifecycleEvent(START_EVENT, null);
+
+        // Force initialization of the random number generator
+        generateSessionId();
+
+        // Load unloaded sessions, if any
+        try {
+            //the channel is already running
+            Cluster cluster = getCluster() ;
+            // stop remove cluster binding
+            //wow, how many nested levels of if statements can we have ;)
+            if(cluster == null) {
+                Container context = getContainer() ;
+                if(context != null && context instanceof Context) {
+                     Container host = context.getParent() ;
+                     if(host != null && host instanceof Host) {
+                         cluster = host.getCluster();
+                         if(cluster != null && cluster instanceof CatalinaCluster) {
+                             setCluster((CatalinaCluster) cluster) ;
+                         } else {
+                             Container engine = host.getParent() ;
+                             if(engine != null && engine instanceof Engine) {
+                                 cluster = engine.getCluster();
+                                 if(cluster != null && cluster instanceof CatalinaCluster) {
+                                     setCluster((CatalinaCluster) cluster) ;
+                                 }
+                             } else {
+                                     cluster = null ;
+                             }
+                         }
+                     }
+                }
+            }
+            if (cluster == null) {
+                log.error(sm.getString("deltaManager.noCluster", getName()));
+                return;
+            } else {
+                if (log.isInfoEnabled()) {
+                    String type = "unknown" ;
+                    if( cluster.getContainer() instanceof Host){
+                        type = "Host" ;
+                    } else if( cluster.getContainer() instanceof Engine){
+                        type = "Engine" ;
+                    }
+                    log.info(sm.getString("deltaManager.registerCluster", getName(), type, cluster.getClusterName()));
+                }
+            }
+            if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.startClustering", getName()));
+            //to survice context reloads, as only a stop/start is called, not
+            // createManager
+            ((CatalinaCluster)cluster).addManager(getName(), this);
+
+            getAllClusterSessions();
+
+        } catch (Throwable t) {
+            log.error(sm.getString("deltaManager.managerLoad"), t);
+        }
+    }
+
+    /**
+     * get from first session master the backup from all clustered sessions
+     * @see #findSessionMasterMember()
+     */
+    public synchronized void getAllClusterSessions() {
+        if (cluster != null && cluster.getMembers().length > 0) {
+            long beforeSendTime = System.currentTimeMillis();
+            Member mbr = findSessionMasterMember();
+            if(mbr == null) { // No domain member found
+                 return;
+            }
+            SessionMessage msg = new SessionMessageImpl(this.getName(),SessionMessage.EVT_GET_ALL_SESSIONS, null, "GET-ALL","GET-ALL-" + getName());
+            // set reference time
+            stateTransferCreateSendTime = beforeSendTime ;
+            // request session state
+            counterSend_EVT_GET_ALL_SESSIONS++;
+            stateTransfered = false ;
+            // FIXME This send call block the deploy thread, when sender waitForAck is enabled
+            try {
+                synchronized(receivedMessageQueue) {
+                     receiverQueue = true ;
+                }
+                cluster.send(msg, mbr);
+                if (log.isWarnEnabled()) log.warn(sm.getString("deltaManager.waitForSessionState",getName(), mbr));
+                // FIXME At sender ack mode this method check only the state transfer and resend is a problem!
+                waitForSendAllSessions(beforeSendTime);
+            } finally {
+                synchronized(receivedMessageQueue) {
+                    for (Iterator iter = receivedMessageQueue.iterator(); iter.hasNext();) {
+                        SessionMessage smsg = (SessionMessage) iter.next();
+                        if (!stateTimestampDrop) {
+                            messageReceived(smsg, smsg.getAddress() != null ? (Member) smsg.getAddress() : null);
+                        } else {
+                            if (smsg.getEventType() != SessionMessage.EVT_GET_ALL_SESSIONS && smsg.getTimestamp() >= stateTransferCreateSendTime) {
+                                // FIXME handle EVT_GET_ALL_SESSIONS later
+                                messageReceived(smsg,smsg.getAddress() != null ? (Member) smsg.getAddress() : null);
+                            } else {
+                                if (log.isWarnEnabled()) {
+                                    log.warn(sm.getString("deltaManager.dropMessage",getName(), smsg.getEventTypeString(),new Date(stateTransferCreateSendTime), new Date(smsg.getTimestamp())));
+                                }
+                            }
+                        }
+                    }        
+                    receivedMessageQueue.clear();
+                    receiverQueue = false ;
+                }
+           }
+        } else {
+            if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.noMembers", getName()));
+        }
+    }
+
+    /**
+     * Register cross context session at replication valve thread local
+     * @param session cross context session
+     */
+    protected void registerSessionAtReplicationValve(DeltaSession session) {
+        if(replicationValve == null) {
+            if(container instanceof StandardContext && ((StandardContext)container).getCrossContext()) {
+                Cluster cluster = getCluster() ;
+                if(cluster != null && cluster instanceof CatalinaCluster) {
+                    Valve[] valves = ((CatalinaCluster)cluster).getValves();
+                    if(valves != null && valves.length > 0) {
+                        for(int i=0; replicationValve == null && i < valves.length ; i++ ){
+                            if(valves[i] instanceof ReplicationValve) replicationValve = (ReplicationValve)valves[i] ;
+                        }//for
+
+                        if(replicationValve == null && log.isDebugEnabled()) {
+                            log.debug("no ReplicationValve found for CrossContext Support");
+                        }//endif 
+                    }//end if
+                }//endif
+            }//end if
+        }//end if
+        if(replicationValve != null) {
+            replicationValve.registerReplicationSession(session);
+        }
+    }
+    
+    /**
+     * Find the master of the session state
+     * @return master member of sessions 
+     */
+    protected Member findSessionMasterMember() {
+        Member mbr = null;
+        Member mbrs[] = cluster.getMembers();
+        if(mbrs.length != 0 ) mbr = mbrs[0];
+        if(mbr == null && log.isWarnEnabled()) log.warn(sm.getString("deltaManager.noMasterMember",getName(), ""));
+        if(mbr != null && log.isDebugEnabled()) log.warn(sm.getString("deltaManager.foundMasterMember",getName(), mbr));
+        return mbr;
+    }
+
+    /**
+     * Wait that cluster session state is transfer or timeout after 60 Sec
+     * With stateTransferTimeout == -1 wait that backup is transfered (forever mode)
+     */
+    protected void waitForSendAllSessions(long beforeSendTime) {
+        long reqStart = System.currentTimeMillis();
+        long reqNow = reqStart ;
+        boolean isTimeout = false;
+        if(getStateTransferTimeout() > 0) {
+            // wait that state is transfered with timeout check
+            do {
+                try {
+                    Thread.sleep(100);
+                } catch (Exception sleep) {
+                    //
+                }
+                reqNow = System.currentTimeMillis();
+                isTimeout = ((reqNow - reqStart) > (1000 * getStateTransferTimeout()));
+            } while ((!getStateTransfered()) && (!isTimeout));
+        } else {
+            if(getStateTransferTimeout() == -1) {
+                // wait that state is transfered
+                do {
+                    try {
+                        Thread.sleep(100);
+                    } catch (Exception sleep) {
+                    }
+                } while ((!getStateTransfered()));
+                reqNow = System.currentTimeMillis();
+            }
+        }
+        if (isTimeout || (!getStateTransfered())) {
+            counterNoStateTransfered++ ;
+            log.error(sm.getString("deltaManager.noSessionState",getName(),new Date(beforeSendTime),new Long(reqNow - beforeSendTime)));
+        } else {
+            if (log.isInfoEnabled())
+                log.info(sm.getString("deltaManager.sessionReceived",getName(), new Date(beforeSendTime), new Long(reqNow - beforeSendTime)));
+        }
+    }
+
+    /**
+     * Gracefully terminate the active use of the public methods of this
+     * component. This method should be the last one called on a given instance
+     * of this component.
+     * 
+     * @exception LifecycleException
+     *                if this component detects a fatal error that needs to be
+     *                reported
+     */
+    public void stop() throws LifecycleException {
+
+        if (log.isDebugEnabled())
+            log.debug(sm.getString("deltaManager.stopped", getName()));
+
+
+        // Validate and update our current component state
+        if (!started)
+            throw new LifecycleException(sm.getString("deltaManager.notStarted"));
+        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
+        started = false;
+
+        // Expire all active sessions
+        if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.expireSessions", getName()));
+        Session sessions[] = findSessions();
+        for (int i = 0; i < sessions.length; i++) {
+            DeltaSession session = (DeltaSession) sessions[i];
+            if (!session.isValid())
+                continue;
+            try {
+                session.expire(true, isExpireSessionsOnShutdown());
+            } catch (Throwable ignore) {
+                ;
+            } 
+        }
+
+        // Require a new random number generator if we are restarted
+        this.random = null;
+        getCluster().removeManager(getName(),this);
+        replicationValve = null;
+        if (initialized) {
+            destroy();
+        }
+    }
+
+    // ----------------------------------------- PropertyChangeListener Methods
+
+    /**
+     * Process property change events from our associated Context.
+     * 
+     * @param event
+     *            The property change event that has occurred
+     */
+    public void propertyChange(PropertyChangeEvent event) {
+
+        // Validate the source of this event
+        if (!(event.getSource() instanceof Context))
+            return;
+        // Process a relevant property change
+        if (event.getPropertyName().equals("sessionTimeout")) {
+            try {
+                setMaxInactiveInterval(((Integer) event.getNewValue()).intValue() * 60);
+            } catch (NumberFormatException e) {
+                log.error(sm.getString("deltaManager.sessionTimeout", event.getNewValue()));
+            }
+        }
+
+    }
+
+    // -------------------------------------------------------- Replication
+    // Methods
+
+    /**
+     * A message was received from another node, this is the callback method to
+     * implement if you are interested in receiving replication messages.
+     * 
+     * @param cmsg -
+     *            the message received.
+     */
+    public void messageDataReceived(ClusterMessage cmsg) {
+        if (cmsg != null && cmsg instanceof SessionMessage) {
+            SessionMessage msg = (SessionMessage) cmsg;
+            switch (msg.getEventType()) {
+                case SessionMessage.EVT_GET_ALL_SESSIONS:
+                case SessionMessage.EVT_SESSION_CREATED: 
+                case SessionMessage.EVT_SESSION_EXPIRED: 
+                case SessionMessage.EVT_SESSION_ACCESSED:
+                case SessionMessage.EVT_SESSION_DELTA: {
+                    synchronized(receivedMessageQueue) {
+                        if(receiverQueue) {
+                            receivedMessageQueue.add(msg);
+                            return ;
+                        }
+                    }
+                   break;
+                }
+                default: {
+                    //we didn't queue, do nothing
+                    break;
+                }
+            } //switch
+            
+            messageReceived(msg, msg.getAddress() != null ? (Member) msg.getAddress() : null);
+        }
+    }
+
+    /**
+     * When the request has been completed, the replication valve will notify
+     * the manager, and the manager will decide whether any replication is
+     * needed or not. If there is a need for replication, the manager will
+     * create a session message and that will be replicated. The cluster
+     * determines where it gets sent.
+     * 
+     * @param sessionId -
+     *            the sessionId that just completed.
+     * @return a SessionMessage to be sent,
+     */
+    public ClusterMessage requestCompleted(String sessionId) {
+        try {
+            DeltaSession session = (DeltaSession) findSession(sessionId);
+            DeltaRequest deltaRequest = session.getDeltaRequest();
+            SessionMessage msg = null;
+            boolean isDeltaRequest = false ;
+            synchronized(deltaRequest) {
+                isDeltaRequest = deltaRequest.getSize() > 0 ;
+                if (isDeltaRequest) {    
+                    counterSend_EVT_SESSION_DELTA++;
+                    byte[] data = serializeDeltaRequest(deltaRequest);
+                    msg = new SessionMessageImpl(getName(),
+                                                 SessionMessage.EVT_SESSION_DELTA, 
+                                                 data, 
+                                                 sessionId,
+                                                 sessionId + "-" + System.currentTimeMillis());
+                    session.resetDeltaRequest();
+                }  
+            }
+            if(!isDeltaRequest) {
+                if(!session.isPrimarySession()) {               
+                    counterSend_EVT_SESSION_ACCESSED++;
+                    msg = new SessionMessageImpl(getName(),
+                                                 SessionMessage.EVT_SESSION_ACCESSED, 
+                                                 null, 
+                                                 sessionId,
+                                                 sessionId + "-" + System.currentTimeMillis());
+                    if (log.isDebugEnabled()) {
+                        log.debug(sm.getString("deltaManager.createMessage.accessChangePrimary",getName(), sessionId));
+                    }
+                }    
+            } else { // log only outside synch block!
+                if (log.isDebugEnabled()) {
+                    log.debug(sm.getString("deltaManager.createMessage.delta",getName(), sessionId));
+                }
+            }
+            session.setPrimarySession(true);
+            //check to see if we need to send out an access message
+            if ((msg == null)) {
+                long replDelta = System.currentTimeMillis() - session.getLastTimeReplicated();
+                if (replDelta > (getMaxInactiveInterval() * 1000)) {
+                    counterSend_EVT_SESSION_ACCESSED++;
+                    msg = new SessionMessageImpl(getName(),
+                                                 SessionMessage.EVT_SESSION_ACCESSED, 
+                                                 null,
+                                                 sessionId, 
+                                                 sessionId + "-" + System.currentTimeMillis());
+                    if (log.isDebugEnabled()) {
+                        log.debug(sm.getString("deltaManager.createMessage.access", getName(),sessionId));
+                    }
+                }
+
+            }
+
+            //update last replicated time
+            if (msg != null) session.setLastTimeReplicated(System.currentTimeMillis());
+            return msg;
+        } catch (IOException x) {
+            log.error(sm.getString("deltaManager.createMessage.unableCreateDeltaRequest",sessionId), x);
+            return null;
+        }
+
+    }
+    /**
+     * Reset manager statistics
+     */
+    public synchronized void resetStatistics() {
+        processingTime = 0 ;
+        expiredSessions = 0 ;
+        rejectedSessions = 0 ;
+        sessionReplaceCounter = 0 ;
+        counterNoStateTransfered = 0 ;
+        maxActive = getActiveSessions() ;
+        sessionCounter = getActiveSessions() ;
+        counterReceive_EVT_ALL_SESSION_DATA = 0;
+        counterReceive_EVT_GET_ALL_SESSIONS = 0;
+        counterReceive_EVT_SESSION_ACCESSED = 0 ;
+        counterReceive_EVT_SESSION_CREATED = 0 ;
+        counterReceive_EVT_SESSION_DELTA = 0 ;
+        counterReceive_EVT_SESSION_EXPIRED = 0 ;
+        counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0;
+        counterSend_EVT_ALL_SESSION_DATA = 0;
+        counterSend_EVT_GET_ALL_SESSIONS = 0;
+        counterSend_EVT_SESSION_ACCESSED = 0 ;
+        counterSend_EVT_SESSION_CREATED = 0 ;
+        counterSend_EVT_SESSION_DELTA = 0 ;
+        counterSend_EVT_SESSION_EXPIRED = 0 ;
+        counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0;
+        
+    }
+   
+    //  -------------------------------------------------------- persistence handler
+
+    public void load() {
+
+    }
+
+    public void unload() {
+
+    }
+
+    //  -------------------------------------------------------- expire
+
+    /**
+     * send session expired to other cluster nodes
+     * 
+     * @param id
+     *            session id
+     */
+    protected void sessionExpired(String id) {
+        counterSend_EVT_SESSION_EXPIRED++ ;
+        SessionMessage msg = new SessionMessageImpl(getName(),SessionMessage.EVT_SESSION_EXPIRED, null, id, id+ "-EXPIRED-MSG");
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.expire",getName(), id));
+        send(msg);
+    }
+
+    /**
+     * Exipre all find sessions.
+     */
+    public void expireAllLocalSessions()
+    {
+        long timeNow = System.currentTimeMillis();
+        Session sessions[] = findSessions();
+        int expireDirect  = 0 ;
+        int expireIndirect = 0 ;
+        
+        if(log.isDebugEnabled()) log.debug("Start expire all sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
+        for (int i = 0; i < sessions.length; i++) {
+            if (sessions[i] instanceof DeltaSession) {
+                DeltaSession session = (DeltaSession) sessions[i];
+                if (session.isPrimarySession()) {
+                    if (session.isValid()) {
+                        session.expire();
+                        expireDirect++;
+                    } else {
+                        expireIndirect++;
+                    }//end if
+                }//end if
+            }//end if
+        }//for
+        long timeEnd = System.currentTimeMillis();
+        if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " exipre processingTime " + (timeEnd - timeNow) + " expired direct sessions: " + expireDirect + " expired direct sessions: " + expireIndirect);
+      
+    }
+    
+    /**
+     * When the manager expires session not tied to a request. The cluster will
+     * periodically ask for a list of sessions that should expire and that
+     * should be sent across the wire.
+     * 
+     * @return The invalidated sessions array
+     */
+    public String[] getInvalidatedSessions() {
+        return new String[0];
+    }
+
+    //  -------------------------------------------------------- message receive
+
+    /**
+     * Test that sender and local domain is the same
+     */
+    protected boolean checkSenderDomain(SessionMessage msg,Member sender) {
+        boolean sameDomain= true;
+        if (!sameDomain && log.isWarnEnabled()) {
+                log.warn(sm.getString("deltaManager.receiveMessage.fromWrongDomain",
+                         new Object[] {getName(), 
+                         msg.getEventTypeString(), 
+                         sender,
+                         "",
+                         "" }));
+        }
+        return sameDomain ;
+    }
+
+    /**
+     * This method is called by the received thread when a SessionMessage has
+     * been received from one of the other nodes in the cluster.
+     * 
+     * @param msg -
+     *            the message received
+     * @param sender -
+     *            the sender of the message, this is used if we receive a
+     *            EVT_GET_ALL_SESSION message, so that we only reply to the
+     *            requesting node
+     */
+    protected void messageReceived(SessionMessage msg, Member sender) {
+        if(isSendClusterDomainOnly() && !checkSenderDomain(msg,sender)) {
+            return;
+        }
+        ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+        try {
+            
+            ClassLoader[] loaders = getClassLoaders();
+            if ( loaders != null && loaders.length > 0) Thread.currentThread().setContextClassLoader(loaders[0]);
+            if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.eventType",getName(), msg.getEventTypeString(), sender));
+            switch (msg.getEventType()) {
+                case SessionMessage.EVT_GET_ALL_SESSIONS: {
+                    handleGET_ALL_SESSIONS(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_ALL_SESSION_DATA: {
+                    handleALL_SESSION_DATA(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE: {
+                    handleALL_SESSION_TRANSFERCOMPLETE(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_CREATED: {
+                    handleSESSION_CREATED(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_EXPIRED: {
+                    handleSESSION_EXPIRED(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_ACCESSED: {
+                    handleSESSION_ACCESSED(msg,sender);
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_DELTA: {
+                   handleSESSION_DELTA(msg,sender);
+                   break;
+                }
+                default: {
+                    //we didn't recognize the message type, do nothing
+                    break;
+                }
+            } //switch
+        } catch (Exception x) {
+            log.error(sm.getString("deltaManager.receiveMessage.error",getName()), x);
+        } finally {
+            Thread.currentThread().setContextClassLoader(contextLoader);
+        }
+    }
+
+    // -------------------------------------------------------- message receiver handler
+
+
+    /**
+     * handle receive session state is complete transfered
+     * @param msg
+     * @param sender
+     */
+    protected void handleALL_SESSION_TRANSFERCOMPLETE(SessionMessage msg, Member sender) {
+        counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE++ ;
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.transfercomplete",getName(), sender.getHost(), new Integer(sender.getPort())));
+        stateTransferCreateSendTime = msg.getTimestamp() ;
+        stateTransfered = true ;
+    }
+
+    /**
+     * handle receive session delta
+     * @param msg
+     * @param sender
+     * @throws IOException
+     * @throws ClassNotFoundException
+     */
+    protected void handleSESSION_DELTA(SessionMessage msg, Member sender) throws IOException, ClassNotFoundException {
+        counterReceive_EVT_SESSION_DELTA++;
+        byte[] delta = msg.getSession();
+        DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+        if (session != null) {
+            if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.delta",getName(), msg.getSessionID()));
+            DeltaRequest dreq = deserializeDeltaRequest(session, delta);
+            dreq.execute(session, notifyListenersOnReplication);
+            session.setPrimarySession(false);
+        }
+    }
+
+    /**
+     * handle receive session is access at other node ( primary session is now false)
+     * @param msg
+     * @param sender
+     * @throws IOException
+     */
+    protected void handleSESSION_ACCESSED(SessionMessage msg,Member sender) throws IOException {
+        counterReceive_EVT_SESSION_ACCESSED++;
+        DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+        if (session != null) {
+            if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.accessed",getName(), msg.getSessionID()));
+            session.access();
+            session.setPrimarySession(false);
+            session.endAccess();
+        }
+    }
+
+    /**
+     * handle receive session is expire at other node ( expire session also here)
+     * @param msg
+     * @param sender
+     * @throws IOException
+     */
+    protected void handleSESSION_EXPIRED(SessionMessage msg,Member sender) throws IOException {
+        counterReceive_EVT_SESSION_EXPIRED++;
+        DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+        if (session != null) {
+            if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.expired",getName(), msg.getSessionID()));
+            session.expire(notifySessionListenersOnReplication, false);
+        }
+    }
+
+    /**
+     * handle receive new session is created at other node (create backup - primary false)
+     * @param msg
+     * @param sender
+     */
+    protected void handleSESSION_CREATED(SessionMessage msg,Member sender) {
+        counterReceive_EVT_SESSION_CREATED++;
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.createNewSession",getName(), msg.getSessionID()));
+        DeltaSession session = (DeltaSession) createEmptySession();
+        session.setManager(this);
+        session.setValid(true);
+        session.setPrimarySession(false);
+        session.setCreationTime(msg.getTimestamp());
+        session.access();
+        if(notifySessionListenersOnReplication)
+            session.setId(msg.getSessionID());
+        else
+            session.setIdInternal(msg.getSessionID());
+        session.resetDeltaRequest();
+        session.endAccess();
+
+    }
+
+    /**
+     * handle receive sessions from other not ( restart )
+     * @param msg
+     * @param sender
+     * @throws ClassNotFoundException
+     * @throws IOException
+     */
+    protected void handleALL_SESSION_DATA(SessionMessage msg,Member sender) throws ClassNotFoundException, IOException {
+        counterReceive_EVT_ALL_SESSION_DATA++;
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataBegin",getName()));
+        byte[] data = msg.getSession();
+        deserializeSessions(data);
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataAfter",getName()));
+        //stateTransferred = true;
+    }
+
+    /**
+     * handle receive that other node want all sessions ( restart )
+     * a) send all sessions with one message
+     * b) send session at blocks
+     * After sending send state is complete transfered
+     * @param msg
+     * @param sender
+     * @throws IOException
+     */
+    protected void handleGET_ALL_SESSIONS(SessionMessage msg, Member sender) throws IOException {
+        counterReceive_EVT_GET_ALL_SESSIONS++;
+        //get a list of all the session from this manager
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.unloadingBegin", getName()));
+        // Write the number of active sessions, followed by the details
+        // get all sessions and serialize without sync
+        Session[] currentSessions = findSessions();
+        long findSessionTimestamp = System.currentTimeMillis() ;
+        if (isSendAllSessions()) {
+            sendSessions(sender, currentSessions, findSessionTimestamp);
+        } else {
+            // send session at blocks
+            int len = currentSessions.length < getSendAllSessionsSize() ? currentSessions.length : getSendAllSessionsSize();
+            Session[] sendSessions = new Session[len];
+            for (int i = 0; i < currentSessions.length; i += getSendAllSessionsSize()) {
+                len = i + getSendAllSessionsSize() > currentSessions.length ? currentSessions.length - i : getSendAllSessionsSize();
+                System.arraycopy(currentSessions, i, sendSessions, 0, len);
+                sendSessions(sender, sendSessions,findSessionTimestamp);
+                if (getSendAllSessionsWaitTime() > 0) {
+                    try {
+                        Thread.sleep(getSendAllSessionsWaitTime());
+                    } catch (Exception sleep) {
+                    }
+                }//end if
+            }//for
+        }//end if
+        
+        SessionMessage newmsg = new SessionMessageImpl(name,SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE, null,"SESSION-STATE-TRANSFERED", "SESSION-STATE-TRANSFERED"+ getName());
+        newmsg.setTimestamp(findSessionTimestamp);
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.allSessionTransfered",getName()));
+        counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE++;
+        cluster.send(newmsg, sender);
+    }
+
+
+    /**
+     * send a block of session to sender
+     * @param sender
+     * @param currentSessions
+     * @param sendTimestamp
+     * @throws IOException
+     */
+    protected void sendSessions(Member sender, Session[] currentSessions,long sendTimestamp) throws IOException {
+        byte[] data = serializeSessions(currentSessions);
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.unloadingAfter",getName()));
+        SessionMessage newmsg = new SessionMessageImpl(name,SessionMessage.EVT_ALL_SESSION_DATA, data,"SESSION-STATE", "SESSION-STATE-" + getName());
+        newmsg.setTimestamp(sendTimestamp);
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.allSessionData",getName()));
+        counterSend_EVT_ALL_SESSION_DATA++;
+        cluster.send(newmsg, sender);
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/DeltaRequest.java b/java/org/apache/catalina/ha/session/DeltaRequest.java
new file mode 100644 (file)
index 0000000..e81c07f
--- /dev/null
@@ -0,0 +1,386 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha.session;
+
+/**
+ * This class is used to track the series of actions that happens when
+ * a request is executed. These actions will then translate into invokations of methods 
+ * on the actual session.
+ * This class is NOT thread safe. One DeltaRequest per session
+ * @author <a href="mailto:fhanik@apache.org">Filip Hanik</a>
+ * @version 1.0
+ */
+
+import java.io.Externalizable;
+import java.security.Principal;
+import java.util.LinkedList;
+
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.util.StringManager;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+
+public class DeltaRequest implements Externalizable {
+
+    public static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog( DeltaRequest.class );
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm = StringManager
+            .getManager(Constants.Package);
+
+    public static final int TYPE_ATTRIBUTE = 0;
+    public static final int TYPE_PRINCIPAL = 1;
+    public static final int TYPE_ISNEW = 2;
+    public static final int TYPE_MAXINTERVAL = 3;
+
+    public static final int ACTION_SET = 0;
+    public static final int ACTION_REMOVE = 1;
+
+    public static final String NAME_PRINCIPAL = "__SET__PRINCIPAL__";
+    public static final String NAME_MAXINTERVAL = "__SET__MAXINTERVAL__";
+    public static final String NAME_ISNEW = "__SET__ISNEW__";
+
+    private String sessionId;
+    private LinkedList actions = new LinkedList();
+    private LinkedList actionPool = new LinkedList();
+    
+    private boolean recordAllActions = false;
+
+    public DeltaRequest() {
+        
+    }
+    
+    public DeltaRequest(String sessionId, boolean recordAllActions) {
+        this.recordAllActions=recordAllActions;
+        if(sessionId != null)
+            setSessionId(sessionId);
+    }
+
+
+    public void setAttribute(String name, Object value) {
+        int action = (value==null)?ACTION_REMOVE:ACTION_SET;
+        addAction(TYPE_ATTRIBUTE,action,name,value);
+    }
+
+    public void removeAttribute(String name) {
+        int action = ACTION_REMOVE;
+        addAction(TYPE_ATTRIBUTE,action,name,null);
+    }
+
+    public void setMaxInactiveInterval(int interval) {
+        int action = ACTION_SET;
+        addAction(TYPE_MAXINTERVAL,action,NAME_MAXINTERVAL,new Integer(interval));
+    }
+    
+    /**
+     * convert principal at SerializablePrincipal for backup nodes.
+     * Only support principals from type {@link GenericPrincipal GenericPrincipal}
+     * @param p Session principal
+     * @see GenericPrincipal
+     */
+    public void setPrincipal(Principal p) {
+        int action = (p==null)?ACTION_REMOVE:ACTION_SET;
+        SerializablePrincipal sp = null;
+        if ( p != null ) {
+            if(p instanceof GenericPrincipal) {
+                sp = SerializablePrincipal.createPrincipal((GenericPrincipal)p);
+                if(log.isDebugEnabled())
+                    log.debug(sm.getString("deltaRequest.showPrincipal", p.getName() , getSessionId()));
+            } else
+                log.error(sm.getString("deltaRequest.wrongPrincipalClass",p.getClass().getName()));
+        }
+        addAction(TYPE_PRINCIPAL,action,NAME_PRINCIPAL,sp);
+    }
+
+    public void setNew(boolean n) {
+        int action = ACTION_SET;
+        addAction(TYPE_ISNEW,action,NAME_ISNEW,new Boolean(n));
+    }
+
+    protected synchronized void addAction(int type,
+                             int action,
+                             String name,
+                             Object value) {
+        AttributeInfo info = null;
+        if ( this.actionPool.size() > 0 ) {
+            try {
+                info = (AttributeInfo) actionPool.removeFirst();
+            }catch ( Exception x ) {
+                log.error("Unable to remove element:",x);
+                info = new AttributeInfo(type, action, name, value);
+            }
+            info.init(type,action,name,value);
+        } else {
+            info = new AttributeInfo(type, action, name, value);
+        }
+        //if we have already done something to this attribute, make sure
+        //we don't send multiple actions across the wire
+        if ( !recordAllActions) {
+            try {
+                actions.remove(info);
+            } catch (java.util.NoSuchElementException x) {
+                //do nothing, we wanted to remove it anyway
+            }
+        }
+        //add the action
+        actions.addLast(info);
+    }
+    
+    public void execute(DeltaSession session) {
+        execute(session,true);
+    }
+
+    public synchronized void execute(DeltaSession session, boolean notifyListeners) {
+        if ( !this.sessionId.equals( session.getId() ) )
+            throw new java.lang.IllegalArgumentException("Session id mismatch, not executing the delta request");
+        session.access();
+        for ( int i=0; i<actions.size(); i++ ) {
+            AttributeInfo info = (AttributeInfo)actions.get(i);
+            switch ( info.getType() ) {
+                case TYPE_ATTRIBUTE: {
+                    if ( info.getAction() == ACTION_SET ) {
+                        if ( log.isTraceEnabled() ) log.trace("Session.setAttribute('"+info.getName()+"', '"+info.getValue()+"')");
+                        session.setAttribute(info.getName(), info.getValue(),notifyListeners,false);
+                    }  else {
+                        if ( log.isTraceEnabled() ) log.trace("Session.removeAttribute('"+info.getName()+"')");
+                        session.removeAttribute(info.getName(),notifyListeners,false);
+                    }
+                        
+                    break;
+                }//case
+                case TYPE_ISNEW: {
+                if ( log.isTraceEnabled() ) log.trace("Session.setNew('"+info.getValue()+"')");
+                    session.setNew(((Boolean)info.getValue()).booleanValue(),false);
+                    break;
+                }//case
+                case TYPE_MAXINTERVAL: {
+                    if ( log.isTraceEnabled() ) log.trace("Session.setMaxInactiveInterval('"+info.getValue()+"')");
+                    session.setMaxInactiveInterval(((Integer)info.getValue()).intValue(),false);
+                    break;
+                }//case
+                case TYPE_PRINCIPAL: {
+                    Principal p = null;
+                    if ( info.getAction() == ACTION_SET ) {
+                        SerializablePrincipal sp = (SerializablePrincipal)info.getValue();
+                        p = (Principal)sp.getPrincipal(session.getManager().getContainer().getRealm());
+                    }
+                    session.setPrincipal(p,false);
+                    break;
+                }//case
+                default : throw new java.lang.IllegalArgumentException("Invalid attribute info type="+info);
+            }//switch
+        }//for
+        session.endAccess();
+        reset();
+    }
+
+    public synchronized void reset() {
+        while ( actions.size() > 0 ) {
+            try {
+                AttributeInfo info = (AttributeInfo) actions.removeFirst();
+                info.recycle();
+                actionPool.addLast(info);
+            }catch  ( Exception x ) {
+                log.error("Unable to remove element",x);
+            }
+        }
+        actions.clear();
+    }
+    
+    public String getSessionId() {
+        return sessionId;
+    }
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+        if ( sessionId == null ) {
+            new Exception("Session Id is null for setSessionId").fillInStackTrace().printStackTrace();
+        }
+    }
+    public int getSize() {
+        return actions.size();
+    }
+    
+    public synchronized void clear() {
+        actions.clear();
+        actionPool.clear();
+    }
+    
+    public synchronized void readExternal(java.io.ObjectInput in) throws IOException,ClassNotFoundException {
+        //sessionId - String
+        //recordAll - boolean
+        //size - int
+        //AttributeInfo - in an array
+        reset();
+        sessionId = in.readUTF();
+        recordAllActions = in.readBoolean();
+        int cnt = in.readInt();
+        if (actions == null)
+            actions = new LinkedList();
+        else
+            actions.clear();
+        for (int i = 0; i < cnt; i++) {
+            AttributeInfo info = null;
+            if (this.actionPool.size() > 0) {
+                try {
+                    info = (AttributeInfo) actionPool.removeFirst();
+                } catch ( Exception x )  {
+                    log.error("Unable to remove element",x);
+                    info = new AttributeInfo(-1,-1,null,null);
+                }
+            }
+            else {
+                info = new AttributeInfo(-1,-1,null,null);
+            }
+            info.readExternal(in);
+            actions.addLast(info);
+        }//for
+    }
+        
+
+
+    public synchronized void writeExternal(java.io.ObjectOutput out ) throws java.io.IOException {
+        //sessionId - String
+        //recordAll - boolean
+        //size - int
+        //AttributeInfo - in an array
+        out.writeUTF(getSessionId());
+        out.writeBoolean(recordAllActions);
+        out.writeInt(getSize());
+        for ( int i=0; i<getSize(); i++ ) {
+            AttributeInfo info = (AttributeInfo)actions.get(i);
+            info.writeExternal(out);
+        }
+    }
+    
+    /**
+     * serialize DeltaRequest
+     * @see DeltaRequest#writeExternal(java.io.ObjectOutput)
+     * 
+     * @param deltaRequest
+     * @return serialized delta request
+     * @throws IOException
+     */
+    protected byte[] serialize() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(bos);
+        writeExternal(oos);
+        oos.flush();
+        oos.close();
+        return bos.toByteArray();
+    }
+    
+    private static class AttributeInfo implements java.io.Externalizable {
+        private String name = null;
+        private Object value = null;
+        private int action;
+        private int type;
+
+        public AttributeInfo() {}
+
+        public AttributeInfo(int type,
+                             int action,
+                             String name,
+                             Object value) {
+            super();
+            init(type,action,name,value);
+        }
+
+        public void init(int type,
+                         int action,
+                         String name,
+                         Object value) {
+            this.name = name;
+            this.value = value;
+            this.action = action;
+            this.type = type;
+        }
+
+        public int getType() {
+            return type;
+        }
+
+        public int getAction() {
+            return action;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+        public int hashCode() {
+            return name.hashCode();
+        }
+
+        public String getName() {
+            return name;
+        }
+        
+        public void recycle() {
+            name = null;
+            value = null;
+            type=-1;
+            action=-1;
+        }
+
+        public boolean equals(Object o) {
+            if ( ! (o instanceof AttributeInfo ) ) return false;
+            AttributeInfo other =  (AttributeInfo)o;
+            return other.getName().equals(this.getName());
+        }
+        
+        public synchronized void readExternal(java.io.ObjectInput in ) throws IOException,ClassNotFoundException {
+            //type - int
+            //action - int
+            //name - String
+            //hasvalue - boolean
+            //value - object
+            type = in.readInt();
+            action = in.readInt();
+            name = in.readUTF();
+            boolean hasValue = in.readBoolean();
+            if ( hasValue ) value = in.readObject();
+        }
+
+        public synchronized void writeExternal(java.io.ObjectOutput out) throws IOException {
+            //type - int
+            //action - int
+            //name - String
+            //hasvalue - boolean
+            //value - object
+            out.writeInt(getType());
+            out.writeInt(getAction());
+            out.writeUTF(getName());
+            out.writeBoolean(getValue()!=null);
+            if (getValue()!=null) out.writeObject(getValue());
+        }
+        
+        public String toString() {
+            StringBuffer buf = new StringBuffer("AttributeInfo[type=");
+            buf.append(getType()).append(", action=").append(getAction());
+            buf.append(", name=").append(getName()).append(", value=").append(getValue());
+            buf.append(", addr=").append(super.toString()).append("]");
+            return buf.toString();
+        }
+
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/DeltaSession.java b/java/org/apache/catalina/ha/session/DeltaSession.java
new file mode 100644 (file)
index 0000000..e55626e
--- /dev/null
@@ -0,0 +1,749 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionContext;
+
+import org.apache.catalina.Manager;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterSession;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.session.StandardSession;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.tribes.tipis.ReplicatedMapEntry;
+import org.apache.catalina.util.Enumerator;
+import org.apache.catalina.util.StringManager;
+import org.apache.catalina.session.StandardManager;
+import org.apache.catalina.session.ManagerBase;
+
+/**
+ *
+ * Similar to the StandardSession except that this session will keep
+ * track of deltas during a request.
+ *
+ * @author Filip Hanik
+ * @version $Revision: 372887 $ $Date: 2006-01-27 09:58:58 -0600 (Fri, 27 Jan 2006) $
+ */
+
+public class DeltaSession extends StandardSession implements Externalizable,ClusterSession,ReplicatedMapEntry {
+
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(DeltaSession.class);
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm = StringManager.getManager(Constants.Package);
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * only the primary session will expire, or be able to expire due to
+     * inactivity. This is set to false as soon as I receive this session over
+     * the wire in a session message. That means that someone else has made a
+     * request on another server.
+     */
+    private transient boolean isPrimarySession = true;
+
+    /**
+     * The delta request contains all the action info
+     *
+     */
+    private transient DeltaRequest deltaRequest = null;
+
+    /**
+     * Last time the session was replicatd, used for distributed expiring of
+     * session
+     */
+    private transient long lastTimeReplicated = System.currentTimeMillis();
+
+
+    protected Lock diffLock = new ReentrantReadWriteLock().writeLock();
+
+    private long version;
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * Construct a new Session associated with the specified Manager.
+     *
+     * @param manager
+     *            The manager with which this Session is associated
+     */
+    public DeltaSession() {
+        this(null);
+    }
+
+    public DeltaSession(Manager manager) {
+        super(manager);
+        this.resetDeltaRequest();
+    }
+
+    // ----------------------------------------------------- ReplicatedMapEntry
+
+    /**
+         * Has the object changed since last replication
+         * and is not in a locked state
+         * @return boolean
+         */
+        public boolean isDirty() {
+            return getDeltaRequest().getSize()>0;
+        }
+
+        /**
+         * If this returns true, the map will extract the diff using getDiff()
+         * Otherwise it will serialize the entire object.
+         * @return boolean
+         */
+        public boolean isDiffable() {
+            return true;
+        }
+
+        /**
+         * Returns a diff and sets the dirty map to false
+         * @return byte[]
+         * @throws IOException
+         */
+        public byte[] getDiff() throws IOException {
+            return getDeltaRequest().serialize();
+        }
+
+        public ClassLoader[] getClassLoaders() {
+            if ( manager instanceof BackupManager ) return ((BackupManager)manager).getClassLoaders();
+            else if ( manager instanceof ClusterManagerBase ) return ((ClusterManagerBase)manager).getClassLoaders();
+            else if ( manager instanceof StandardManager ) {
+                StandardManager sm = (StandardManager)manager;
+                return ClusterManagerBase.getClassLoaders(sm.getContainer());
+            } else if ( manager instanceof ManagerBase ) {
+                ManagerBase mb = (ManagerBase)manager;
+                return ClusterManagerBase.getClassLoaders(mb.getContainer());
+            }//end if
+            return null;
+        }
+
+        /**
+         * Applies a diff to an existing object.
+         * @param diff byte[]
+         * @param offset int
+         * @param length int
+         * @throws IOException
+         */
+        public void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException {
+            ReplicationStream stream = ((ClusterManager)getManager()).getReplicationStream(diff,offset,length);
+            getDeltaRequest().readExternal(stream);
+            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+            try {
+                ClassLoader[] loaders = getClassLoaders();
+                if ( loaders != null && loaders.length >0 ) Thread.currentThread().setContextClassLoader(loaders[0]);
+                getDeltaRequest().execute(this);
+            }finally {
+                Thread.currentThread().setContextClassLoader(contextLoader);
+            }
+        }
+
+        /**
+         * Resets the current diff state and resets the dirty flag
+         */
+        public void resetDiff() {
+            resetDeltaRequest();
+        }
+
+        /**
+         * Lock during serialization
+         */
+        public void lock() {
+            diffLock.lock();
+        }
+
+        /**
+         * Unlock after serialization
+         */
+        public void unlock() {
+            diffLock.unlock();
+        }
+
+        public void setOwner(Object owner) {
+            if ( owner instanceof ClusterManager && getManager()==null) {
+                ClusterManager cm = (ClusterManager)owner;
+                this.setManager(cm);
+                this.setValid(true);
+                this.setPrimarySession(false);
+                this.access();
+                if (cm.isNotifyListenersOnReplication()) this.setId(getIdInternal());
+                this.resetDeltaRequest();
+                this.endAccess();
+            }
+        }
+    // ----------------------------------------------------- Session Properties
+
+    /**
+     * returns true if this session is the primary session, if that is the case,
+     * the manager can expire it upon timeout.
+     */
+    public boolean isPrimarySession() {
+        return isPrimarySession;
+    }
+
+    /**
+     * Sets whether this is the primary session or not.
+     *
+     * @param primarySession
+     *            Flag value
+     */
+    public void setPrimarySession(boolean primarySession) {
+        this.isPrimarySession = primarySession;
+    }
+
+    /**
+     * Set the session identifier for this session without notify listeners.
+     *
+     * @param id
+     *            The new session identifier
+     */
+    public void setIdInternal(String id) {
+        super.setId(id);
+        resetDeltaRequest();
+    }
+
+    /**
+     * Set the session identifier for this session.
+     *
+     * @param id
+     *            The new session identifier
+     */
+    public void setId(String id) {
+        setIdInternal(id);
+    }
+
+   
+
+    /**
+     * Return the last client access time without invalidation check
+     * @see #getLastAccessedTime().
+     */
+    public long getLastAccessedTimeInternal() {
+        return (this.lastAccessedTime);
+    }
+
+
+   
+    public void setMaxInactiveInterval(int interval, boolean addDeltaRequest) {
+        super.maxInactiveInterval = interval;
+        if (isValid && interval == 0) {
+            expire();
+        } else {
+            if (addDeltaRequest && (deltaRequest != null))
+                deltaRequest.setMaxInactiveInterval(interval);
+        }
+    }
+
+    /**
+     * Set the <code>isNew</code> flag for this session.
+     *
+     * @param isNew
+     *            The new value for the <code>isNew</code> flag
+     */
+    public void setNew(boolean isNew) {
+        setNew(isNew, true);
+    }
+
+    public void setNew(boolean isNew, boolean addDeltaRequest) {
+        super.setNew(isNew);
+        if (addDeltaRequest && (deltaRequest != null))
+            deltaRequest.setNew(isNew);
+    }
+
+    /**
+     * Set the authenticated Principal that is associated with this Session.
+     * This provides an <code>Authenticator</code> with a means to cache a
+     * previously authenticated Principal, and avoid potentially expensive
+     * <code>Realm.authenticate()</code> calls on every request.
+     *
+     * @param principal
+     *            The new Principal, or <code>null</code> if none
+     */
+    public void setPrincipal(Principal principal) {
+        setPrincipal(principal, true);
+    }
+
+    public void setPrincipal(Principal principal, boolean addDeltaRequest) {
+        try { 
+            lock();
+            super.setPrincipal(principal);
+            if (addDeltaRequest && (deltaRequest != null))
+                deltaRequest.setPrincipal(principal);
+        } finally {
+            unlock();
+        }
+    }
+
+    /**
+     * Return the <code>isValid</code> flag for this session.
+     */
+    public boolean isValid() {
+        if (this.expiring) {
+            return true;
+        }
+        if (!this.isValid) {
+            return false;
+        }
+        if (accessCount.get() > 0) {
+            return true;
+        }
+        if (maxInactiveInterval >= 0) {
+            long timeNow = System.currentTimeMillis();
+            int timeIdle = (int) ( (timeNow - thisAccessedTime) / 1000L);
+            if (isPrimarySession()) {
+                if (timeIdle >= maxInactiveInterval) {
+                    expire(true);
+                }
+            } else {
+                if (timeIdle >= (2 * maxInactiveInterval)) {
+                    //if the session has been idle twice as long as allowed,
+                    //the primary session has probably crashed, and no other
+                    //requests are coming in. that is why we do this. otherwise
+                    //we would have a memory leak
+                    expire(true, false);
+                }
+            }
+        }
+        return (this.isValid);
+    }
+
+    // ------------------------------------------------- Session Public Methods
+
+    /**
+     * Perform the internal processing required to invalidate this session,
+     * without triggering an exception if the session has already expired.
+     *
+     * @param notify
+     *            Should we notify listeners about the demise of this session?
+     */
+    public void expire(boolean notify) {
+        expire(notify, true);
+    }
+
+    public void expire(boolean notify, boolean notifyCluster) {
+        String expiredId = getIdInternal();
+        super.expire(notify);
+
+        if (notifyCluster) {
+            if (log.isDebugEnabled())
+                log.debug(sm.getString("deltaSession.notifying",
+                                       ((DeltaManager)manager).getName(), 
+                                       new Boolean(isPrimarySession()), 
+                                       expiredId));
+            if ( manager instanceof DeltaManager ) {
+                ( (DeltaManager) manager).sessionExpired(expiredId);
+            }
+        }
+    }
+
+    /**
+     * Release all object references, and initialize instance variables, in
+     * preparation for reuse of this object.
+     */
+    public void recycle() {
+        super.recycle();
+        deltaRequest.clear();
+    }
+
+
+    /**
+     * Return a string representation of this object.
+     */
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("DeltaSession[");
+        sb.append(id);
+        sb.append("]");
+        return (sb.toString());
+    }
+
+    // ------------------------------------------------ Session Package Methods
+
+    public synchronized void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
+        readObjectData(in);
+    }
+
+
+    /**
+     * Read a serialized version of the contents of this session object from the
+     * specified object input stream, without requiring that the StandardSession
+     * itself have been serialized.
+     *
+     * @param stream
+     *            The object input stream to read from
+     *
+     * @exception ClassNotFoundException
+     *                if an unknown class is specified
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    public void readObjectData(ObjectInput stream) throws ClassNotFoundException, IOException {
+        readObject(stream);
+    }
+
+    /**
+     * Write a serialized version of the contents of this session object to the
+     * specified object output stream, without requiring that the
+     * StandardSession itself have been serialized.
+     *
+     * @param stream
+     *            The object output stream to write to
+     *
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    public void writeObjectData(ObjectOutput stream) throws IOException {
+        writeObject(stream);
+    }
+
+    public void resetDeltaRequest() {
+        if (deltaRequest == null) {
+            deltaRequest = new DeltaRequest(getIdInternal(), false);
+        } else {
+            deltaRequest.reset();
+            deltaRequest.setSessionId(getIdInternal());
+        }
+    }
+
+    public DeltaRequest getDeltaRequest() {
+        if (deltaRequest == null) resetDeltaRequest();
+        return deltaRequest;
+    }
+
+    // ------------------------------------------------- HttpSession Properties
+
+    // ----------------------------------------------HttpSession Public Methods
+
+
+
+    /**
+     * Remove the object bound with the specified name from this session. If the
+     * session does not have an object bound with this name, this method does
+     * nothing.
+     * <p>
+     * After this method executes, and if the object implements
+     * <code>HttpSessionBindingListener</code>, the container calls
+     * <code>valueUnbound()</code> on the object.
+     *
+     * @param name
+     *            Name of the object to remove from this session.
+     * @param notify
+     *            Should we notify interested listeners that this attribute is
+     *            being removed?
+     *
+     * @exception IllegalStateException
+     *                if this method is called on an invalidated session
+     */
+    public void removeAttribute(String name, boolean notify) {
+        removeAttribute(name, notify, true);
+    }
+
+    public void removeAttribute(String name, boolean notify,boolean addDeltaRequest) {
+        // Validate our current state
+        if (!isValid()) throw new IllegalStateException(sm.getString("standardSession.removeAttribute.ise"));
+        removeAttributeInternal(name, notify, addDeltaRequest);
+    }
+
+    /**
+     * Bind an object to this session, using the specified name. If an object of
+     * the same name is already bound to this session, the object is replaced.
+     * <p>
+     * After this method executes, and if the object implements
+     * <code>HttpSessionBindingListener</code>, the container calls
+     * <code>valueBound()</code> on the object.
+     *
+     * @param name
+     *            Name to which the object is bound, cannot be null
+     * @param value
+     *            Object to be bound, cannot be null
+     *
+     * @exception IllegalArgumentException
+     *                if an attempt is made to add a non-serializable object in
+     *                an environment marked distributable.
+     * @exception IllegalStateException
+     *                if this method is called on an invalidated session
+     */
+    public void setAttribute(String name, Object value) {
+        setAttribute(name, value, true, true);
+    }
+
+    public void setAttribute(String name, Object value, boolean notify,boolean addDeltaRequest) {
+
+        // Name cannot be null
+        if (name == null) throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull"));
+
+        // Null value is the same as removeAttribute()
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+
+        try {
+            lock();
+            super.setAttribute(name,value, notify);
+            if (addDeltaRequest && (deltaRequest != null)) deltaRequest.setAttribute(name, value);
+        } finally {
+            unlock();
+        }
+    }
+
+    // -------------------------------------------- HttpSession Private Methods
+
+    /**
+     * Read a serialized version of this session object from the specified
+     * object input stream.
+     * <p>
+     * <b>IMPLEMENTATION NOTE </b>: The reference to the owning Manager is not
+     * restored by this method, and must be set explicitly.
+     *
+     * @param stream
+     *            The input stream to read from
+     *
+     * @exception ClassNotFoundException
+     *                if an unknown class is specified
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    private void readObject(ObjectInput stream) throws ClassNotFoundException, IOException {
+
+        // Deserialize the scalar instance variables (except Manager)
+        authType = null; // Transient only
+        creationTime = ( (Long) stream.readObject()).longValue();
+        lastAccessedTime = ( (Long) stream.readObject()).longValue();
+        maxInactiveInterval = ( (Integer) stream.readObject()).intValue();
+        isNew = ( (Boolean) stream.readObject()).booleanValue();
+        isValid = ( (Boolean) stream.readObject()).booleanValue();
+        thisAccessedTime = ( (Long) stream.readObject()).longValue();
+        version = ( (Long) stream.readObject()).longValue();
+        boolean hasPrincipal = stream.readBoolean();
+        principal = null;
+        if (hasPrincipal) {
+            principal = SerializablePrincipal.readPrincipal(stream,getManager().getContainer().getRealm());
+        }
+
+        //        setId((String) stream.readObject());
+        id = (String) stream.readObject();
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaSession.readSession", id));
+
+        // Deserialize the attribute count and attribute values
+        if (attributes == null) attributes = new Hashtable();
+        int n = ( (Integer) stream.readObject()).intValue();
+        boolean isValidSave = isValid;
+        isValid = true;
+        for (int i = 0; i < n; i++) {
+            String name = (String) stream.readObject();
+            Object value = (Object) stream.readObject();
+            if ( (value instanceof String) && (value.equals(NOT_SERIALIZED)))
+                continue;
+            attributes.put(name, value);
+        }
+        isValid = isValidSave;
+
+        if (listeners == null) {
+            listeners = new ArrayList();
+        }
+
+        if (notes == null) {
+            notes = new Hashtable();
+        }
+    }
+
+    public synchronized void writeExternal(ObjectOutput out ) throws java.io.IOException {
+        writeObject(out);
+    }
+
+
+    /**
+     * Write a serialized version of this session object to the specified object
+     * output stream.
+     * <p>
+     * <b>IMPLEMENTATION NOTE </b>: The owning Manager will not be stored in the
+     * serialized representation of this Session. After calling
+     * <code>readObject()</code>, you must set the associated Manager
+     * explicitly.
+     * <p>
+     * <b>IMPLEMENTATION NOTE </b>: Any attribute that is not Serializable will
+     * be unbound from the session, with appropriate actions if it implements
+     * HttpSessionBindingListener. If you do not want any such attributes, be
+     * sure the <code>distributable</code> property of the associated Manager
+     * is set to <code>true</code>.
+     *
+     * @param stream
+     *            The output stream to write to
+     *
+     * @exception IOException
+     *                if an input/output error occurs
+     */
+    private void writeObject(ObjectOutput stream) throws IOException {
+
+        // Write the scalar instance variables (except Manager)
+        stream.writeObject(new Long(creationTime));
+        stream.writeObject(new Long(lastAccessedTime));
+        stream.writeObject(new Integer(maxInactiveInterval));
+        stream.writeObject(new Boolean(isNew));
+        stream.writeObject(new Boolean(isValid));
+        stream.writeObject(new Long(thisAccessedTime));
+        stream.writeObject(new Long(version));
+        stream.writeBoolean(getPrincipal() != null);
+        if (getPrincipal() != null) {
+            SerializablePrincipal.writePrincipal((GenericPrincipal) principal,stream);
+        }
+
+        stream.writeObject(id);
+        if (log.isDebugEnabled()) log.debug(sm.getString("deltaSession.writeSession", id));
+
+        // Accumulate the names of serializable and non-serializable attributes
+        String keys[] = keys();
+        ArrayList saveNames = new ArrayList();
+        ArrayList saveValues = new ArrayList();
+        for (int i = 0; i < keys.length; i++) {
+            Object value = null;
+            value = attributes.get(keys[i]);
+            if (value == null)
+                continue;
+            else if (value instanceof Serializable) {
+                saveNames.add(keys[i]);
+                saveValues.add(value);
+            }
+        }
+
+        // Serialize the attribute count and the Serializable attributes
+        int n = saveNames.size();
+        stream.writeObject(new Integer(n));
+        for (int i = 0; i < n; i++) {
+            stream.writeObject( (String) saveNames.get(i));
+            try {
+                stream.writeObject(saveValues.get(i));
+            } catch (NotSerializableException e) {
+                log.error(sm.getString("standardSession.notSerializable",saveNames.get(i), id), e);
+                stream.writeObject(NOT_SERIALIZED);
+                log.error("  storing attribute '" + saveNames.get(i)+ "' with value NOT_SERIALIZED");
+            }
+        }
+
+    }
+
+    // -------------------------------------------------------- Private Methods
+
+    
+
+    /**
+     * Return the value of an attribute without a check for validity.
+     */
+    protected Object getAttributeInternal(String name) {
+        return (attributes.get(name));
+    }
+
+    protected void removeAttributeInternal(String name, boolean notify,
+                                           boolean addDeltaRequest) {
+        try {
+            lock();
+            // Remove this attribute from our collection
+            Object value = attributes.get(name);
+            if (value == null) return;
+
+            super.removeAttributeInternal(name,notify);
+            if (addDeltaRequest && (deltaRequest != null)) deltaRequest.removeAttribute(name);
+
+        }finally {
+            unlock();
+        }
+    }
+
+    protected long getLastTimeReplicated() {
+        return lastTimeReplicated;
+    }
+
+    public long getVersion() {
+        return version;
+    }
+
+    protected void setLastTimeReplicated(long lastTimeReplicated) {
+        this.lastTimeReplicated = lastTimeReplicated;
+    }
+
+    public void setVersion(long version) {
+        this.version = version;
+    }
+
+    protected void setAccessCount(int count) {
+        super.accessCount.set(count);
+    }
+}
+
+// -------------------------------------------------------------- Private Class
+
+/**
+ * This class is a dummy implementation of the <code>HttpSessionContext</code>
+ * interface, to conform to the requirement that such an object be returned when
+ * <code>HttpSession.getSessionContext()</code> is called.
+ *
+ * @author Craig R. McClanahan
+ *
+ * @deprecated As of Java Servlet API 2.1 with no replacement. The interface
+ *             will be removed in a future version of this API.
+ */
+
+final class StandardSessionContext
+    implements HttpSessionContext {
+
+    private HashMap dummy = new HashMap();
+
+    /**
+     * Return the session identifiers of all sessions defined within this
+     * context.
+     *
+     * @deprecated As of Java Servlet API 2.1 with no replacement. This method
+     *             must return an empty <code>Enumeration</code> and will be
+     *             removed in a future version of the API.
+     */
+    public Enumeration getIds() {
+        return (new Enumerator(dummy));
+    }
+
+    /**
+     * Return the <code>HttpSession</code> associated with the specified
+     * session identifier.
+     *
+     * @param id
+     *            Session identifier for which to look up a session
+     *
+     * @deprecated As of Java Servlet API 2.1 with no replacement. This method
+     *             must return null and will be removed in a future version of
+     *             the API.
+     */
+    public HttpSession getSession(String id) {
+        return (null);
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java b/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java
new file mode 100644 (file)
index 0000000..e77c3b3
--- /dev/null
@@ -0,0 +1,543 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Globals;
+import org.apache.catalina.Host;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Manager;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterValve;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.session.ManagerBase;
+import org.apache.catalina.util.LifecycleSupport;
+import org.apache.catalina.util.StringManager;
+import org.apache.catalina.valves.ValveBase;
+
+/**
+ * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node
+ * failure. After a node crashed the next request going to other cluster node.
+ * Now the answering from apache is slower ( make some error handshaking. Very
+ * bad with apache at my windows.). We rewrite now the cookie jsessionid
+ * information to the backup cluster node. After the next response all client
+ * request goes direct to the backup node. The change sessionid send also to all
+ * other cluster nodes. Well, now the session stickyness work directly to the
+ * backup node and traffic don't go back too restarted cluster nodes!
+ * 
+ * At all cluster node you must configure the as ClusterListener since 5.5.10
+ * {@link org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener JvmRouteSessionIDBinderListener}
+ * or before with
+ * org.apache.catalina.ha.session.JvmRouteSessionIDBinderListenerLifecycle.
+ * 
+ * Add this Valve to your host definition at conf/server.xml .
+ * 
+ * Since 5.5.10 as direct cluster valve:<br/>
+ * <pre>
+ *  &lt;Cluster&gt;
+ *  &lt;Valve className=&quot;org.apache.catalina.ha.session.JvmRouteBinderValve&quot; /&gt;  
+ *  &lt;/Cluster&gt;
+ * </pre>
+ * <br />
+ * Before 5.5.10 as Host element:<br/>
+ * <pre>
+ *  &lt;Hostr&gt;
+ *  &lt;Valve className=&quot;org.apache.catalina.ha.session.JvmRouteBinderValve&quot; /&gt;  
+ *  &lt;/Hostr&gt;
+ * </pre>
+ * 
+ * Trick:<br/>
+ * You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes!
+ * Set enable true on all JvmRouteBinderValve backups, disable worker at mod_jk 
+ * and then drop node and restart it! Then enable mod_jk Worker and disable JvmRouteBinderValves again. 
+ * This use case means that only requested session are migrated.
+ * 
+ * @author Peter Rossbach
+ * @version $Revision: 326110 $ $Date: 2005-10-18 09:08:36 -0500 (Tue, 18 Oct 2005) $
+ */
+public class JvmRouteBinderValve extends ValveBase implements ClusterValve, Lifecycle {
+
+    /*--Static Variables----------------------------------------*/
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+            .getLog(JvmRouteBinderValve.class);
+
+    /**
+     * The descriptive information about this implementation.
+     */
+    protected static final String info = "org.apache.catalina.ha.session.JvmRouteBinderValve/1.2";
+
+    /*--Instance Variables--------------------------------------*/
+
+    /**
+     * the cluster
+     */
+    protected CatalinaCluster cluster;
+
+    /**
+     * The string manager for this package.
+     */
+    protected StringManager sm = StringManager.getManager(Constants.Package);
+
+    /**
+     * Has this component been started yet?
+     */
+    protected boolean started = false;
+
+    /**
+     * enabled this component
+     */
+    protected boolean enabled = true;
+
+    /**
+     * number of session that no at this tomcat instanz hosted
+     */
+    protected long numberOfSessions = 0;
+
+    protected String sessionIdAttribute = "org.apache.catalina.ha.session.JvmRouteOrignalSessionID";
+
+    /**
+     * The lifecycle event support for this component.
+     */
+    protected LifecycleSupport lifecycle = new LifecycleSupport(this);
+
+    /*--Logic---------------------------------------------------*/
+
+    /**
+     * Return descriptive information about this implementation.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+
+    /**
+     * set session id attribute to failed node for request.
+     * 
+     * @return Returns the sessionIdAttribute.
+     */
+    public String getSessionIdAttribute() {
+        return sessionIdAttribute;
+    }
+
+    /**
+     * get name of failed reqeust session attribute
+     * 
+     * @param sessionIdAttribute
+     *            The sessionIdAttribute to set.
+     */
+    public void setSessionIdAttribute(String sessionIdAttribute) {
+        this.sessionIdAttribute = sessionIdAttribute;
+    }
+
+    /**
+     * @return Returns the number of migrated sessions.
+     */
+    public long getNumberOfSessions() {
+        return numberOfSessions;
+    }
+
+    /**
+     * @return Returns the enabled.
+     */
+    public boolean getEnabled() {
+        return enabled;
+    }
+
+    /**
+     * @param enabled
+     *            The enabled to set.
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * Detect possible the JVMRoute change at cluster backup node..
+     * 
+     * @param request
+     *            tomcat request being processed
+     * @param response
+     *            tomcat response being processed
+     * @exception IOException
+     *                if an input/output error has occurred
+     * @exception ServletException
+     *                if a servlet error has occurred
+     */
+    public void invoke(Request request, Response response) throws IOException,
+            ServletException {
+
+         if (getEnabled() 
+             && getCluster() != null
+             && request.getContext() != null
+             && request.getContext().getDistributable() ) {
+             // valve cluster can access manager - other cluster handle turnover 
+             // at host level - hopefully!
+             Manager manager = request.getContext().getManager();
+             if (manager != null && manager instanceof ClusterManager
+                     && getCluster().getManager(((ClusterManager)manager).getName()) != null)
+                 handlePossibleTurnover(request, response);
+        }
+        // Pass this request on to the next valve in our pipeline
+        getNext().invoke(request, response);
+    }
+
+    /**
+     * handle possible session turn over.
+     * 
+     * @see JvmRouteBinderValve#handleJvmRoute(Request, Response, String, String)
+     * @param request current request
+     * @param response current response
+     */
+    protected void handlePossibleTurnover(Request request, Response response) {
+        Session session = request.getSessionInternal(false);
+        if (session != null) {
+            long t1 = System.currentTimeMillis();
+            String jvmRoute = getLocalJvmRoute(request);
+            if (jvmRoute == null) {
+                if (log.isWarnEnabled())
+                    log.warn(sm.getString("jvmRoute.missingJvmRouteAttribute"));
+                return;
+            }
+            handleJvmRoute( request, response,session.getIdInternal(), jvmRoute);
+            if (log.isDebugEnabled()) {
+                long t2 = System.currentTimeMillis();
+                long time = t2 - t1;
+                log.debug(sm.getString("jvmRoute.turnoverInfo", new Long(time)));
+            }
+        }
+    }
+
+    /**
+     * get jvmroute from engine
+     * 
+     * @param request current request
+     * @return return jvmRoute from ManagerBase or null
+     */
+    protected String getLocalJvmRoute(Request request) {
+        Manager manager = getManager(request);
+        if(manager instanceof ManagerBase)
+            return ((ManagerBase) manager).getJvmRoute();
+        return null ;
+    }
+
+    /**
+     * get Cluster DeltaManager
+     * 
+     * @param request current request
+     * @return manager or null
+     */
+    protected Manager getManager(Request request) {
+        Manager manager = request.getContext().getManager();
+        if (log.isDebugEnabled()) {
+            if(manager != null)
+                log.debug(sm.getString("jvmRoute.foundManager", manager,  request.getContext().getName()));
+            else 
+                log.debug(sm.getString("jvmRoute.notFoundManager", manager,  request.getContext().getName()));
+        }
+        return manager;
+    }
+
+    /**
+     * @return Returns the cluster.
+     */
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+    
+    /**
+     * @param cluster The cluster to set.
+     */
+    public void setCluster(CatalinaCluster cluster) {
+        this.cluster = cluster;
+    }
+    
+    /**
+     * Handle jvmRoute stickyness after tomcat instance failed. After this
+     * correction a new Cookie send to client with new jvmRoute and the
+     * SessionID change propage to the other cluster nodes.
+     * 
+     * @param request current request
+     * @param response
+     *            Tomcat Response
+     * @param sessionId
+     *            request SessionID from Cookie
+     * @param localJvmRoute
+     *            local jvmRoute
+     */
+    protected void handleJvmRoute(
+            Request request, Response response,String sessionId, String localJvmRoute) {
+        // get requested jvmRoute.
+        String requestJvmRoute = null;
+        int index = sessionId.indexOf(".");
+        if (index > 0) {
+            requestJvmRoute = sessionId
+                    .substring(index + 1, sessionId.length());
+        }
+        if (requestJvmRoute != null && !requestJvmRoute.equals(localJvmRoute)) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString("jvmRoute.failover", requestJvmRoute,
+                        localJvmRoute, sessionId));
+            }
+            // OK - turnover the session ?
+            String newSessionID = sessionId.substring(0, index) + "."
+                    + localJvmRoute;
+            Session catalinaSession = null;
+            try {
+                catalinaSession = getManager(request).findSession(sessionId);
+            } catch (IOException e) {
+                // Hups!
+            }
+            if (catalinaSession != null) {
+                changeSessionID(request, response, sessionId, newSessionID,
+                        catalinaSession);
+                numberOfSessions++;
+            } else {
+                if (log.isDebugEnabled()) {
+                    log.debug(sm.getString("jvmRoute.cannotFindSession",
+                            sessionId));
+                }
+            }
+        }
+    }
+
+    /**
+     * change session id and send to all cluster nodes
+     * 
+     * @param request current request
+     * @param response current response
+     * @param sessionId
+     *            original session id
+     * @param newSessionID
+     *            new session id for node migration
+     * @param catalinaSession
+     *            current session with original session id
+     */
+    protected void changeSessionID(Request request,
+            Response response, String sessionId, String newSessionID, Session catalinaSession) {
+        lifecycle.fireLifecycleEvent("Before session migration",
+                catalinaSession);
+        request.setRequestedSessionId(newSessionID);
+        catalinaSession.setId(newSessionID);
+        if (catalinaSession instanceof DeltaSession)
+            ((DeltaSession) catalinaSession).resetDeltaRequest();
+        if(request.isRequestedSessionIdFromCookie()) setNewSessionCookie(request, response,newSessionID);
+        // set orginal sessionid at request, to allow application detect the
+        // change
+        if (sessionIdAttribute != null && !"".equals(sessionIdAttribute)) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString("jvmRoute.set.orignalsessionid",sessionIdAttribute,sessionId));
+            }
+            request.setAttribute(sessionIdAttribute, sessionId);
+        }
+        // now sending the change to all other clusternode!
+        ClusterManager manager = (ClusterManager)catalinaSession.getManager();
+        sendSessionIDClusterBackup(manager,request,sessionId, newSessionID);
+        lifecycle
+                .fireLifecycleEvent("After session migration", catalinaSession);
+        if (log.isDebugEnabled()) {
+            log.debug(sm.getString("jvmRoute.changeSession", sessionId,
+                    newSessionID));
+        }
+    }
+
+    /**
+     * Send the changed Sessionid to all clusternodes.
+     * 
+     * @see JvmRouteSessionIDBinderListener#messageReceived(ClusterMessage)
+     * @param manager
+     *            ClusterManager
+     * @param sessionId
+     *            current failed sessionid
+     * @param newSessionID
+     *            new session id, bind to the new cluster node
+     */
+    protected void sendSessionIDClusterBackup(ClusterManager manager,Request request,String sessionId,
+            String newSessionID) {
+        SessionIDMessage msg = new SessionIDMessage();
+        msg.setOrignalSessionID(sessionId);
+        msg.setBackupSessionID(newSessionID);
+        Context context = request.getContext();
+        msg.setContextPath(context.getPath());
+        msg.setHost(context.getParent().getName());
+        if(manager.isSendClusterDomainOnly())
+            cluster.sendClusterDomain(msg);
+        else
+            cluster.send(msg);
+    }
+
+    /**
+     * Sets a new cookie for the given session id and response and see
+     * {@link org.apache.catalina.connector.Request#configureSessionCookie(javax.servlet.http.Cookie)}
+     * 
+     * @param request current request
+     * @param response Tomcat Response
+     * @param sessionId The session id
+     */
+    protected void setNewSessionCookie(Request request,
+                                       Response response, String sessionId) {
+        if (response != null) {
+            Context context = request.getContext();
+            if (context.getCookies()) {
+                // set a new session cookie
+                Cookie newCookie = new Cookie(Globals.SESSION_COOKIE_NAME,
+                        sessionId);
+                newCookie.setMaxAge(-1);
+                String contextPath = null;
+                if (!response.getConnector().getEmptySessionPath()
+                        && (context != null)) {
+                    contextPath = context.getEncodedPath();
+                }
+                if ((contextPath != null) && (contextPath.length() > 0)) {
+                    newCookie.setPath(contextPath);
+                } else {
+                    newCookie.setPath("/");
+                }
+                if (request.isSecure()) {
+                    newCookie.setSecure(true);
+                }
+                if (log.isDebugEnabled()) {
+                    log.debug(sm.getString("jvmRoute.newSessionCookie",
+                            sessionId, Globals.SESSION_COOKIE_NAME, newCookie
+                                    .getPath(), new Boolean(newCookie
+                                    .getSecure())));
+                }
+                response.addCookie(newCookie);
+            }
+        }
+    }
+
+    // ------------------------------------------------------ Lifecycle Methods
+
+    /**
+     * Add a lifecycle event listener to this component.
+     * 
+     * @param listener
+     *            The listener to add
+     */
+    public void addLifecycleListener(LifecycleListener listener) {
+
+        lifecycle.addLifecycleListener(listener);
+
+    }
+
+    /**
+     * Get the lifecycle listeners associated with this lifecycle. If this
+     * Lifecycle has no listeners registered, a zero-length array is returned.
+     */
+    public LifecycleListener[] findLifecycleListeners() {
+
+        return lifecycle.findLifecycleListeners();
+
+    }
+
+    /**
+     * Remove a lifecycle event listener from this component.
+     * 
+     * @param listener
+     *            The listener to add
+     */
+    public void removeLifecycleListener(LifecycleListener listener) {
+
+        lifecycle.removeLifecycleListener(listener);
+
+    }
+
+    /**
+     * Prepare for the beginning of active use of the public methods of this
+     * component. This method should be called after <code>configure()</code>,
+     * and before any of the public methods of the component are utilized.
+     * 
+     * @exception LifecycleException
+     *                if this component detects a fatal error that prevents this
+     *                component from being used
+     */
+    public void start() throws LifecycleException {
+
+        // Validate and update our current component state
+        if (started)
+            throw new LifecycleException(sm
+                    .getString("jvmRoute.valve.alreadyStarted"));
+        lifecycle.fireLifecycleEvent(START_EVENT, null);
+        started = true;
+        if (cluster == null) {
+            Container hostContainer = getContainer();
+            // compatibility with JvmRouteBinderValve version 1.1
+            // ( setup at context.xml or context.xml.default )
+            if (!(hostContainer instanceof Host)) {
+                if (log.isWarnEnabled())
+                    log.warn(sm.getString("jvmRoute.configure.warn"));
+                hostContainer = hostContainer.getParent();
+            }
+            if (hostContainer instanceof Host
+                    && ((Host) hostContainer).getCluster() != null) {
+                cluster = (CatalinaCluster) ((Host) hostContainer).getCluster();
+            } else {
+                Container engine = hostContainer.getParent() ;
+                if (engine instanceof Engine
+                        && ((Engine) engine).getCluster() != null) {
+                    cluster = (CatalinaCluster) ((Engine) engine).getCluster();
+                }
+            }
+        }
+        if (cluster == null) {
+            throw new RuntimeException("No clustering support at container "
+                    + container.getName());
+        }
+        
+        if (log.isInfoEnabled())
+            log.info(sm.getString("jvmRoute.valve.started"));
+
+    }
+
+    /**
+     * Gracefully terminate the active use of the public methods of this
+     * component. This method should be the last one called on a given instance
+     * of this component.
+     * 
+     * @exception LifecycleException
+     *                if this component detects a fatal error that needs to be
+     *                reported
+     */
+    public void stop() throws LifecycleException {
+
+        // Validate and update our current component state
+        if (!started)
+            throw new LifecycleException(sm
+                    .getString("jvmRoute.valve.notStarted"));
+        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
+        started = false;
+        cluster = null;
+        numberOfSessions = 0;
+        if (log.isInfoEnabled())
+            log.info(sm.getString("jvmRoute.valve.stopped"));
+
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java
new file mode 100644 (file)
index 0000000..316238b
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+import javax.management.DynamicMBean;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.core.StandardHost;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterListener;
+import org.apache.catalina.util.StringManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tomcat.util.modeler.ManagedBean;
+import org.apache.tomcat.util.modeler.Registry;
+
+/**
+ * Register new JvmRouteSessionIDBinderListener to receive Session ID changes.
+ * 
+ * add following at your server.xml Host section
+ * 
+ * <pre>
+ *        &lt;Host &gt;... 
+ *          &lt;Listener className=&quot;org.apache.catalina.ha.session.JvmRouteSessionIDBinderLifecycleListener&quot; /&gt;
+ *          &lt;Cluster ...&gt;
+ *        &lt;/Host&gt;
+ * </pre>
+ * FIXME add Engine support
+ * @deprecated
+ * @author Peter Rossbach
+ */
+public class JvmRouteSessionIDBinderLifecycleListener implements
+        LifecycleListener {
+    private static Log log = LogFactory
+            .getLog(JvmRouteSessionIDBinderLifecycleListener.class);
+
+    /**
+     * The descriptive information string for this implementation.
+     */
+    private static final String info = "org.apache.catalina.ha.session.JvmRouteSessionIDBinderLifecycleListener/1.0";
+
+    /**
+     * The string resources for this package.
+     */
+    protected static final StringManager sm = StringManager
+            .getManager(Constants.Package);
+
+    private boolean enabled = true;
+
+    private MBeanServer mserver = null;
+
+    private Registry registry = null;
+
+    private ClusterListener sessionMoverListener;
+
+    /*
+     * start and stop cluster
+     * 
+     * @see org.apache.catalina.LifecycleListener#lifecycleEvent(org.apache.catalina.LifecycleEvent)
+     */
+    public void lifecycleEvent(LifecycleEvent event) {
+
+        if (enabled && event.getSource() instanceof StandardHost) {
+
+            if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
+                if (log.isDebugEnabled())
+                    log.debug(sm.getString("jvmRoute.listener.started"));
+                startSessionIDListener((StandardHost) event.getSource());
+            } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
+                if (log.isDebugEnabled())
+                    log.debug(sm.getString("jvmRoute.listener.stopped"));
+                stopSessionIDListener((StandardHost) event.getSource());
+            }
+        }
+    }
+
+    /**
+     * stop sessionID binder at cluster
+     * 
+     * @param host
+     *            clustered host
+     */
+    protected void stopSessionIDListener(StandardHost host) {
+        if (sessionMoverListener != null) {
+            CatalinaCluster cluster = (CatalinaCluster) host.getCluster();
+            cluster.removeClusterListener(sessionMoverListener);
+            if (mserver != null) {
+                try {
+                    ObjectName objectName = getObjectName(host);
+                    mserver.unregisterMBean(objectName);
+                } catch (Exception e) {
+                    log.error(e);
+                }
+            }
+        }
+    }
+
+    /**
+     * @param host
+     * @return The object name
+     * @throws MalformedObjectNameException
+     */
+    protected ObjectName getObjectName(StandardHost host) throws MalformedObjectNameException {
+        ObjectName objectName = new ObjectName(
+                host.getDomain()
+                        + ":type=Listener,name=JvmRouteSessionIDBinderListener,host=" + host.getName());
+        return objectName;
+    }
+
+    /**
+     * start sessionID mover at cluster
+     * 
+     * @param host
+     *            clustered host
+     */
+    protected void startSessionIDListener(StandardHost host) {
+        try {
+            ObjectName objectName = null;
+            getMBeanServer();
+            objectName = getObjectName(host);
+            if (mserver.isRegistered(objectName)) {
+                if (log.isInfoEnabled())
+                    log.info(sm.getString("jvmRoute.run.already"));
+                return;
+            }
+            sessionMoverListener = new JvmRouteSessionIDBinderListener();
+            mserver.registerMBean(getManagedBean(sessionMoverListener),
+                    objectName);
+            CatalinaCluster cluster = (CatalinaCluster) host.getCluster();
+            sessionMoverListener.setCluster(cluster);
+            ((JvmRouteSessionIDBinderListener) sessionMoverListener).start();
+        } catch (Exception ex) {
+            log.error(ex.getMessage(), ex);
+        }
+    }
+
+    protected MBeanServer getMBeanServer() throws Exception {
+        if (mserver == null) {
+            if (MBeanServerFactory.findMBeanServer(null).size() > 0) {
+                mserver = (MBeanServer) MBeanServerFactory
+                        .findMBeanServer(null).get(0);
+            } else {
+                mserver = MBeanServerFactory.createMBeanServer();
+            }
+            registry = Registry.getRegistry(null, null);
+            registry.loadMetadata(this.getClass().getResourceAsStream(
+                    "mbeans-descriptors.xml"));
+        }
+        return (mserver);
+    }
+
+    /**
+     * Returns the ModelMBean
+     * 
+     * @param object
+     *            The Object to get the ModelMBean for
+     * @return The ModelMBean
+     * @throws Exception
+     *             If an error occurs this constructors throws this exception
+     */
+    protected DynamicMBean getManagedBean(Object object) throws Exception {
+        DynamicMBean mbean = null;
+        if (registry != null) {
+            ManagedBean managedBean = registry.findManagedBean(object
+                    .getClass().getName());
+            mbean = managedBean.createMBean(object);
+        }
+        return mbean;
+    }
+
+    /**
+     * @return Returns the enabled.
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * @param enabled
+     *            The enabled to set.
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * Return descriptive information about this Listener implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java
new file mode 100644 (file)
index 0000000..3d23939
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+import java.io.IOException;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.core.StandardEngine;
+import org.apache.catalina.ha.*;
+
+/**
+ * Receive SessionID cluster change from other backup node after primary session
+ * node is failed.
+ * 
+ * @author Peter Rossbach
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public class JvmRouteSessionIDBinderListener extends ClusterListener {
+    /**
+     * The descriptive information about this implementation.
+     */
+    protected static final String info = "org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener/1.1";
+
+    //--Instance Variables--------------------------------------
+
+
+    protected boolean started = false;
+
+    /**
+     * number of session that goes to this cluster node
+     */
+    private long numberOfSessions = 0;
+
+    //--Constructor---------------------------------------------
+
+    public JvmRouteSessionIDBinderListener() {
+    }
+
+    //--Logic---------------------------------------------------
+
+    /**
+     * Return descriptive information about this implementation.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+
+    /**
+     * @return Returns the numberOfSessions.
+     */
+    public long getNumberOfSessions() {
+        return numberOfSessions;
+    }
+
+    /**
+     * Add this Mover as Cluster Listener ( receiver)
+     * 
+     * @throws LifecycleException
+     */
+    public void start() throws LifecycleException {
+        if (started)
+            return;
+        getCluster().addClusterListener(this);
+        started = true;
+        if (log.isInfoEnabled())
+            log.info(sm.getString("jvmRoute.clusterListener.started"));
+    }
+
+    /**
+     * Remove this from Cluster Listener
+     * 
+     * @throws LifecycleException
+     */
+    public void stop() throws LifecycleException {
+        started = false;
+        getCluster().removeClusterListener(this);
+        if (log.isInfoEnabled())
+            log.info(sm.getString("jvmRoute.clusterListener.stopped"));
+    }
+
+    /**
+     * Callback from the cluster, when a message is received, The cluster will
+     * broadcast it invoking the messageReceived on the receiver.
+     * 
+     * @param msg
+     *            ClusterMessage - the message received from the cluster
+     */
+    public void messageReceived(ClusterMessage msg) {
+        if (msg instanceof SessionIDMessage && msg != null) {
+            SessionIDMessage sessionmsg = (SessionIDMessage) msg;
+            if (log.isDebugEnabled())
+                log.debug(sm.getString(
+                        "jvmRoute.receiveMessage.sessionIDChanged", sessionmsg
+                                .getOrignalSessionID(), sessionmsg
+                                .getBackupSessionID(), sessionmsg
+                                .getContextPath()));
+            Container container = getCluster().getContainer();
+            Container host = null ;
+            if(container instanceof Engine) {
+                host = container.findChild(sessionmsg.getHost());
+            } else {
+                host = container ;
+            }
+            if (host != null) {
+                Context context = (Context) host.findChild(sessionmsg
+                        .getContextPath());
+                if (context != null) {
+                    try {
+                        Session session = context.getManager().findSession(
+                                sessionmsg.getOrignalSessionID());
+                        if (session != null) {
+                            session.setId(sessionmsg.getBackupSessionID());
+                        } else if (log.isInfoEnabled())
+                            log.info(sm.getString("jvmRoute.lostSession",
+                                    sessionmsg.getOrignalSessionID(),
+                                    sessionmsg.getContextPath()));
+                    } catch (IOException e) {
+                        log.error(e);
+                    }
+
+                } else if (log.isErrorEnabled())
+                    log.error(sm.getString("jvmRoute.contextNotFound",
+                            sessionmsg.getContextPath(), ((StandardEngine) host
+                                    .getParent()).getJvmRoute()));
+            } else if (log.isErrorEnabled())
+                log.error(sm.getString("jvmRoute.hostNotFound", sessionmsg.getContextPath()));
+        }
+        return;
+    }
+
+    /**
+     * Accept only SessionIDMessages
+     * 
+     * @param msg
+     *            ClusterMessage
+     * @return boolean - returns true to indicate that messageReceived should be
+     *         invoked. If false is returned, the messageReceived method will
+     *         not be invoked.
+     */
+    public boolean accept(ClusterMessage msg) {
+        return (msg instanceof SessionIDMessage);
+    }
+}
+
diff --git a/java/org/apache/catalina/ha/session/LocalStrings.properties b/java/org/apache/catalina/ha/session/LocalStrings.properties
new file mode 100644 (file)
index 0000000..acf4705
--- /dev/null
@@ -0,0 +1,96 @@
+deltaManager.createSession.ise=createSession: Too many active sessions
+deltaManager.createSession.newSession=Created a DeltaSession with Id [{0}] Total count={1}
+deltaManager.createMessage.access=Manager [{0}]: create session message [{1}] access.
+deltaManager.createMessage.accessChangePrimary=Manager [{0}]: create session message [{1}] access to change primary.
+deltaManager.createMessage.allSessionData=Manager [{0}] send all session data.
+deltaManager.createMessage.allSessionTransfered=Manager [{0}] send all session data transfered
+deltaManager.createMessage.delta=Manager [{0}]: create session message [{1}] delta request.
+deltaManager.createMessage.expire=Manager [{0}]: create session message [{1}] expire.
+deltaManager.createMessage.unableCreateDeltaRequest=Unable to serialize delta request for sessionid [{0}]
+deltaManager.dropMessage=Manager [{0}]: Drop message {1} inside GET_ALL_SESSIONS sync phase start date {2} message date {3}
+deltaManager.foundMasterMember=Found for context [{0}] the replication master member [{1}]
+deltaManager.loading.cnfe=ClassNotFoundException while loading persisted sessions: {0}
+deltaManager.loading.existing.session=overload existing session {0} 
+deltaManager.loading.ioe=IOException while loading persisted sessions: {0}
+deltaManager.loading.withContextClassLoader=Manager [{0}]: Loading the object data with a context class loader.
+deltaManager.loading.withoutClassLoader=Manager [{0}]: Loading the object data without a context class loader.
+deltaManager.managerLoad=Exception loading sessions from persistent storage
+deltaManager.noCluster=Starting... no cluster associated with this context: [{0}]
+deltaManager.noMasterMember=Starting... with no other member for context [{0}] at domain [{1}]
+deltaManager.noMembers=Manager [{0}]: skipping state transfer. No members active in cluster group.
+deltaManager.noSessionState=Manager [{0}]: No session state send at {1} received, timing out after {2} ms.
+deltaManager.notStarted=Manager has not yet been started
+deltaManager.sendMessage.newSession=Manager [{0}] send new session ({1})
+deltaManager.expireSessions=Manager [{0}] expiring sessions upon shutdown
+deltaManager.receiveMessage.accessed=Manager [{0}]: received session [{1}] accessed.
+deltaManager.receiveMessage.createNewSession=Manager [{0}]: received session [{1}] created.
+deltaManager.receiveMessage.delta=Manager [{0}]: received session [{1}] delta.
+deltaManager.receiveMessage.error=Manager [{0}]: Unable to receive message through TCP channel
+deltaManager.receiveMessage.eventType=Manager [{0}]: Received SessionMessage of type=({1}) from [{2}]
+deltaManager.receiveMessage.expired=Manager [{0}]: received session [{1}] expired.
+deltaManager.receiveMessage.transfercomplete=Manager [{0}] received from node [{1}:{2}] session state transfered.
+deltaManager.receiveMessage.unloadingAfter=Manager [{0}]: unloading sessions complete
+deltaManager.receiveMessage.unloadingBegin=Manager [{0}]: start unloading sessions
+deltaManager.receiveMessage.allSessionDataAfter=Manager [{0}]: session state deserialized
+deltaManager.receiveMessage.allSessionDataBegin=Manager [{0}]: received session state data
+deltaManager.receiveMessage.fromWrongDomain=Manager [{0}]: Received wrong SessionMessage of type=({1}) from [{2}] with domain [{3}] (localdomain [{4}] 
+deltaManager.registerCluster=Register manager {0} to cluster element {1} with name {2}
+deltaManager.sessionReceived=Manager [{0}]; session state send at {1} received in {2} ms.
+deltaManager.sessionTimeout=Invalid session timeout setting {0}
+deltaManager.startClustering=Starting clustering manager at {0}
+deltaManager.stopped=Manager [{0}] is stopping
+deltaManager.unloading.ioe=IOException while saving persisted sessions: {0}
+deltaManager.waitForSessionState=Manager [{0}], requesting session state from {1}. This operation will timeout if no session state has been received within 60 seconds.
+deltaRequest.showPrincipal=Principal [{0}] is set to session {1}
+deltaRequest.wrongPrincipalClass=DeltaManager only support GenericPrincipal. Your realm used principal class {0}.
+deltaSession.notifying=Notifying cluster of expiration primary={0} sessionId [{1}]
+deltaSession.valueBound.ex=Session bound listener throw an exception
+deltaSession.valueBinding.ex=Session binding listener throw an exception
+deltaSession.valueUnbound.ex=Session unbound listener throw an exception
+deltaSession.readSession=readObject() loading session [{0}]
+deltaSession.readAttribute=session [{0}] loading attribute '{1}' with value '{2}'
+deltaSession.writeSession=writeObject() storing session [{0}]
+jvmRoute.cannotFindSession=Can't find session [{0}]
+jvmRoute.changeSession=Changed session from [{0}] to [{1}]
+jvmRoute.clusterListener.started=Cluster JvmRouteSessionIDBinderListener started
+jvmRoute.clusterListener.stopped=Cluster JvmRouteSessionIDBinderListener stopped
+jvmRoute.configure.warn=Please, setup your JvmRouteBinderValve at host valve, not at context valve!
+jvmRoute.contextNotFound=Context [{0}] not found at node [{1}]!
+jvmRoute.failover=Detected a failover with different jvmRoute - orginal route: [{0}] new one: [{1}] at session id [{2}]
+jvmRoute.foundManager=Found Cluster DeltaManager {0} at {1}
+jvmRoute.hostNotFound=No host found [{0}]
+jvmRoute.listener.started=SessionID Binder Listener started
+jvmRoute.listener.stopped=SessionID Binder Listener stopped
+jvmRoute.lostSession=Lost Session [{0}] at path [{1}]
+jvmRoute.missingJvmRouteAttribute=No engine jvmRoute attribute configured!
+jvmRoute.newSessionCookie=Setting cookie with session id [{0}] name: [{1}] path: [{2}] secure: [{3}]
+jvmRoute.notFoundManager=Not found Cluster DeltaManager {0} at {1}
+jvmRoute.receiveMessage.sessionIDChanged=Cluster JvmRouteSessionIDBinderListener received orginal session ID [{0}] set to new id [{1}] for context path [{2}]
+jvmRoute.run.already=jvmRoute SessionID receiver run already
+jvmRoute.skipURLSessionIDs=Skip reassign jvm route check, sessionid comes from URL!
+jvmRoute.turnoverInfo=Turnover Check time {0} msec
+jvmRoute.valve.alreadyStarted=jvmRoute backup sessionID correction is started
+jvmRoute.valve.notStarted=jvmRoute backup sessionID correction run already
+jvmRoute.valve.started=JvmRouteBinderValve started
+jvmRoute.valve.stopped=JvmRouteBinderValve stopped
+jvmRoute.set.orignalsessionid=Set Orginal Session id at request attriute {0} value: {1}
+standardSession.getId.ise=getId: Session already invalidated
+standardSession.attributeEvent=Session attribute event listener threw exception
+standardSession.attributeEvent=Session attribute event listener threw exception
+standardSession.bindingEvent=Session binding event listener threw exception
+standardSession.invalidate.ise=invalidate: Session already invalidated
+standardSession.isNew.ise=isNew: Session already invalidated
+standardSession.getAttribute.ise=getAttribute: Session already invalidated
+standardSession.getAttributeNames.ise=getAttributeNames: Session already invalidated
+standardSession.getCreationTime.ise=getCreationTime: Session already invalidated
+standardSession.getLastAccessedTime.ise=getLastAccessedTime: Session already invalidated
+standardSession.getId.ise=getId: Session already invalidated
+standardSession.getMaxInactiveInterval.ise=getMaxInactiveInterval: Session already invalidated
+standardSession.getValueNames.ise=getValueNames: Session already invalidated
+standardSession.notSerializable=Cannot serialize session attribute {0} for session {1}
+standardSession.removeAttribute.ise=removeAttribute: Session already invalidated
+standardSession.sessionEvent=Session event listener threw exception
+standardSession.setAttribute.iae=setAttribute: Non-serializable attribute
+standardSession.setAttribute.ise=setAttribute: Session already invalidated
+standardSession.setAttribute.namenull=setAttribute: name parameter cannot be null
+standardSession.sessionCreated=Created Session id = {0}
diff --git a/java/org/apache/catalina/ha/session/ReplicatedSession.java b/java/org/apache/catalina/ha/session/ReplicatedSession.java
new file mode 100644 (file)
index 0000000..7f4017a
--- /dev/null
@@ -0,0 +1,285 @@
+
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.session;
+
+/**
+ * Title:        Tomcat Session Replication for Tomcat 4.0 <BR>
+ * Description:  A very simple straight forward implementation of
+ *               session replication of servers in a cluster.<BR>
+ *               This session replication is implemented "live". By live
+ *               I mean, when a session attribute is added into a session on Node A
+ *               a message is broadcasted to other messages and setAttribute is called on the replicated
+ *               sessions.<BR>
+ *               A full description of this implementation can be found under
+ *               <href="http://www.filip.net/tomcat/">Filip's Tomcat Page</a><BR>
+ *
+ * Copyright:    See apache license
+ * @author  Filip Hanik
+ * @version $Revision: 303842 $ $Date: 2005-04-10 11:20:46 -0500 (Sun, 10 Apr 2005) $
+ * Description:<BR>
+ * The ReplicatedSession class is a simple extension of the StandardSession class
+ * It overrides a few methods (setAttribute, removeAttribute, expire, access) and has
+ * hooks into the InMemoryReplicationManager to broadcast and receive events from the cluster.<BR>
+ * This class inherits the readObjectData and writeObject data methods from the StandardSession
+ * and does not contain any serializable elements in addition to the inherited ones from the StandardSession
+ *
+ */
+import org.apache.catalina.Manager;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.Principal;
+
+public class ReplicatedSession extends org.apache.catalina.session.StandardSession
+implements org.apache.catalina.ha.ClusterSession{
+
+    private transient Manager mManager = null;
+    protected boolean isDirty = false;
+    private transient long lastAccessWasDistributed = System.currentTimeMillis();
+    private boolean isPrimarySession=true;
+    
+
+    public ReplicatedSession(Manager manager) {
+        super(manager);
+        mManager = manager;
+    }
+
+
+    public boolean isDirty()
+    {
+        return isDirty;
+    }
+
+    public void setIsDirty(boolean dirty)
+    {
+        isDirty = dirty;
+    }
+
+
+    public void setLastAccessWasDistributed(long time) {
+        lastAccessWasDistributed = time;
+    }
+
+    public long getLastAccessWasDistributed() {
+        return lastAccessWasDistributed;
+    }
+
+
+    public void removeAttribute(String name) {
+        setIsDirty(true);
+        super.removeAttribute(name);
+    }
+
+    /**
+     * see parent description,
+     * plus we also notify other nodes in the cluster
+     */
+    public void removeAttribute(String name, boolean notify) {
+        setIsDirty(true);
+        super.removeAttribute(name,notify);
+    }
+
+
+    /**
+     * Sets an attribute and notifies the other nodes in the cluster
+     */
+    public void setAttribute(String name, Object value)
+    {
+        if ( value == null ) {
+          removeAttribute(name);
+          return;
+        }
+        if (!(value instanceof java.io.Serializable))
+            throw new java.lang.IllegalArgumentException("Value for attribute "+name+" is not serializable.");
+        setIsDirty(true);
+        super.setAttribute(name,value);
+    }
+
+    public void setMaxInactiveInterval(int interval) {
+        setIsDirty(true);
+        super.setMaxInactiveInterval(interval);
+    }
+
+
+    /**
+     * Sets the manager for this session
+     * @param mgr - the servers InMemoryReplicationManager
+     */
+    public void setManager(SimpleTcpReplicationManager mgr)
+    {
+        mManager = mgr;
+        super.setManager(mgr);
+    }
+
+
+    /**
+     * Set the authenticated Principal that is associated with this Session.
+     * This provides an <code>Authenticator</code> with a means to cache a
+     * previously authenticated Principal, and avoid potentially expensive
+     * <code>Realm.authenticate()</code> calls on every request.
+     *
+     * @param principal The new Principal, or <code>null</code> if none
+     */
+    public void setPrincipal(Principal principal) {
+        super.setPrincipal(principal);
+        setIsDirty(true);
+    }
+
+    public void expire() {
+        SimpleTcpReplicationManager mgr =(SimpleTcpReplicationManager)getManager();
+        mgr.sessionInvalidated(getIdInternal());
+        setIsDirty(true);
+        super.expire();
+    }
+
+    public void invalidate() {
+        SimpleTcpReplicationManager mgr =(SimpleTcpReplicationManager)getManager();
+        mgr.sessionInvalidated(getIdInternal());
+        setIsDirty(true);
+        super.invalidate();
+    }
+
+
+    /**
+     * Read a serialized version of the contents of this session object from
+     * the specified object input stream, without requiring that the
+     * StandardSession itself have been serialized.
+     *
+     * @param stream The object input stream to read from
+     *
+     * @exception ClassNotFoundException if an unknown class is specified
+     * @exception IOException if an input/output error occurs
+     */
+    public void readObjectData(ObjectInputStream stream)
+        throws ClassNotFoundException, IOException {
+
+        super.readObjectData(stream);
+
+    }
+
+
+    /**
+     * Write a serialized version of the contents of this session object to
+     * the specified object output stream, without requiring that the
+     * StandardSession itself have been serialized.
+     *
+     * @param stream The object output stream to write to
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    public void writeObjectData(ObjectOutputStream stream)
+        throws IOException {
+
+        super.writeObjectData(stream);
+
+    }
+    
+    public void setId(String id, boolean tellNew) {
+
+        if ((this.id != null) && (manager != null))
+            manager.remove(this);
+
+        this.id = id;
+
+        if (manager != null)
+            manager.add(this);
+        if (tellNew) tellNew();
+    }
+    
+    
+
+
+
+
+
+
+    /**
+     * returns true if this session is the primary session, if that is the
+     * case, the manager can expire it upon timeout.
+     */
+    public boolean isPrimarySession() {
+        return isPrimarySession;
+    }
+
+    /**
+     * Sets whether this is the primary session or not.
+     * @param primarySession Flag value
+     */
+    public void setPrimarySession(boolean primarySession) {
+        this.isPrimarySession=primarySession;
+    }
+
+
+
+
+    /**
+     * Implements a log method to log through the manager
+     */
+    protected void log(String message) {
+
+        if ((mManager != null) && (mManager instanceof SimpleTcpReplicationManager)) {
+            ((SimpleTcpReplicationManager) mManager).log.debug("ReplicatedSession: " + message);
+        } else {
+            System.out.println("ReplicatedSession: " + message);
+        }
+
+    }
+
+    protected void log(String message, Throwable x) {
+
+        if ((mManager != null) && (mManager instanceof SimpleTcpReplicationManager)) {
+            ((SimpleTcpReplicationManager) mManager).log.error("ReplicatedSession: " + message,x);
+        } else {
+            System.out.println("ReplicatedSession: " + message);
+            x.printStackTrace();
+        }
+
+    }
+
+    public String toString() {
+        StringBuffer buf = new StringBuffer("ReplicatedSession id=");
+        buf.append(getIdInternal()).append(" ref=").append(super.toString()).append("\n");
+        java.util.Enumeration e = getAttributeNames();
+        while ( e.hasMoreElements() ) {
+            String name = (String)e.nextElement();
+            Object value = getAttribute(name);
+            buf.append("\tname=").append(name).append("; value=").append(value).append("\n");
+        }
+        buf.append("\tLastAccess=").append(getLastAccessedTime()).append("\n");
+        return buf.toString();
+    }
+    public int getAccessCount() {
+        return accessCount.get();
+    }
+    public void setAccessCount(int accessCount) {
+        this.accessCount.set(accessCount);
+    }
+    public long getLastAccessedTime() {
+        return lastAccessedTime;
+    }
+    public void setLastAccessedTime(long lastAccessedTime) {
+        this.lastAccessedTime = lastAccessedTime;
+    }
+    public long getThisAccessedTime() {
+        return thisAccessedTime;
+    }
+    public void setThisAccessedTime(long thisAccessedTime) {
+        this.thisAccessedTime = thisAccessedTime;
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/SerializablePrincipal.java b/java/org/apache/catalina/ha/session/SerializablePrincipal.java
new file mode 100644 (file)
index 0000000..08518b6
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha.session;
+
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.catalina.Realm;
+
+
+/**
+ * Generic implementation of <strong>java.security.Principal</strong> that
+ * is available for use by <code>Realm</code> implementations.
+ * The GenericPrincipal does NOT implement serializable and I didn't want to change that implementation
+ * hence I implemented this one instead.
+ * @author Filip Hanik
+ * @version $Revision: 303587 $ $Date: 2004-12-09 08:36:43 -0600 (Thu, 09 Dec 2004) $
+ */
+import org.apache.catalina.realm.GenericPrincipal;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+public class SerializablePrincipal  implements java.io.Serializable {
+
+
+    // ----------------------------------------------------------- Constructors
+
+    public SerializablePrincipal()
+    {
+        super();
+    }
+    /**
+     * Construct a new Principal, associated with the specified Realm, for the
+     * specified username and password.
+     *
+     * @param realm The Realm that owns this Principal
+     * @param name The username of the user represented by this Principal
+     * @param password Credentials used to authenticate this user
+     */
+    public SerializablePrincipal(Realm realm, String name, String password) {
+
+        this(realm, name, password, null);
+
+    }
+
+
+    /**
+     * Construct a new Principal, associated with the specified Realm, for the
+     * specified username and password, with the specified role names
+     * (as Strings).
+     *
+     * @param realm The Realm that owns this principal
+     * @param name The username of the user represented by this Principal
+     * @param password Credentials used to authenticate this user
+     * @param roles List of roles (must be Strings) possessed by this user
+     */
+    public SerializablePrincipal(Realm realm, String name, String password,
+                            List roles) {
+
+        super();
+        this.realm = realm;
+        this.name = name;
+        this.password = password;
+        if (roles != null) {
+            this.roles = new String[roles.size()];
+            this.roles = (String[]) roles.toArray(this.roles);
+            if (this.roles.length > 0)
+                Arrays.sort(this.roles);
+        }
+
+    }
+
+
+    // ------------------------------------------------------------- Properties
+
+
+    /**
+     * The username of the user represented by this Principal.
+     */
+    protected String name = null;
+
+    public String getName() {
+        return (this.name);
+    }
+
+
+    /**
+     * The authentication credentials for the user represented by
+     * this Principal.
+     */
+    protected String password = null;
+
+    public String getPassword() {
+        return (this.password);
+    }
+
+
+    /**
+     * The Realm with which this Principal is associated.
+     */
+    protected transient Realm realm = null;
+
+    public Realm getRealm() {
+        return (this.realm);
+    }
+
+    public void setRealm(Realm realm) {
+        this.realm = realm;
+    }
+
+
+
+
+    /**
+     * The set of roles associated with this user.
+     */
+    protected String roles[] = new String[0];
+
+    public String[] getRoles() {
+        return (this.roles);
+    }
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+
+
+    /**
+     * Return a String representation of this object, which exposes only
+     * information that should be public.
+     */
+    public String toString() {
+
+        StringBuffer sb = new StringBuffer("SerializablePrincipal[");
+        sb.append(this.name);
+        sb.append("]");
+        return (sb.toString());
+
+    }
+
+    public static SerializablePrincipal createPrincipal(GenericPrincipal principal)
+    {
+        if ( principal==null) return null;
+        return new SerializablePrincipal(principal.getRealm(),
+                                         principal.getName(),
+                                         principal.getPassword(),
+                                         principal.getRoles()!=null?Arrays.asList(principal.getRoles()):null);
+    }
+
+    public GenericPrincipal getPrincipal( Realm realm )
+    {
+        return new GenericPrincipal(realm,name,password,getRoles()!=null?Arrays.asList(getRoles()):null);
+    }
+    
+    public static GenericPrincipal readPrincipal(ObjectInput in, Realm realm) throws java.io.IOException{
+        String name = in.readUTF();
+        boolean hasPwd = in.readBoolean();
+        String pwd = null;
+        if ( hasPwd ) pwd = in.readUTF();
+        int size = in.readInt();
+        String[] roles = new String[size];
+        for ( int i=0; i<size; i++ ) roles[i] = in.readUTF();
+        return new GenericPrincipal(realm,name,pwd,Arrays.asList(roles));
+    }
+    
+    public static void writePrincipal(GenericPrincipal p, ObjectOutput out) throws java.io.IOException {
+        out.writeUTF(p.getName());
+        out.writeBoolean(p.getPassword()!=null);
+        if ( p.getPassword()!= null ) out.writeUTF(p.getPassword());
+        String[] roles = p.getRoles();
+        if ( roles == null ) roles = new String[0];
+        out.writeInt(roles.length);
+        for ( int i=0; i<roles.length; i++ ) out.writeUTF(roles[i]);
+    }
+
+
+}
diff --git a/java/org/apache/catalina/ha/session/SessionIDMessage.java b/java/org/apache/catalina/ha/session/SessionIDMessage.java
new file mode 100644 (file)
index 0000000..126aa4d
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterMessageBase;
+
+/**
+ * Session id change cluster message
+ * 
+ * @author Peter Rossbach
+ * 
+ * @version $Revision: 326110 $ $Date: 2005-10-18 09:08:36 -0500 (Tue, 18 Oct 2005) $
+ */
+public class SessionIDMessage extends ClusterMessageBase implements ClusterMessage {
+
+       private int messageNumber;
+
+       private String orignalSessionID;
+
+       private String backupSessionID;
+
+       private String host ;
+       private String contextPath;
+
+       public String getUniqueId() {
+               StringBuffer result = new StringBuffer(getOrignalSessionID());
+               result.append("#-#");
+               result.append(getHost());
+                result.append("#-#");
+                result.append(getContextPath());
+               result.append("#-#");
+               result.append(getMessageNumber());
+               result.append("#-#");
+               result.append(System.currentTimeMillis());
+               return result.toString();
+       }
+
+    /**
+     * @return Returns the host.
+     */
+    public String getHost() {
+        return host;
+    }
+
+    /**
+     * @param host The host to set.
+     */
+    public void setHost(String host) {
+        this.host = host;
+    }
+    
+       /**
+        * @return Returns the contextPath.
+        */
+       public String getContextPath() {
+               return contextPath;
+       }
+       /**
+        * @param contextPath The contextPath to set.
+        */
+       public void setContextPath(String contextPath) {
+               this.contextPath = contextPath;
+       }
+       /**
+        * @return Returns the messageNumber.
+        */
+       public int getMessageNumber() {
+               return messageNumber;
+       }
+
+       /**
+        * @param messageNumber
+        *            The messageNumber to set.
+        */
+       public void setMessageNumber(int messageNumber) {
+               this.messageNumber = messageNumber;
+       }
+
+       
+       /**
+        * @return Returns the backupSessionID.
+        */
+       public String getBackupSessionID() {
+               return backupSessionID;
+       }
+
+       /**
+        * @param backupSessionID
+        *            The backupSessionID to set.
+        */
+       public void setBackupSessionID(String backupSessionID) {
+               this.backupSessionID = backupSessionID;
+       }
+
+       /**
+        * @return Returns the orignalSessionID.
+        */
+       public String getOrignalSessionID() {
+               return orignalSessionID;
+       }
+
+       /**
+        * @param orignalSessionID
+        *            The orignalSessionID to set.
+        */
+       public void setOrignalSessionID(String orignalSessionID) {
+               this.orignalSessionID = orignalSessionID;
+       }
+
+    
+
+
+}
+
diff --git a/java/org/apache/catalina/ha/session/SessionMessage.java b/java/org/apache/catalina/ha/session/SessionMessage.java
new file mode 100644 (file)
index 0000000..13fb89b
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+import org.apache.catalina.ha.ClusterMessage;
+
+/**
+ *
+ * <B>Class Description:</B><BR>
+ * The SessionMessage class is a class that is used when a session has been
+ * created, modified, expired in a Tomcat cluster node.<BR>
+ *
+ * The following events are currently available:
+ * <ul>
+ *   <li><pre>public static final int EVT_SESSION_CREATED</pre><li>
+ *   <li><pre>public static final int EVT_SESSION_ACCESSED</pre><li>
+ *   <li><pre>public static final int EVT_ATTRIBUTE_ADDED</pre><li>
+ *   <li><pre>public static final int EVT_ATTRIBUTE_REMOVED</pre><li>
+ *   <li><pre>public static final int EVT_SESSION_EXPIRED_WONOTIFY</pre><li>
+ *   <li><pre>public static final int EVT_SESSION_EXPIRED_WNOTIFY</pre><li>
+ *   <li><pre>public static final int EVT_GET_ALL_SESSIONS</pre><li>
+ *   <li><pre>public static final int EVT_SET_USER_PRINCIPAL</pre><li>
+ *   <li><pre>public static final int EVT_SET_SESSION_NOTE</pre><li>
+ *   <li><pre>public static final int EVT_REMOVE_SESSION_NOTE</pre><li>
+ * </ul>
+ *
+ */
+
+public interface SessionMessage extends ClusterMessage, java.io.Serializable
+{
+
+    /**
+     * Event type used when a session has been created on a node
+     */
+    public static final int EVT_SESSION_CREATED = 1;
+    /**
+     * Event type used when a session has expired
+     */
+    public static final int EVT_SESSION_EXPIRED = 2;
+
+    /**
+     * Event type used when a session has been accessed (ie, last access time
+     * has been updated. This is used so that the replicated sessions will not expire
+     * on the network
+     */
+    public static final int EVT_SESSION_ACCESSED = 3;
+    /**
+     * Event type used when a server comes online for the first time.
+     * The first thing the newly started server wants to do is to grab the
+     * all the sessions from one of the nodes and keep the same state in there
+     */
+    public static final int EVT_GET_ALL_SESSIONS = 4;
+    /**
+     * Event type used when an attribute has been added to a session,
+     * the attribute will be sent to all the other nodes in the cluster
+     */
+    public static final int EVT_SESSION_DELTA  = 13;
+
+    /**
+     * When a session state is transferred, this is the event.
+     */
+    public static final int EVT_ALL_SESSION_DATA = 12;
+    
+    /**
+     * When a session state is complete transferred, this is the event.
+     */
+    public static final int EVT_ALL_SESSION_TRANSFERCOMPLETE = 14;
+    
+
+    
+    public String getContextName();
+    
+    public String getEventTypeString();
+    
+    /**
+     * returns the event type
+     * @return one of the event types EVT_XXXX
+     */
+    public int getEventType(); 
+    /**
+     * @return the serialized data for the session
+     */
+    public byte[] getSession();
+    /**
+     * @return the session ID for the session
+     */
+    public String getSessionID();
+    
+
+
+}//SessionMessage
diff --git a/java/org/apache/catalina/ha/session/SessionMessageImpl.java b/java/org/apache/catalina/ha/session/SessionMessageImpl.java
new file mode 100644 (file)
index 0000000..f08d623
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.ha.ClusterMessageBase;
+
+/**
+ * Session cluster message
+ * 
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * 
+ * @version $Revision: 326110 $ $Date: 2005-10-18 09:08:36 -0500 (Tue, 18 Oct 2005) $
+ */
+public class SessionMessageImpl extends ClusterMessageBase implements SessionMessage, java.io.Serializable {
+    
+    public SessionMessageImpl() {
+    }
+    
+    
+    /*
+
+     * Private serializable variables to keep the messages state
+     */
+    private int mEvtType = -1;
+    private byte[] mSession;
+    private String mSessionID;
+
+    private String mContextName;
+    private long serializationTimestamp;
+    private boolean timestampSet = false ;
+    private String uniqueId;
+
+
+    private SessionMessageImpl( String contextName,
+                           int eventtype,
+                           byte[] session,
+                           String sessionID)
+    {
+        mEvtType = eventtype;
+        mSession = session;
+        mSessionID = sessionID;
+        mContextName = contextName;
+        uniqueId = sessionID;
+    }
+
+    /**
+     * Creates a session message. Depending on what event type you want this
+     * message to represent, you populate the different parameters in the constructor<BR>
+      * The following rules apply dependent on what event type argument you use:<BR>
+     * <B>EVT_SESSION_CREATED</B><BR>
+     *    The parameters: session, sessionID must be set.<BR>
+     * <B>EVT_SESSION_EXPIRED</B><BR>
+     *    The parameters: sessionID must be set.<BR>
+     * <B>EVT_SESSION_ACCESSED</B><BR>
+     *    The parameters: sessionID must be set.<BR>
+     * <B>EVT_SESSION_EXPIRED_XXXX</B><BR>
+     *    The parameters: sessionID must be set.<BR>
+     * <B>EVT_SESSION_DELTA</B><BR>
+     *    Send attribute delta (add,update,remove attribute or principal, ...).<BR>
+     * <B>EVT_ALL_SESSION_DATA</B><BR>
+     *    Send complete serializes session list<BR>
+     * <B>EVT_ALL_SESSION_TRANSFERCOMPLETE</B><BR>
+     *    send that all session state information are transfered
+     *    after GET_ALL_SESSION received from this sender.<BR>
+     * @param contextName - the name of the context (application
+     * @param eventtype - one of the 8 event type defined in this class
+     * @param session - the serialized byte array of the session itself
+     * @param sessionID - the id that identifies this session
+     * @param uniqueID - the id that identifies this message
+     */
+    public SessionMessageImpl( String contextName,
+                           int eventtype,
+                           byte[] session,
+                           String sessionID,
+                           String uniqueID)
+    {
+        this(contextName,eventtype,session,sessionID);
+        uniqueId = uniqueID;
+    }
+
+    /**
+     * returns the event type
+     * @return one of the event types EVT_XXXX
+     */
+    public int getEventType() { return mEvtType; }
+
+    /**
+     * @return the serialized data for the session
+     */
+    public byte[] getSession() { return mSession;}
+
+    /**
+     * @return the session ID for the session
+     */
+    public String getSessionID(){ return mSessionID; }
+    
+    /**
+     * set message send time but only the first setting works (one shot)
+     */
+    public void setTimestamp(long time) {
+        synchronized(this) {
+            if(!timestampSet) {
+                serializationTimestamp=time;
+                timestampSet = true ;
+            }
+        }
+    }
+    
+    public long getTimestamp() { return serializationTimestamp;}
+    
+    /**
+     * clear text event type name (for logging purpose only) 
+     * @return the event type in a string representating, useful for debugging
+     */
+    public String getEventTypeString()
+    {
+        switch (mEvtType)
+        {
+            case EVT_SESSION_CREATED : return "SESSION-MODIFIED";
+            case EVT_SESSION_EXPIRED : return "SESSION-EXPIRED";
+            case EVT_SESSION_ACCESSED : return "SESSION-ACCESSED";
+            case EVT_GET_ALL_SESSIONS : return "SESSION-GET-ALL";
+            case EVT_SESSION_DELTA : return "SESSION-DELTA";
+            case EVT_ALL_SESSION_DATA : return "ALL-SESSION-DATA";
+            case EVT_ALL_SESSION_TRANSFERCOMPLETE : return "SESSION-STATE-TRANSFERED";
+            default : return "UNKNOWN-EVENT-TYPE";
+        }
+    }
+
+    public String getContextName() {
+       return mContextName;
+    }
+    public String getUniqueId() {
+        return uniqueId;
+    }
+    public void setUniqueId(String uniqueId) {
+        this.uniqueId = uniqueId;
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java b/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java
new file mode 100644 (file)
index 0000000..6dfb769
--- /dev/null
@@ -0,0 +1,690 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.ha.session;
+
+import java.io.IOException;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.session.StandardManager;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import java.io.ByteArrayInputStream;
+import org.apache.catalina.Loader;
+
+/**
+ * Title:        Tomcat Session Replication for Tomcat 4.0 <BR>
+ * Description:  A very simple straight forward implementation of
+ *               session replication of servers in a cluster.<BR>
+ *               This session replication is implemented "live". By live
+ *               I mean, when a session attribute is added into a session on Node A
+ *               a message is broadcasted to other messages and setAttribute is called on the
+ *               replicated sessions.<BR>
+ *               A full description of this implementation can be found under
+ *               <href="http://www.filip.net/tomcat/">Filip's Tomcat Page</a><BR>
+ *
+ * Copyright:    See apache license
+ * Company:      www.filip.net
+ * @author  <a href="mailto:mail@filip.net">Filip Hanik</a>
+ * @author Bela Ban (modifications for synchronous replication)
+ * @version 1.0 for TC 4.0
+ * Description: The InMemoryReplicationManager is a session manager that replicated
+ * session information in memory. It uses <a href="www.javagroups.com">JavaGroups</a> as
+ * a communication protocol to ensure guaranteed and ordered message delivery.
+ * JavaGroups also provides a very flexible protocol stack to ensure that the replication
+ * can be used in any environment.
+ * <BR><BR>
+ * The InMemoryReplicationManager extends the StandardManager hence it allows for us
+ * to inherit all the basic session management features like expiration, session listeners etc
+ * <BR><BR>
+ * To communicate with other nodes in the cluster, the InMemoryReplicationManager sends out 7 different type of multicast messages
+ * all defined in the SessionMessage class.<BR>
+ * When a session is replicated (not an attribute added/removed) the session is serialized into
+ * a byte array using the StandardSession.readObjectData, StandardSession.writeObjectData methods.
+ */
+public class SimpleTcpReplicationManager extends StandardManager implements ClusterManager
+{
+    public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( SimpleTcpReplicationManager.class );
+
+    //the channel configuration
+    protected String mChannelConfig = null;
+
+    //the group name
+    protected String mGroupName = "TomcatReplication";
+
+    //somehow start() gets called more than once
+    protected boolean mChannelStarted = false;
+
+    //log to screen
+    protected boolean mPrintToScreen = true;
+
+    protected boolean defaultMode = false;
+
+    protected boolean mManagerRunning = false;
+
+    /** Use synchronous rather than asynchronous replication. Every session modification (creation, change, removal etc)
+     * will be sent to all members. The call will then wait for max milliseconds, or forever (if timeout is 0) for
+     * all responses.
+     */
+    protected boolean synchronousReplication=true;
+
+    /** Set to true if we don't want the sessions to expire on shutdown */
+    protected boolean mExpireSessionsOnShutdown = true;
+
+    protected boolean useDirtyFlag = false;
+
+    protected String name;
+
+    protected boolean distributable = true;
+
+    protected CatalinaCluster cluster;
+
+    protected java.util.HashMap invalidatedSessions = new java.util.HashMap();
+
+    /**
+     * Flag to keep track if the state has been transferred or not
+     * Assumes false.
+     */
+    protected boolean stateTransferred = false;
+    private boolean notifyListenersOnReplication;
+    private boolean sendClusterDomainOnly = true ;
+
+    /**
+     * Constructor, just calls super()
+     *
+     */
+    public SimpleTcpReplicationManager()
+    {
+        super();
+    }
+
+    public boolean isSendClusterDomainOnly() {
+        return sendClusterDomainOnly;
+    }
+    
+    /**
+     * @param sendClusterDomainOnly The sendClusterDomainOnly to set.
+     */
+    public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) {
+        this.sendClusterDomainOnly = sendClusterDomainOnly;
+    }
+  
+    /**
+     * @return Returns the defaultMode.
+     */
+    public boolean isDefaultMode() {
+        return defaultMode;
+    }
+    /**
+     * @param defaultMode The defaultMode to set.
+     */
+    public void setDefaultMode(boolean defaultMode) {
+        this.defaultMode = defaultMode;
+    }
+    
+    public boolean isManagerRunning()
+    {
+        return mManagerRunning;
+    }
+
+    public void setUseDirtyFlag(boolean usedirtyflag)
+    {
+        this.useDirtyFlag = usedirtyflag;
+    }
+
+    public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown)
+    {
+        mExpireSessionsOnShutdown = expireSessionsOnShutdown;
+    }
+
+    public void setCluster(CatalinaCluster cluster) {
+        if(log.isDebugEnabled())
+            log.debug("Cluster associated with SimpleTcpReplicationManager");
+        this.cluster = cluster;
+    }
+
+    public boolean getExpireSessionsOnShutdown()
+    {
+        return mExpireSessionsOnShutdown;
+    }
+
+    public void setPrintToScreen(boolean printtoscreen)
+    {
+        if(log.isDebugEnabled())
+            log.debug("Setting screen debug to:"+printtoscreen);
+        mPrintToScreen = printtoscreen;
+    }
+
+    public void setSynchronousReplication(boolean flag)
+    {
+        synchronousReplication=flag;
+    }
+
+    /**
+     * Override persistence since they don't go hand in hand with replication for now.
+     */
+    public void unload() throws IOException {
+        if ( !getDistributable() ) {
+            super.unload();
+        }
+    }
+
+    /**
+     * Creates a HTTP session.
+     * Most of the code in here is copied from the StandardManager.
+     * This is not pretty, yeah I know, but it was necessary since the
+     * StandardManager had hard coded the session instantiation to the a
+     * StandardSession, when we actually want to instantiate a ReplicatedSession<BR>
+     * If the call comes from the Tomcat servlet engine, a SessionMessage goes out to the other
+     * nodes in the cluster that this session has been created.
+     * @param notify - if set to true the other nodes in the cluster will be notified.
+     *                 This flag is needed so that we can create a session before we deserialize
+     *                 a replicated one
+     *
+     * @see ReplicatedSession
+     */
+    protected Session createSession(String sessionId, boolean notify, boolean setId)
+    {
+
+        //inherited from the basic manager
+        if ((getMaxActiveSessions() >= 0) &&
+           (sessions.size() >= getMaxActiveSessions()))
+            throw new IllegalStateException(sm.getString("standardManager.createSession.ise"));
+
+
+        Session session = new ReplicatedSession(this);
+
+        // Initialize the properties of the new session and return it
+        session.setNew(true);
+        session.setValid(true);
+        session.setCreationTime(System.currentTimeMillis());
+        session.setMaxInactiveInterval(this.maxInactiveInterval);
+        if(sessionId == null)
+            sessionId = generateSessionId();
+        if ( setId ) session.setId(sessionId);
+        if ( notify && (cluster!=null) ) {
+            ((ReplicatedSession)session).setIsDirty(true);
+        }
+        return (session);
+    }//createSession
+
+    //=========================================================================
+    // OVERRIDE THESE METHODS TO IMPLEMENT THE REPLICATION
+    //=========================================================================
+
+    /**
+     * Construct and return a new session object, based on the default
+     * settings specified by this Manager's properties.  The session
+     * id will be assigned by this method, and available via the getId()
+     * method of the returned session.  If a new session cannot be created
+     * for any reason, return <code>null</code>.
+     *
+     * @exception IllegalStateException if a new session cannot be
+     *  instantiated for any reason
+     */
+    public Session createSession(String sessionId)
+    {
+        //create a session and notify the other nodes in the cluster
+        Session session =  createSession(sessionId,getDistributable(),true);
+        add(session);
+        return session;
+    }
+
+    public void sessionInvalidated(String sessionId) {
+        synchronized ( invalidatedSessions ) {
+            invalidatedSessions.put(sessionId, sessionId);
+        }
+    }
+
+    public String[] getInvalidatedSessions() {
+        synchronized ( invalidatedSessions ) {
+            String[] result = new String[invalidatedSessions.size()];
+            invalidatedSessions.values().toArray(result);
+            return result;
+        }
+
+    }
+
+    public ClusterMessage requestCompleted(String sessionId)
+    {
+        if (  !getDistributable() ) {
+            log.warn("Received requestCompleted message, although this context["+
+                     getName()+"] is not distributable. Ignoring message");
+            return null;
+        }
+        //notify javagroups
+        try
+        {
+            if ( invalidatedSessions.get(sessionId) != null ) {
+                synchronized ( invalidatedSessions ) {
+                    invalidatedSessions.remove(sessionId);
+                    SessionMessage msg = new SessionMessageImpl(name,
+                    SessionMessage.EVT_SESSION_EXPIRED,
+                    null,
+                    sessionId,
+                    sessionId);
+                return msg;
+                }
+            } else {
+                ReplicatedSession session = (ReplicatedSession) findSession(
+                    sessionId);
+                if (session != null) {
+                    //return immediately if the session is not dirty
+                    if (useDirtyFlag && (!session.isDirty())) {
+                        //but before we return doing nothing,
+                        //see if we should send
+                        //an updated last access message so that
+                        //sessions across cluster dont expire
+                        long interval = session.getMaxInactiveInterval();
+                        long lastaccdist = System.currentTimeMillis() -
+                            session.getLastAccessWasDistributed();
+                        if ( ((interval*1000) / lastaccdist)< 3 ) {
+                            SessionMessage accmsg = new SessionMessageImpl(name,
+                                SessionMessage.EVT_SESSION_ACCESSED,
+                                null,
+                                sessionId,
+                                sessionId);
+                            session.setLastAccessWasDistributed(System.currentTimeMillis());
+                            return accmsg;
+                        }
+                        return null;
+                    }
+
+                    session.setIsDirty(false);
+                    if (log.isDebugEnabled()) {
+                        try {
+                            log.debug("Sending session to cluster=" + session);
+                        }
+                        catch (Exception ignore) {}
+                    }
+                    SessionMessage msg = new SessionMessageImpl(name,
+                        SessionMessage.EVT_SESSION_CREATED,
+                        writeSession(session),
+                        session.getIdInternal(),
+                        session.getIdInternal());
+                    return msg;
+                } //end if
+            }//end if
+        }
+        catch (Exception x )
+        {
+            log.error("Unable to replicate session",x);
+        }
+        return null;
+    }
+
+    /**
+     * Serialize a session into a byte array<BR>
+     * This method simple calls the writeObjectData method on the session
+     * and returns the byte data from that call
+     * @param session - the session to be serialized
+     * @return a byte array containing the session data, null if the serialization failed
+     */
+    protected byte[] writeSession( Session session )
+    {
+        try
+        {
+            java.io.ByteArrayOutputStream session_data = new java.io.ByteArrayOutputStream();
+            java.io.ObjectOutputStream session_out = new java.io.ObjectOutputStream(session_data);
+            session_out.flush();
+            boolean hasPrincipal = session.getPrincipal() != null;
+            session_out.writeBoolean(hasPrincipal);
+            if ( hasPrincipal )
+            {
+                session_out.writeObject(SerializablePrincipal.createPrincipal((GenericPrincipal)session.getPrincipal()));
+            }//end if
+            ((ReplicatedSession)session).writeObjectData(session_out);
+            return session_data.toByteArray();
+
+        }
+        catch ( Exception x )
+        {
+            log.error("Failed to serialize the session!",x);
+        }
+        return null;
+    }
+    
+    /**
+     * Open Stream and use correct ClassLoader (Container) Switch
+     * ThreadClassLoader
+     * 
+     * @param data
+     * @return The object input stream
+     * @throws IOException
+     */
+    public ReplicationStream getReplicationStream(byte[] data) throws IOException {
+        return getReplicationStream(data,0,data.length);
+    }
+    
+    public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException {
+        ByteArrayInputStream fis =null;
+        ReplicationStream ois = null;
+        Loader loader = null;
+        ClassLoader classLoader = null;
+        //fix to be able to run the DeltaManager
+        //stand alone without a container.
+        //use the Threads context class loader
+        if (container != null)
+            loader = container.getLoader();
+        if (loader != null)
+            classLoader = loader.getClassLoader();
+        else
+            classLoader = Thread.currentThread().getContextClassLoader();
+        //end fix
+        fis = new ByteArrayInputStream(data, offset, length);
+        if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+            ois = new ReplicationStream(fis, new ClassLoader[] {classLoader});
+        } else {
+            ois = new ReplicationStream(fis, new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()});
+        }
+        return ois;
+    }    
+
+
+    
+
+    /**
+     * Reinstantiates a serialized session from the data passed in.
+     * This will first call createSession() so that we get a fresh instance with all
+     * the managers set and all the transient fields validated.
+     * Then it calls Session.readObjectData(byte[]) to deserialize the object
+     * @param data - a byte array containing session data
+     * @return a valid Session object, null if an error occurs
+     *
+     */
+    protected Session readSession( byte[] data, String sessionId )
+    {
+        try
+        {
+            ReplicationStream session_in = getReplicationStream(data);
+
+            Session session = sessionId!=null?this.findSession(sessionId):null;
+            boolean isNew = (session==null);
+            //clear the old values from the existing session
+            if ( session!=null ) {
+                ReplicatedSession rs = (ReplicatedSession)session;
+                rs.expire(false);  //cleans up the previous values, since we are not doing removes
+                session = null;
+            }//end if
+
+            if (session==null) {
+                session = createSession(null,false, false);
+                sessions.remove(session.getIdInternal());
+            }
+            
+            
+            boolean hasPrincipal = session_in.readBoolean();
+            SerializablePrincipal p = null;
+            if ( hasPrincipal )
+                p = (SerializablePrincipal)session_in.readObject();
+            ((ReplicatedSession)session).readObjectData(session_in);
+            if ( hasPrincipal )
+                session.setPrincipal(p.getPrincipal(getContainer().getRealm()));
+            ((ReplicatedSession)session).setId(sessionId,isNew);
+            ReplicatedSession rsession = (ReplicatedSession)session; 
+            rsession.setAccessCount(1);
+            session.setManager(this);
+            session.setValid(true);
+            rsession.setLastAccessedTime(System.currentTimeMillis());
+            rsession.setThisAccessedTime(System.currentTimeMillis());
+            ((ReplicatedSession)session).setAccessCount(0);
+            session.setNew(false);
+            if(log.isTraceEnabled())
+                 log.trace("Session loaded id="+sessionId +
+                               " actualId="+session.getId()+ 
+                               " exists="+this.sessions.containsKey(sessionId)+
+                               " valid="+rsession.isValid());
+            return session;
+
+        }
+        catch ( Exception x )
+        {
+            log.error("Failed to deserialize the session!",x);
+        }
+        return null;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+    /**
+     * Prepare for the beginning of active use of the public methods of this
+     * component.  This method should be called after <code>configure()</code>,
+     * and before any of the public methods of the component are utilized.<BR>
+     * Starts the cluster communication channel, this will connect with the other nodes
+     * in the cluster, and request the current session state to be transferred to this node.
+     * @exception IllegalStateException if this component has already been
+     *  started
+     * @exception LifecycleException if this component detects a fatal error
+     *  that prevents this component from being used
+     */
+    public void start() throws LifecycleException {
+        mManagerRunning = true;
+        super.start();
+        //start the javagroups channel
+        try {
+            //the channel is already running
+            if ( mChannelStarted ) return;
+            if(log.isInfoEnabled())
+                log.info("Starting clustering manager...:"+getName());
+            if ( cluster == null ) {
+                log.error("Starting... no cluster associated with this context:"+getName());
+                return;
+            }
+            cluster.addManager(getName(),this);
+
+            if (cluster.getMembers().length > 0) {
+                Member mbr = cluster.getMembers()[0];
+                SessionMessage msg =
+                    new SessionMessageImpl(this.getName(),
+                                       SessionMessage.EVT_GET_ALL_SESSIONS,
+                                       null,
+                                       "GET-ALL",
+                                       "GET-ALL-"+this.getName());
+                cluster.send(msg, mbr);
+                if(log.isWarnEnabled())
+                     log.warn("Manager["+getName()+"], requesting session state from "+mbr+
+                         ". This operation will timeout if no session state has been received within "+
+                         "60 seconds");
+                long reqStart = System.currentTimeMillis();
+                long reqNow = 0;
+                boolean isTimeout=false;
+                do {
+                    try {
+                        Thread.sleep(100);
+                    }catch ( Exception sleep) {}
+                    reqNow = System.currentTimeMillis();
+                    isTimeout=((reqNow-reqStart)>(1000*60));
+                } while ( (!isStateTransferred()) && (!isTimeout));
+                if ( isTimeout || (!isStateTransferred()) ) {
+                    log.error("Manager["+getName()+"], No session state received, timing out.");
+                }else {
+                    if(log.isInfoEnabled())
+                        log.info("Manager["+getName()+"], session state received in "+(reqNow-reqStart)+" ms.");
+                }
+            } else {
+                if(log.isInfoEnabled())
+                    log.info("Manager["+getName()+"], skipping state transfer. No members active in cluster group.");
+            }//end if
+            mChannelStarted = true;
+        }  catch ( Exception x ) {
+            log.error("Unable to start SimpleTcpReplicationManager",x);
+        }
+    }
+
+    /**
+     * Gracefully terminate the active use of the public methods of this
+     * component.  This method should be the last one called on a given
+     * instance of this component.<BR>
+     * This will disconnect the cluster communication channel and stop the listener thread.
+     * @exception IllegalStateException if this component has not been started
+     * @exception LifecycleException if this component detects a fatal error
+     *  that needs to be reported
+     */
+    public void stop() throws LifecycleException
+    {
+        mManagerRunning = false;
+        mChannelStarted = false;
+        super.stop();
+        //stop the javagroup channel
+        try
+        {
+            this.sessions.clear();
+            cluster.removeManager(getName(),this);
+//            mReplicationListener.stopListening();
+//            mReplicationTransmitter.stop();
+//            service.stop();
+//            service = null;
+        }
+        catch ( Exception x )
+        {
+            log.error("Unable to stop SimpleTcpReplicationManager",x);
+        }
+    }
+
+    public void setDistributable(boolean dist) {
+        this.distributable = dist;
+    }
+
+    public boolean getDistributable() {
+        return distributable;
+    }
+
+    /**
+     * This method is called by the received thread when a SessionMessage has
+     * been received from one of the other nodes in the cluster.
+     * @param msg - the message received
+     * @param sender - the sender of the message, this is used if we receive a
+     *                 EVT_GET_ALL_SESSION message, so that we only reply to
+     *                 the requesting node
+     */
+    protected void messageReceived( SessionMessage msg, Member sender ) {
+        try  {
+            if(log.isInfoEnabled()) {
+                log.debug("Received SessionMessage of type="+msg.getEventTypeString());
+                log.debug("Received SessionMessage sender="+sender);
+            }
+            switch ( msg.getEventType() ) {
+                case SessionMessage.EVT_GET_ALL_SESSIONS: {
+                    //get a list of all the session from this manager
+                    Object[] sessions = findSessions();
+                    java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream();
+                    java.io.ObjectOutputStream oout = new java.io.ObjectOutputStream(bout);
+                    oout.writeInt(sessions.length);
+                    for (int i=0; i<sessions.length; i++){
+                        ReplicatedSession ses = (ReplicatedSession)sessions[i];
+                        oout.writeUTF(ses.getIdInternal());
+                        byte[] data = writeSession(ses);
+                        oout.writeObject(data);
+                    }//for
+                    //don't send a message if we don't have to
+                    oout.flush();
+                    oout.close();
+                    byte[] data = bout.toByteArray();
+                    SessionMessage newmsg = new SessionMessageImpl(name,
+                        SessionMessage.EVT_ALL_SESSION_DATA,
+                        data, "SESSION-STATE","SESSION-STATE-"+getName());
+                    cluster.send(newmsg, sender);
+                    break;
+                }
+                case SessionMessage.EVT_ALL_SESSION_DATA: {
+                    java.io.ByteArrayInputStream bin =
+                        new java.io.ByteArrayInputStream(msg.getSession());
+                    java.io.ObjectInputStream oin = new java.io.ObjectInputStream(bin);
+                    int size = oin.readInt();
+                    for ( int i=0; i<size; i++) {
+                        String id = oin.readUTF();
+                        byte[] data = (byte[])oin.readObject();
+                        Session session = readSession(data,id);
+                    }//for
+                    stateTransferred=true;
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_CREATED: {
+                    Session session = this.readSession(msg.getSession(),msg.getSessionID());
+                    if ( log.isDebugEnabled() ) {
+                        log.debug("Received replicated session=" + session +
+                            " isValid=" + session.isValid());
+                    }
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_EXPIRED: {
+                    Session session = findSession(msg.getSessionID());
+                    if ( session != null ) {
+                        session.expire();
+                        this.remove(session);
+                    }//end if
+                    break;
+                }
+                case SessionMessage.EVT_SESSION_ACCESSED :{
+                    Session session = findSession(msg.getSessionID());
+                    if ( session != null ) {
+                        session.access();
+                        session.endAccess();
+                    }
+                    break;
+                }
+                default:  {
+                    //we didn't recognize the message type, do nothing
+                    break;
+                }
+            }//switch
+        }
+        catch ( Exception x )
+        {
+            log.error("Unable to receive message through TCP channel",x);
+        }
+    }
+
+    public void messageDataReceived(ClusterMessage cmsg) {
+        try {
+            if ( cmsg instanceof SessionMessage ) {
+                SessionMessage msg = (SessionMessage)cmsg;
+                messageReceived(msg,
+                                msg.getAddress() != null ? (Member) msg.getAddress() : null);
+            }
+        } catch(Throwable ex){
+            log.error("InMemoryReplicationManager.messageDataReceived()", ex);
+        }//catch
+    }
+
+    public boolean isStateTransferred() {
+        return stateTransferred;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+    public boolean isNotifyListenersOnReplication() {
+        return notifyListenersOnReplication;
+    }
+    public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) {
+        this.notifyListenersOnReplication = notifyListenersOnReplication;
+    }
+
+
+    /* 
+     * @see org.apache.catalina.ha.ClusterManager#getCluster()
+     */
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/session/mbeans-descriptors.xml b/java/org/apache/catalina/ha/session/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..f062121
--- /dev/null
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mbeans-descriptors PUBLIC
+   "-//Apache Software Foundation//DTD Model MBeans Configuration File"
+   "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
+<mbeans-descriptors>
+    <mbean name="JvmRouteBinderValve" description="mod_jk jvmRoute jsessionid cookie backup correction" domain="Catalina"
+        group="Valve" type="org.apache.catalina.ha.session.JvmRouteBinderValve">
+        <attribute name="className"
+               description="Fully qualified class name of the managed object"
+               type="java.lang.String"
+               writeable="false"/>        
+        <attribute name="info" 
+                          description="describe version" type="java.lang.String" writeable="false"/>
+        <attribute name="enabled" 
+                          description="enable a jvm Route check" type="boolean"/>
+        <attribute name="numberOfSessions"
+                          description="number of jvmRoute session corrections" type="long" writeable="false"/>
+        <attribute name="sessionIdAttribute" 
+                   description="Name of attribute with sessionid value before turnover a session" 
+                   type="java.lang.String" 
+                   />
+        <operation name="start" description="Stops the Cluster JvmRouteBinderValve" 
+                          impact="ACTION" returnType="void"/>
+        <operation name="stop" description="Stops the Cluster JvmRouteBinderValve" 
+                          impact="ACTION" returnType="void"/>
+    </mbean>
+       <mbean name="JvmRouteSessionIDBinderListener"
+               description="Monitors the jvmRoute activity"
+               domain="Catalina"
+        group="Listener"
+               type="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener">
+        <attribute name="info" 
+                          description="describe version" type="java.lang.String" writeable="false"/>
+        <attribute name="numberOfSessions" 
+                   description="number of jvmRoute session corrections" 
+                   type="long" 
+                   writeable="false"/>
+    </mbean>
+    
+   <mbean        name="DeltaManager"
+          description="Cluster Manager implementation of the Manager interface"
+               domain="Catalina"
+                group="Manager"
+                 type="org.apache.catalina.ha.session.DeltaManager">
+
+    <attribute   name="info" 
+                 description="describe version"
+                        type="java.lang.String"
+                   writeable="false"/>
+                   
+    <attribute   name="algorithm"
+          description="The message digest algorithm to be used when generating
+                       session identifiers"
+                 type="java.lang.String"/>
+                 
+    <attribute   name="randomFile"
+          description="File source of random - /dev/urandom or a pipe"
+                 type="java.lang.String"/>
+
+    <attribute   name="className"
+          description="Fully qualified class name of the managed object"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="distributable"
+          description="The distributable flag for Sessions created by this
+                       Manager"
+                 type="boolean"/>
+
+    <attribute   name="entropy"
+          description="A String initialization parameter used to increase the
+                       entropy of the initialization of our random number
+                       generator"
+                 type="java.lang.String"/>
+
+    <attribute   name="maxActiveSessions"
+          description="The maximum number of active Sessions allowed, or -1
+                       for no limit"
+                 type="int"/>
+
+    <attribute   name="maxInactiveInterval"
+          description="The default maximum inactive interval for Sessions
+                       created by this Manager"
+                 type="int"/>
+
+    <attribute name="processExpiresFrequency"
+               description="The frequency of the manager checks (expiration and passivation)"
+               type="int"/>
+               
+    <attribute   name="sessionIdLength"
+          description="The session id length (in bytes) of Sessions
+                       created by this Manager"
+                 type="int"/>
+
+    <attribute   name="name"
+          description="The descriptive name of this Manager implementation
+                       (for logging)"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="activeSessions"
+          description="Number of active sessions at this moment"
+                 type="int" 
+            writeable="false"/>
+
+    <attribute   name="sessionCounter"
+          description="Total number of sessions created by this manager"
+                 type="int" />
+
+    <attribute   name="sessionReplaceCounter"
+          description="Total number of replaced sessions that load from external nodes"
+                 type="long" />
+
+    <attribute   name="maxActive"
+          description="Maximum number of active sessions so far"
+                 type="int" />
+
+    <attribute   name="sessionMaxAliveTime"
+          description="Longest time an expired session had been alive"
+                 type="int" />
+
+    <attribute   name="sessionAverageAliveTime"
+          description="Average time an expired session had been alive"
+                 type="int" />
+
+    <attribute   name="sendClusterDomainOnly"
+                   is="true"
+          description="The sendClusterDomainOnly flag send sessions only to members as same cluster domain"
+                 type="boolean"/>
+
+    <attribute   name="rejectedSessions"
+          description="Number of sessions we rejected due to maxActive beeing reached"
+                 type="int" />
+
+    <attribute   name="expiredSessions"
+          description="Number of sessions that expired ( doesn't include explicit invalidations )"
+                 type="int" />
+
+    <attribute   name="stateTransferTimeout"
+          description="state transfer timeout in sec"
+                 type="int"/>
+
+    <attribute   name="processingTime"
+          description="Time spent doing housekeeping and expiration"
+                 type="long" />
+
+    <attribute   name="duplicates"
+          description="Number of duplicated session ids generated"
+                 type="int" />
+
+    <attribute   name="counterReceive_EVT_GET_ALL_SESSIONS"
+          description="Count receive EVT_GET_ALL_SESSIONS messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_ALL_SESSION_DATA"
+          description="Count receive EVT_ALL_SESSION_DATA messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_SESSION_CREATED"
+          description="Count receive EVT_SESSION_CREATED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_SESSION_DELTA"
+          description="Count receive EVT_SESSION_DELTA messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_SESSION_ACCESSED"
+          description="Count receive EVT_SESSION_ACCESSED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_SESSION_EXPIRED"
+          description="Count receive EVT_SESSION_EXPIRED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE"
+          description="Count receive EVT_ALL_SESSION_TRANSFERCOMPLETE messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_GET_ALL_SESSIONS"
+          description="Count send EVT_GET_ALL_SESSIONS messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_ALL_SESSION_DATA"
+          description="Count send EVT_ALL_SESSION_DATA messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_SESSION_CREATED"
+          description="Count send EVT_SESSION_CREATED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_SESSION_DELTA"
+          description="Count send EVT_SESSION_DELTA messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_SESSION_ACCESSED"
+          description="Count send EVT_SESSION_ACCESSED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_SESSION_EXPIRED"
+          description="Count send EVT_SESSION_EXPIRED messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE"
+          description="Count send EVT_ALL_SESSION_TRANSFERCOMPLETE messages"
+                 type="long"
+            writeable="false" />
+
+    <attribute   name="counterNoStateTransfered"
+          description="Count the failed session transfers noStateTransfered"
+                 type="int"
+            writeable="false" />
+            
+    <attribute   name="receivedQueueSize"
+          description="length of receive queue size when session received from other node"
+                 type="int"
+            writeable="false" />            
+
+    <attribute   name="expireSessionsOnShutdown"
+                   is="true"
+          description="exipre all sessions cluster wide as one node goes down"
+                 type="boolean" />
+
+    <attribute   name="notifyListenersOnReplication"
+                   is="true"
+          description="Send session attribute change events on backup nodes"
+                 type="boolean" />
+
+    <attribute   name="notifySessionListenersOnReplication"
+                   is="true"
+          description="Send session start/stop events on backup nodes"
+                 type="boolean" />
+
+    <attribute   name="sendAllSessions"
+                   is="true"
+          description="Send all sessions at one big block"
+                 type="boolean" />
+
+    <attribute   name="sendAllSessionsSize"
+          description="session block size when sendAllSessions=false (default=1000)"
+                 type="int" />
+
+    <attribute   name="sendAllSessionsWaitTime"
+          description="wait time between send session block (default 2 sec)"
+                 type="int" />
+
+    <operation   name="listSessionIds"
+          description="Return the list of active session ids"
+               impact="ACTION"
+           returnType="java.lang.String">
+    </operation>
+
+    <operation   name="getSessionAttribute"
+          description="Return a session attribute"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="sessionId"
+          description="Id of the session"
+                 type="java.lang.String"/>
+      <parameter name="key"
+          description="key of the attribute"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="expireSession"
+          description="Expire a session"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="sessionId"
+          description="Id of the session"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="getLastAccessedTime"
+          description="Get the last access time"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="sessionId"
+          description="Id of the session"
+                 type="java.lang.String"/>
+    </operation>
+    
+       <operation name="expireAllLocalSessions"
+               description="Exipre all active local sessions and replicate the invalid sessions"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+
+       <operation name="processExpires"
+               description="force process to expire sessions"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="getAllClusterSessions"
+               description="send to oldest cluster member that this node need all cluster sessions (resync member)"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+
+  </mbean>
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/ha/tcp/Constants.java b/java/org/apache/catalina/ha/tcp/Constants.java
new file mode 100644 (file)
index 0000000..7ed1dd5
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.ha.tcp;
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.ha.tcp</code>
+ * package.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 303753 $ $Date: 2005-03-14 15:24:30 -0600 (Mon, 14 Mar 2005) $
+ */
+
+public class Constants {
+
+    public static final String Package = "org.apache.catalina.ha.tcp";
+
+}
diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings.properties b/java/org/apache/catalina/ha/tcp/LocalStrings.properties
new file mode 100644 (file)
index 0000000..91b2d37
--- /dev/null
@@ -0,0 +1,72 @@
+AsyncSocketSender.create.thread=Create sender [{0}:{1,number,integer}] queue thread to tcp background replication
+AsyncSocketSender.queue.message=Queue message to [{0}:{1,number,integer}] id=[{2}] size={3}
+AsyncSocketSender.send.error=Unable to asynchronously send session with id=[{0}] - message will be ignored.
+AsyncSocketSender.queue.empty=Queue in sender [{0}:{1,number,integer}] returned null element!
+cluster.mbean.register.already=MBean {0} already registered!
+FastAsyncSocketSender.setThreadPriority=[{0}:{1,number,integer}] set priority to {2}
+FastAsyncSocketSender.min.exception=[{0}:{1,number,integer}] new priority {2} < MIN_PRIORITY
+FastAsyncSocketSender.max.exception=[{0}:{1,number,integer}] new priority {2} > MAX_PRIORITY
+IDataSender.ack.eof=EOF reached at local port [{0}:{1,number,integer}]
+IDataSender.ack.receive=Got ACK at local port [{0}:{1,number,integer}]
+IDataSender.ack.missing=Unable to read acknowledgement from [{0}:{1,number,integer}] in {2,number,integer} ms. Disconnecting socket, and trying again.
+IDataSender.ack.read=Read wait ack char '{2}' [{0}:{1,number,integer}]
+IDataSender.ack.start=Waiting for ACK message [{0}:{1,number,integer}]
+IDataSender.ack.wrong=Missing correct ACK after 10 bytes read at local port [{0}:{1,number,integer}]
+IDataSender.closeSocket=Sender close socket to [{0}:{1,number,integer}] (close count {2,number,integer})
+IDataSender.connect=Sender connect to [{0}:{1,number,integer}] (connect count {2,number,integer})
+IDataSender.create=Create sender [{0}:{1,number,integer}]
+IDataSender.disconnect=Sender disconnect from [{0}:{1,number,integer}] (disconnect count {2,number,integer})
+IDataSender.message.disconnect=Message transfered: Sender can't disconnect from [{0}:{1,number,integer}]
+IDataSender.message.create=Message transfered: Sender can't create current socket [{0}:{1,number,integer}]
+IDataSender.openSocket=Sender open socket to [{0}:{1,number,integer}] (open count {2,number,integer})
+IDataSender.openSocket.failure=Open sender socket [{0}:{1,number,integer}] failure! (open failure count {2,number,integer})
+IDataSender.send.again=Send data again to [{0}:{1,number,integer}]
+IDataSender.send.crash=Send message crashed [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.send.message=Send message to [{0}:{1,number,integer}] id=[{2}] size={3,number,integer}
+IDataSender.send.lost=Message lost: [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.senderModes.Configured=Configured a data replication sender for mode {0}
+IDataSender.senderModes.Instantiate=Can't instantiate a data replication sender of class {0}
+IDataSender.senderModes.Missing=Can't configure a data replication sender for mode {0}
+IDataSender.senderModes.Resources=Can't load data replication sender mapping list
+IDataSender.stats=Send stats from [{0}:{1,number,integer}], Nr of bytes sent={2,number,integer} over {3} = {4,number,integer} bytes/request, processing time {5,number,integer} msec, avg processing time {6,number,integer} msec
+PoolSocketSender.senderQueue.sender.failed=PoolSocketSender create new sender to [{0}:{1,number,integer}] failed
+PoolSocketSender.noMoreSender=No socket sender available for client [{0}:{1,number,integer}] did it disappeared?
+ReplicationTransmitter.getProperty=get property {0}
+ReplicationTransmitter.setProperty=set property {0}: {1} old value {2}
+ReplicationTransmitter.started=Start ClusterSender at cluster {0} with name {1}
+ReplicationTransmitter.stopped=Stopped ClusterSender at cluster {0} with name {1}
+ReplicationValve.crossContext.add=add Cross Context session replication container to replicationValve threadlocal
+ReplicationValve.crossContext.registerSession=register Cross context session id={0} from context {1}
+ReplicationValve.crossContext.remove=remove Cross Context session replication container from replicationValve threadlocal
+ReplicationValve.crossContext.sendDelta=send Cross Context session delta from context {0}.
+ReplicationValve.filter.loading=Loading request filters={0}
+ReplicationValve.filter.token=Request filter={0}
+ReplicationValve.filter.token.failure=Unable to compile filter={0}
+ReplicationValve.invoke.uri=Invoking replication request on {0}
+ReplicationValve.nocluster=No cluster configured for this request.
+ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context {0}
+ReplicationValve.send.failure=Unable to perform replication request.
+ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster.
+ReplicationValve.session.found=Context {0}: Found session {1} but it isn't a ClusterSession.
+ReplicationValve.session.indicator=Context {0}: Primarity of session {0} in request attribute {1} is {2}.
+ReplicationValve.session.invalid=Context {0}: Requested session {1} is invalid, removed or not replicated at this node.
+ReplicationValve.stats=Average request time= {0} ms for Cluster overhead time={1} ms for {2} requests {3} filter requests {4} send requests {5} cross context requests (Request={6} ms Cluster={7} ms).
+SimpleTcpCluster.event.log=Cluster receive listener event {0} with data {1}
+SimpleTcpCluster.getProperty=get property {0}
+SimpleTcpCluster.setProperty=set property {0}: {1} old value {2}
+SimpleTcpCluster.default.addClusterListener=Add Default ClusterListener at cluster {0}
+SimpleTcpCluster.default.addClusterValves=Add Default ClusterValves at cluster {0}
+SimpleTcpCluster.default.addClusterReceiver=Add Default ClusterReceiver at cluster {0}
+SimpleTcpCluster.default.addClusterSender=Add Default ClusterSender at cluster {0}
+SimpleTcpCluster.default.addMembershipService=Add Default Membership Service at cluster {0}
+SimpleTcpCluster.log.receive=RECEIVE {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} {5}
+SimpleTcpCluster.log.send=SEND {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} 
+SimpleTcpCluster.log.send.all=SEND {0,date}:{0,time} {1,number} - {2}
+SocketReplictionListener.allreadyExists=ServerSocket [{0}:{1}] allready started!
+SocketReplictionListener.accept.failure=ServerSocket [{0}:{1}] - Exception to start thread or accept server socket
+SocketReplictionListener.open=Open Socket at [{0}:{1}]
+SocketReplictionListener.openclose.failure=ServerSocket [{0}:{1}] - Exception to open or close server socket
+SocketReplictionListener.portbusy=Port busy at [{0}:{i}] - reason [{2}]
+SocketReplictionListener.serverSocket.notExists=Fatal error: Receiver socket not bound address={0} port={1} maxport={2}
+SocketReplictionListener.timeout=Receiver ServerSocket no started [{0}:{1}] - reason: timeout={2} or listen={3}
+SocketReplictionListener.unlockSocket.failure=UnLocksocket failure at ServerSocket [{0:{1}]
diff --git a/java/org/apache/catalina/ha/tcp/ReplicationValve.java b/java/org/apache/catalina/ha/tcp/ReplicationValve.java
new file mode 100644 (file)
index 0000000..198204d
--- /dev/null
@@ -0,0 +1,658 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.tcp;
+
+import java.io.IOException;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Iterator;
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Manager;
+import org.apache.catalina.Session;
+import org.apache.catalina.Context;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterSession;
+import org.apache.catalina.ha.ClusterValve;
+import org.apache.catalina.ha.session.DeltaManager;
+import org.apache.catalina.ha.session.DeltaSession;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.util.StringManager;
+import org.apache.catalina.valves.ValveBase;
+
+/**
+ * <p>Implementation of a Valve that logs interesting contents from the
+ * specified Request (before processing) and the corresponding Response
+ * (after processing).  It is especially useful in debugging problems
+ * related to headers and cookies.</p>
+ *
+ * <p>This Valve may be attached to any Container, depending on the granularity
+ * of the logging you wish to perform.</p>
+ *
+ * <p>primaryIndicator=true, then the request attribute <i>org.apache.catalina.ha.tcp.isPrimarySession.</i>
+ * is set true, when request processing is at sessions primary node.
+ * </p>
+ *
+ * @author Craig R. McClanahan
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 375709 $ $Date: 2006-02-07 15:13:25 -0600 (Tue, 07 Feb 2006) $
+ */
+
+public class ReplicationValve
+    extends ValveBase implements ClusterValve {
+    
+    private static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog( ReplicationValve.class );
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * The descriptive information related to this implementation.
+     */
+    private static final String info =
+        "org.apache.catalina.ha.tcp.ReplicationValve/2.0";
+
+
+    /**
+     * The StringManager for this package.
+     */
+    protected static StringManager sm =
+        StringManager.getManager(Constants.Package);
+
+    private CatalinaCluster cluster = null ;
+
+    /**
+     * holds file endings to not call for like images and others
+     */
+    protected java.util.regex.Pattern[] reqFilters = new java.util.regex.Pattern[0];
+    
+    /**
+     * Orginal filter 
+     */
+    protected String filter ;
+    
+    /**
+     * crossContext session container 
+     */
+    protected ThreadLocal crossContextSessions = new ThreadLocal() ;
+    
+    /**
+     * doProcessingStats (default = off)
+     */
+    protected boolean doProcessingStats = false;
+    
+    protected long totalRequestTime = 0;
+    protected long totalSendTime = 0;
+    protected long nrOfRequests = 0;
+    protected long lastSendTime = 0;
+    protected long nrOfFilterRequests = 0;
+    protected long nrOfSendRequests = 0;
+    protected long nrOfCrossContextSendRequests = 0;
+    
+    /**
+     * must primary change indicator set 
+     */
+    protected boolean primaryIndicator = false ;
+    
+    /**
+     * Name of primary change indicator as request attribute
+     */
+    protected String primaryIndicatorName = "org.apache.catalina.ha.tcp.isPrimarySession";
+   
+    // ------------------------------------------------------------- Properties
+
+    public ReplicationValve() {
+    }
+    
+    /**
+     * Return descriptive information about this Valve implementation.
+     */
+    public String getInfo() {
+
+        return (info);
+
+    }
+    
+    /**
+     * @return Returns the cluster.
+     */
+    public CatalinaCluster getCluster() {
+        return cluster;
+    }
+    
+    /**
+     * @param cluster The cluster to set.
+     */
+    public void setCluster(CatalinaCluster cluster) {
+        this.cluster = cluster;
+    }
+    /**
+     * @return Returns the filter
+     */
+    public String getFilter() {
+       return filter ;
+    }
+
+    /**
+     * compile filter string to regular expressions
+     * @see Pattern#compile(java.lang.String)
+     * @param filter
+     *            The filter to set.
+     */
+    public void setFilter(String filter) {
+        if (log.isDebugEnabled())
+            log.debug(sm.getString("ReplicationValve.filter.loading", filter));
+        this.filter = filter;
+        StringTokenizer t = new StringTokenizer(filter, ";");
+        this.reqFilters = new Pattern[t.countTokens()];
+        int i = 0;
+        while (t.hasMoreTokens()) {
+            String s = t.nextToken();
+            if (log.isTraceEnabled())
+                log.trace(sm.getString("ReplicationValve.filter.token", s));
+            try {
+                reqFilters[i++] = Pattern.compile(s);
+            } catch (Exception x) {
+                log.error(sm.getString("ReplicationValve.filter.token.failure",
+                        s), x);
+            }
+        }
+    }
+
+    /**
+     * @return Returns the primaryIndicator.
+     */
+    public boolean isPrimaryIndicator() {
+        return primaryIndicator;
+    }
+
+    /**
+     * @param primaryIndicator The primaryIndicator to set.
+     */
+    public void setPrimaryIndicator(boolean primaryIndicator) {
+        this.primaryIndicator = primaryIndicator;
+    }
+    
+    /**
+     * @return Returns the primaryIndicatorName.
+     */
+    public String getPrimaryIndicatorName() {
+        return primaryIndicatorName;
+    }
+    
+    /**
+     * @param primaryIndicatorName The primaryIndicatorName to set.
+     */
+    public void setPrimaryIndicatorName(String primaryIndicatorName) {
+        this.primaryIndicatorName = primaryIndicatorName;
+    }
+    
+    /**
+     * Calc processing stats
+     */
+    public boolean isDoProcessingStats() {
+        return doProcessingStats;
+    }
+
+    /**
+     * Set Calc processing stats
+     * @see #resetStatistics()
+     */
+    public void setDoProcessingStats(boolean doProcessingStats) {
+        this.doProcessingStats = doProcessingStats;
+    }
+
+    /**
+     * @return Returns the lastSendTime.
+     */
+    public long getLastSendTime() {
+        return lastSendTime;
+    }
+    
+    /**
+     * @return Returns the nrOfRequests.
+     */
+    public long getNrOfRequests() {
+        return nrOfRequests;
+    }
+    
+    /**
+     * @return Returns the nrOfFilterRequests.
+     */
+    public long getNrOfFilterRequests() {
+        return nrOfFilterRequests;
+    }
+
+    /**
+     * @return Returns the nrOfCrossContextSendRequests.
+     */
+    public long getNrOfCrossContextSendRequests() {
+        return nrOfCrossContextSendRequests;
+    }
+
+    /**
+     * @return Returns the nrOfSendRequests.
+     */
+    public long getNrOfSendRequests() {
+        return nrOfSendRequests;
+    }
+
+    /**
+     * @return Returns the totalRequestTime.
+     */
+    public long getTotalRequestTime() {
+        return totalRequestTime;
+    }
+    
+    /**
+     * @return Returns the totalSendTime.
+     */
+    public long getTotalSendTime() {
+        return totalSendTime;
+    }
+
+    /**
+     * @return Returns the reqFilters.
+     */
+    protected java.util.regex.Pattern[] getReqFilters() {
+        return reqFilters;
+    }
+    
+    /**
+     * @param reqFilters The reqFilters to set.
+     */
+    protected void setReqFilters(java.util.regex.Pattern[] reqFilters) {
+        this.reqFilters = reqFilters;
+    }
+    
+    
+    // --------------------------------------------------------- Public Methods
+    
+    /**
+     * Register all cross context sessions inside endAccess.
+     * Use a list with contains check, that the Portlet API can include a lot of fragments from same or
+     * different applications with session changes.
+     *
+     * @param session cross context session
+     */
+    public void registerReplicationSession(DeltaSession session) {
+        List sessions = (List)crossContextSessions.get();
+        if(sessions != null) {
+            if(!sessions.contains(session)) {
+                if(log.isDebugEnabled())
+                    log.debug(sm.getString("ReplicationValve.crossContext.registerSession",
+                        session.getIdInternal(),
+                        session.getManager().getContainer().getName()));
+                sessions.add(session);
+            }
+        }
+    }
+
+    /**
+     * Log the interesting request parameters, invoke the next Valve in the
+     * sequence, and log the interesting response parameters.
+     *
+     * @param request The servlet request to be processed
+     * @param response The servlet response to be created
+     *
+     * @exception IOException if an input/output error occurs
+     * @exception ServletException if a servlet error occurs
+     */
+    public void invoke(Request request, Response response)
+        throws IOException, ServletException
+    {
+        long totalstart = 0;
+
+        //this happens before the request
+        if(isDoProcessingStats()) {
+            totalstart = System.currentTimeMillis();
+        }
+        if (primaryIndicator) {
+            createPrimaryIndicator(request) ;
+        }
+        Context context = request.getContext();
+        boolean isCrossContext = context != null
+                && context instanceof StandardContext
+                && ((StandardContext) context).getCrossContext();
+        try {
+            if(isCrossContext) {
+                if(log.isDebugEnabled())
+                    log.debug(sm.getString("ReplicationValve.crossContext.add"));
+                //FIXME add Pool of Arraylists
+                crossContextSessions.set(new ArrayList());
+            }
+            getNext().invoke(request, response);
+            Manager manager = request.getContext().getManager();
+            if (manager != null && manager instanceof ClusterManager) {
+                ClusterManager clusterManager = (ClusterManager) manager;
+                CatalinaCluster containerCluster = (CatalinaCluster) getContainer().getCluster();
+                if (containerCluster == null) {
+                    if (log.isWarnEnabled())
+                        log.warn(sm.getString("ReplicationValve.nocluster"));
+                    return;
+                }
+                // valve cluster can access manager - other cluster handle replication 
+                // at host level - hopefully!
+                if(containerCluster.getManager(clusterManager.getName()) == null)
+                    return ;
+                if(containerCluster.hasMembers()) {
+                    sendReplicationMessage(request, totalstart, isCrossContext, clusterManager, containerCluster);
+                } else {
+                    resetReplicationRequest(request,isCrossContext);
+                }        
+            }
+        } finally {
+            // Array must be remove: Current master request send endAccess at recycle. 
+            // Don't register this request session again!
+            if(isCrossContext) {
+                if(log.isDebugEnabled())
+                    log.debug(sm.getString("ReplicationValve.crossContext.remove"));
+                // crossContextSessions.remove() only exist at Java 5
+                // register ArrayList at a pool
+                crossContextSessions.set(null);
+            }
+        }
+    }
+
+    
+    /**
+     * reset the active statitics 
+     */
+    public void resetStatistics() {
+        totalRequestTime = 0 ;
+        totalSendTime = 0 ;
+        lastSendTime = 0 ;
+        nrOfFilterRequests = 0 ;
+        nrOfRequests = 0 ;
+        nrOfSendRequests = 0;
+        nrOfCrossContextSendRequests = 0;
+    }
+    
+    /**
+     * Return a String rendering of this object.
+     */
+    public String toString() {
+
+        StringBuffer sb = new StringBuffer("ReplicationValve[");
+        if (container != null)
+            sb.append(container.getName());
+        sb.append("]");
+        return (sb.toString());
+
+    }
+
+    // --------------------------------------------------------- Protected Methods
+
+    /**
+     * @param request
+     * @param totalstart
+     * @param isCrossContext
+     * @param clusterManager
+     * @param containerCluster
+     */
+    protected void sendReplicationMessage(Request request, long totalstart, boolean isCrossContext, ClusterManager clusterManager, CatalinaCluster containerCluster) {
+        //this happens after the request
+        long start = 0;
+        if(isDoProcessingStats()) {
+            start = System.currentTimeMillis();
+        }
+        try {
+            // send invalid sessions
+            // DeltaManager returns String[0]
+            if (!(clusterManager instanceof DeltaManager))
+                sendInvalidSessions(clusterManager, containerCluster);
+            // send replication
+            sendSessionReplicationMessage(request, clusterManager, containerCluster);
+            if(isCrossContext)
+                sendCrossContextSession(containerCluster);
+        } catch (Exception x) {
+            // FIXME we have a lot of sends, but the trouble with one node stops the correct replication to other nodes!
+            log.error(sm.getString("ReplicationValve.send.failure"), x);
+        } finally {
+            // FIXME this stats update are not cheap!!
+            if(isDoProcessingStats()) {
+                updateStats(totalstart,start);
+            }
+        }
+    }
+
+    /**
+     * Send all changed cross context sessions to backups
+     * @param containerCluster
+     */
+    protected void sendCrossContextSession(CatalinaCluster containerCluster) {
+        Object sessions = crossContextSessions.get();
+        if(sessions != null && sessions instanceof List
+                && ((List)sessions).size() >0) {
+            for(Iterator iter = ((List)sessions).iterator(); iter.hasNext() ;) {          
+                Session session = (Session)iter.next();
+                if(log.isDebugEnabled())
+                    log.debug(sm.getString("ReplicationValve.crossContext.sendDelta",  
+                            session.getManager().getContainer().getName() ));
+                sendMessage(session,(ClusterManager)session.getManager(),containerCluster);
+                if(isDoProcessingStats()) {
+                    nrOfCrossContextSendRequests++;
+                }
+            }
+        }
+    }
+  
+    /**
+     * Fix memory leak for long sessions with many changes, when no backup member exists!
+     * @param request current request after responce is generated
+     * @param isCrossContext check crosscontext threadlocal
+     */
+    protected void resetReplicationRequest(Request request, boolean isCrossContext) {
+        Session contextSession = request.getSessionInternal(false);
+        if(contextSession != null & contextSession instanceof DeltaSession){
+            resetDeltaRequest(contextSession);
+            ((DeltaSession)contextSession).setPrimarySession(true);
+        }
+        if(isCrossContext) {
+            Object sessions = crossContextSessions.get();
+            if(sessions != null && sessions instanceof List
+               && ((List)sessions).size() >0) {
+                Iterator iter = ((List)sessions).iterator();
+                for(; iter.hasNext() ;) {          
+                    Session session = (Session)iter.next();
+                    resetDeltaRequest(session);
+                    if(session instanceof DeltaSession)
+                        ((DeltaSession)contextSession).setPrimarySession(true);
+
+                }
+            }
+        }                     
+    }
+
+    /**
+     * Reset DeltaRequest from session
+     * @param session HttpSession from current request or cross context session
+     */
+    protected void resetDeltaRequest(Session session) {
+        if(log.isDebugEnabled()) {
+            log.debug(sm.getString("ReplicationValve.resetDeltaRequest" , 
+                session.getManager().getContainer().getName() ));
+        }
+        ((DeltaSession)session).resetDeltaRequest();
+    }
+
+    /**
+     * Send Cluster Replication Request
+     * @param request current request
+     * @param manager session manager
+     * @param cluster replication cluster
+     */
+    protected void sendSessionReplicationMessage(Request request,
+            ClusterManager manager, CatalinaCluster cluster) {
+        Session session = request.getSessionInternal(false);
+        if (session != null) {
+            String uri = request.getDecodedRequestURI();
+            // request without session change
+            if (!isRequestWithoutSessionChange(uri)) {
+                if (log.isDebugEnabled())
+                    log.debug(sm.getString("ReplicationValve.invoke.uri", uri));
+                sendMessage(session,manager,cluster);
+            } else
+                if(isDoProcessingStats())
+                    nrOfFilterRequests++;
+        }
+
+    }
+
+   /**
+    * Send message delta message from request session 
+    * @param request current request
+    * @param manager session manager
+    * @param cluster replication cluster
+    */
+    protected void sendMessage(Session session,
+             ClusterManager manager, CatalinaCluster cluster) {
+        String id = session.getIdInternal();
+        if (id != null) {
+            send(manager, cluster, id);
+        }
+    }
+
+    /**
+     * send manager requestCompleted message to cluster
+     * @param manager SessionManager
+     * @param cluster replication cluster
+     * @param sessionId sessionid from the manager
+     * @see DeltaManager#requestCompleted(String)
+     * @see SimpleTcpCluster#send(ClusterMessage)
+     */
+    protected void send(ClusterManager manager, CatalinaCluster cluster, String sessionId) {
+        ClusterMessage msg = manager.requestCompleted(sessionId);
+        if (msg != null) {
+            if(manager.isSendClusterDomainOnly()) {
+                cluster.sendClusterDomain(msg);
+            } else {
+                cluster.send(msg);
+            }
+            if(isDoProcessingStats())
+                nrOfSendRequests++;
+        }
+    }
+    
+    /**
+     * check for session invalidations
+     * @param manager
+     * @param cluster
+     */
+    protected void sendInvalidSessions(ClusterManager manager, CatalinaCluster cluster) {
+        String[] invalidIds=manager.getInvalidatedSessions();
+        if ( invalidIds.length > 0 ) {
+            for ( int i=0;i<invalidIds.length; i++ ) {
+                try {
+                    send(manager,cluster,invalidIds[i]);
+                } catch ( Exception x ) {
+                    log.error(sm.getString("ReplicationValve.send.invalid.failure",invalidIds[i]),x);
+                }
+            }
+        }
+    }
+    
+    /**
+     * is request without possible session change
+     * @param uri The request uri
+     * @return True if no session change
+     */
+    protected boolean isRequestWithoutSessionChange(String uri) {
+
+        boolean filterfound = false;
+
+        for (int i = 0; (i < reqFilters.length) && (!filterfound); i++) {
+            java.util.regex.Matcher matcher = reqFilters[i].matcher(uri);
+            filterfound = matcher.matches();
+        }
+        return filterfound;
+    }
+
+    /**
+     * protocol cluster replications stats
+     * @param requestTime
+     * @param clusterTime
+     */
+    protected  void updateStats(long requestTime, long clusterTime) {
+        synchronized(this) {
+            lastSendTime=System.currentTimeMillis();
+            totalSendTime+=lastSendTime - clusterTime;
+            totalRequestTime+=lastSendTime - requestTime;
+            nrOfRequests++;
+        }
+        if(log.isInfoEnabled()) {
+            if ( (nrOfRequests % 100) == 0 ) {
+                 log.info(sm.getString("ReplicationValve.stats",
+                     new Object[]{
+                         new Long(totalRequestTime/nrOfRequests),
+                         new Long(totalSendTime/nrOfRequests),
+                         new Long(nrOfRequests),
+                         new Long(nrOfSendRequests),
+                         new Long(nrOfCrossContextSendRequests),
+                         new Long(nrOfFilterRequests),
+                         new Long(totalRequestTime),
+                         new Long(totalSendTime)}));
+             }
+        }
+    }
+
+
+    /**
+     * Mark Request that processed at primary node with attribute
+     * primaryIndicatorName
+     * 
+     * @param request
+     * @throws IOException
+     */
+    protected void createPrimaryIndicator(Request request) throws IOException {
+        String id = request.getRequestedSessionId();
+        if ((id != null) && (id.length() > 0)) {
+            Manager manager = request.getContext().getManager();
+            Session session = manager.findSession(id);
+            if (session instanceof ClusterSession) {
+                ClusterSession cses = (ClusterSession) session;
+                if (cses != null) {
+                    Boolean isPrimary = new Boolean(cses.isPrimarySession());
+                    if (log.isDebugEnabled())
+                        log.debug(sm.getString(
+                                "ReplicationValve.session.indicator", request.getContext().getName(),id,
+                                primaryIndicatorName, isPrimary));
+                    request.setAttribute(primaryIndicatorName, isPrimary);
+                }
+            } else {
+                if (log.isDebugEnabled()) {
+                    if (session != null) {
+                        log.debug(sm.getString(
+                                "ReplicationValve.session.found", request.getContext().getName(),id));
+                    } else {
+                        log.debug(sm.getString(
+                                "ReplicationValve.session.invalid", request.getContext().getName(),id));
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/java/org/apache/catalina/ha/tcp/SendMessageData.java b/java/org/apache/catalina/ha/tcp/SendMessageData.java
new file mode 100644 (file)
index 0000000..4fa914f
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.catalina.ha.tcp;
+
+import org.apache.catalina.tribes.Member;
+
+/**
+ * @author Peter Rossbach
+ * @version $Revision: 303987 $ $Date: 2005-07-08 15:50:30 -0500 (Fri, 08 Jul 2005) $
+ */
+public class SendMessageData {
+
+    private Object message ;
+    private Member destination ;
+    private Exception exception ;
+    
+    
+    /**
+     * @param message
+     * @param destination
+     * @param exception
+     */
+    public SendMessageData(Object message, Member destination,
+            Exception exception) {
+        super();
+        this.message = message;
+        this.destination = destination;
+        this.exception = exception;
+    }
+    
+    /**
+     * @return Returns the destination.
+     */
+    public Member getDestination() {
+        return destination;
+    }
+    /**
+     * @param destination The destination to set.
+     */
+    public void setDestination(Member destination) {
+        this.destination = destination;
+    }
+    /**
+     * @return Returns the exception.
+     */
+    public Exception getException() {
+        return exception;
+    }
+    /**
+     * @param exception The exception to set.
+     */
+    public void setException(Exception exception) {
+        this.exception = exception;
+    }
+    /**
+     * @return Returns the message.
+     */
+    public Object getMessage() {
+        return message;
+    }
+    /**
+     * @param message The message to set.
+     */
+    public void setMessage(Object message) {
+        this.message = message;
+    }
+}
diff --git a/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java b/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java
new file mode 100644 (file)
index 0000000..3e6a589
--- /dev/null
@@ -0,0 +1,932 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.tcp;
+
+import java.beans.PropertyChangeSupport;
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Host;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Manager;
+import org.apache.catalina.Valve;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.ha.ClusterListener;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterValve;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.group.GroupChannel;
+
+import org.apache.catalina.ha.session.DeltaManager;
+import org.apache.catalina.ha.util.IDynamicProperty;
+import org.apache.catalina.util.LifecycleSupport;
+import org.apache.catalina.util.StringManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tomcat.util.IntrospectionUtils;
+
+/**
+ * A <b>Cluster </b> implementation using simple multicast. Responsible for
+ * setting up a cluster and provides callers with a valid multicast
+ * receiver/sender.
+ * 
+ * FIXME remove install/remove/start/stop context dummys
+ * FIXME wrote testcases 
+ * 
+ * @author Filip Hanik
+ * @author Remy Maucherat
+ * @author Peter Rossbach
+ * @version $Revision: 379550 $, $Date: 2006-02-21 12:06:35 -0600 (Tue, 21 Feb 2006) $
+ */
+public class SimpleTcpCluster 
+    implements CatalinaCluster, Lifecycle, LifecycleListener, IDynamicProperty,
+               MembershipListener, ChannelListener{
+
+    public static Log log = LogFactory.getLog(SimpleTcpCluster.class);
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * Descriptive information about this component implementation.
+     */
+    protected static final String info = "SimpleTcpCluster/2.2";
+
+    public static final String BEFORE_MEMBERREGISTER_EVENT = "before_member_register";
+
+    public static final String AFTER_MEMBERREGISTER_EVENT = "after_member_register";
+
+    public static final String BEFORE_MANAGERREGISTER_EVENT = "before_manager_register";
+
+    public static final String AFTER_MANAGERREGISTER_EVENT = "after_manager_register";
+
+    public static final String BEFORE_MANAGERUNREGISTER_EVENT = "before_manager_unregister";
+
+    public static final String AFTER_MANAGERUNREGISTER_EVENT = "after_manager_unregister";
+
+    public static final String BEFORE_MEMBERUNREGISTER_EVENT = "before_member_unregister";
+
+    public static final String AFTER_MEMBERUNREGISTER_EVENT = "after_member_unregister";
+
+    public static final String SEND_MESSAGE_FAILURE_EVENT = "send_message_failure";
+
+    public static final String RECEIVE_MESSAGE_FAILURE_EVENT = "receive_message_failure";
+    
+    /**
+     * Group channel.
+     */
+    protected Channel channel = new GroupChannel();
+
+
+    /**
+     * Name for logging purpose
+     */
+    protected String clusterImpName = "SimpleTcpCluster";
+
+    /**
+     * The string manager for this package.
+     */
+    protected StringManager sm = StringManager.getManager(Constants.Package);
+
+    /**
+     * The cluster name to join
+     */
+    protected String clusterName ;
+
+    /**
+     * The Container associated with this Cluster.
+     */
+    protected Container container = null;
+
+    /**
+     * The lifecycle event support for this component.
+     */
+    protected LifecycleSupport lifecycle = new LifecycleSupport(this);
+
+    /**
+     * Has this component been started?
+     */
+    protected boolean started = false;
+
+    /**
+     * The property change support for this component.
+     */
+    protected PropertyChangeSupport support = new PropertyChangeSupport(this);
+
+    /**
+     * The context name <->manager association for distributed contexts.
+     */
+    protected Map managers = new HashMap();
+
+    private String managerClassName = "org.apache.catalina.ha.session.DeltaManager";
+
+
+    private List valves = new ArrayList();
+
+    private org.apache.catalina.ha.ClusterDeployer clusterDeployer;
+
+    /**
+     * Listeners of messages
+     */
+    protected List clusterListeners = new ArrayList();
+
+    /**
+     * Comment for <code>notifyLifecycleListenerOnFailure</code>
+     */
+    private boolean notifyLifecycleListenerOnFailure = false;
+
+    /**
+     * dynamic sender <code>properties</code>
+     */
+    private Map properties = new HashMap();
+    
+    private int channelSendOptions = 
+        Channel.SEND_OPTIONS_ASYNCHRONOUS |
+        Channel.SEND_OPTIONS_SYNCHRONIZED_ACK |
+        Channel.SEND_OPTIONS_USE_ACK;
+
+    // ------------------------------------------------------------- Properties
+
+    public SimpleTcpCluster() {
+    }
+
+    /**
+     * Return descriptive information about this Cluster implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+        return (info);
+    }
+
+    /**
+     * Set the name of the cluster to join, if no cluster with this name is
+     * present create one.
+     * 
+     * @param clusterName
+     *            The clustername to join
+     */
+    public void setClusterName(String clusterName) {
+        this.clusterName = clusterName;
+    }
+
+    /**
+     * Return the name of the cluster that this Server is currently configured
+     * to operate within.
+     * 
+     * @return The name of the cluster associated with this server
+     */
+    public String getClusterName() {
+        if(clusterName == null && container != null)
+            return container.getName() ;
+        return clusterName;
+    }
+
+    /**
+     * Set the Container associated with our Cluster
+     * 
+     * @param container
+     *            The Container to use
+     */
+    public void setContainer(Container container) {
+        Container oldContainer = this.container;
+        this.container = container;
+        support.firePropertyChange("container", oldContainer, this.container);
+    }
+
+    /**
+     * Get the Container associated with our Cluster
+     * 
+     * @return The Container associated with our Cluster
+     */
+    public Container getContainer() {
+        return (this.container);
+    }
+
+    /**
+     * @return Returns the notifyLifecycleListenerOnFailure.
+     */
+    public boolean isNotifyLifecycleListenerOnFailure() {
+        return notifyLifecycleListenerOnFailure;
+    }
+
+    /**
+     * @param notifyListenerOnFailure
+     *            The notifyLifecycleListenerOnFailure to set.
+     */
+    public void setNotifyLifecycleListenerOnFailure(
+            boolean notifyListenerOnFailure) {
+        boolean oldNotifyListenerOnFailure = this.notifyLifecycleListenerOnFailure;
+        this.notifyLifecycleListenerOnFailure = notifyListenerOnFailure;
+        support.firePropertyChange("notifyLifecycleListenerOnFailure",
+                oldNotifyListenerOnFailure,
+                this.notifyLifecycleListenerOnFailure);
+    }
+
+    public String getManagerClassName() {
+        if(managerClassName != null)
+            return managerClassName;
+        return (String)getProperty("manager.className");
+    }
+
+    public void setManagerClassName(String managerClassName) {
+        this.managerClassName = managerClassName;
+    }
+
+    /**
+     * Add cluster valve 
+     * Cluster Valves are only add to container when cluster is started!
+     * @param valve The new cluster Valve.
+     */
+    public void addValve(Valve valve) {
+        if (valve instanceof ClusterValve)
+            valves.add(valve);
+    }
+
+    /**
+     * get all cluster valves
+     * @return current cluster valves
+     */
+    public Valve[] getValves() {
+        return (Valve[]) valves.toArray(new Valve[valves.size()]);
+    }
+
+    /**
+     * Get the cluster listeners associated with this cluster. If this Array has
+     * no listeners registered, a zero-length array is returned.
+     */
+    public ClusterListener[] findClusterListeners() {
+        if (clusterListeners.size() > 0) {
+            ClusterListener[] listener = new ClusterListener[clusterListeners.size()];
+            clusterListeners.toArray(listener);
+            return listener;
+        } else
+            return new ClusterListener[0];
+
+    }
+
+    /**
+     * add cluster message listener and register cluster to this listener
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#addClusterListener(org.apache.catalina.ha.MessageListener)
+     */
+    public void addClusterListener(ClusterListener listener) {
+        if (listener != null && !clusterListeners.contains(listener)) {
+            clusterListeners.add(listener);
+            listener.setCluster(this);
+        }
+    }
+
+    /**
+     * remove message listener and deregister Cluster from listener
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#removeClusterListener(org.apache.catalina.ha.MessageListener)
+     */
+    public void removeClusterListener(ClusterListener listener) {
+        if (listener != null) {
+            clusterListeners.remove(listener);
+            listener.setCluster(null);
+        }
+    }
+
+    /**
+     * get current Deployer
+     */
+    public org.apache.catalina.ha.ClusterDeployer getClusterDeployer() {
+        return clusterDeployer;
+    }
+
+    /**
+     * set a new Deployer, must be set before cluster started!
+     */
+    public void setClusterDeployer(
+            org.apache.catalina.ha.ClusterDeployer clusterDeployer) {
+        this.clusterDeployer = clusterDeployer;
+    }
+
+    public void setChannel(Channel channel) {
+        this.channel = channel;
+    }
+
+    /**
+     * has members
+     */
+    protected boolean hasMembers = false;
+    public boolean hasMembers() {
+        return hasMembers;
+    }
+    
+    /**
+     * Get all current cluster members
+     * @return all members or empty array 
+     */
+    public Member[] getMembers() {
+        return channel.getMembers();
+    }
+
+    /**
+     * Return the member that represents this node.
+     * 
+     * @return Member
+     */
+    public Member getLocalMember() {
+        return channel.getLocalMember(true);
+    }
+
+    // ------------------------------------------------------------- dynamic
+    // manager property handling
+
+    /**
+     * JMX hack to direct use at jconsole
+     * 
+     * @param name
+     * @param value
+     */
+    public void setProperty(String name, String value) {
+        setProperty(name, (Object) value);
+    }
+
+    /**
+     * set config attributes with reflect and propagate to all managers
+     * 
+     * @param name
+     * @param value
+     */
+    public void setProperty(String name, Object value) {
+        if (log.isTraceEnabled())
+            log.trace(sm.getString("SimpleTcpCluster.setProperty", name, value,
+                    properties.get(name)));
+
+        properties.put(name, value);
+        if(started) {
+            // FIXME Hmm, is that correct when some DeltaManagers are direct configured inside Context?
+            // Why we not support it for other elements, like sender, receiver or membership?
+            // Must we restart element after change?
+            if (name.startsWith("manager")) {
+                String key = name.substring("manager".length() + 1);
+                String pvalue = value.toString();
+                for (Iterator iter = managers.values().iterator(); iter.hasNext();) {
+                    Manager manager = (Manager) iter.next();
+                    if(manager instanceof DeltaManager && ((ClusterManager) manager).isDefaultMode()) {
+                        IntrospectionUtils.setProperty(manager, key, pvalue );
+                    }
+                }
+            } 
+        }
+    }
+
+    /**
+     * get current config
+     * 
+     * @param key
+     * @return The property
+     */
+    public Object getProperty(String key) {
+        if (log.isTraceEnabled())
+            log.trace(sm.getString("SimpleTcpCluster.getProperty", key));
+        return properties.get(key);
+    }
+
+    /**
+     * Get all properties keys
+     * 
+     * @return An iterator over the property names.
+     */
+    public Iterator getPropertyNames() {
+        return properties.keySet().iterator();
+    }
+
+    /**
+     * remove a configured property.
+     * 
+     * @param key
+     */
+    public void removeProperty(String key) {
+        properties.remove(key);
+    }
+
+    /**
+     * transfer properties from cluster configuration to subelement bean.
+     * @param prefix
+     * @param bean
+     */
+    protected void transferProperty(String prefix, Object bean) {
+        if (prefix != null) {
+            for (Iterator iter = getPropertyNames(); iter.hasNext();) {
+                String pkey = (String) iter.next();
+                if (pkey.startsWith(prefix)) {
+                    String key = pkey.substring(prefix.length() + 1);
+                    Object value = getProperty(pkey);
+                    IntrospectionUtils.setProperty(bean, key, value.toString());
+                }
+            }
+        }
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * @return Returns the managers.
+     */
+    public Map getManagers() {
+        return managers;
+    }
+
+    public Channel getChannel() {
+        return channel;
+    }
+
+    /**
+     * Create new Manager without add to cluster (comes with start the manager)
+     * 
+     * @param name
+     *            Context Name of this manager
+     * @see org.apache.catalina.Cluster#createManager(java.lang.String)
+     * @see #addManager(String, Manager)
+     * @see DeltaManager#start()
+     */
+    public synchronized Manager createManager(String name) {
+        if (log.isDebugEnabled()) log.debug("Creating ClusterManager for context " + name + " using class " + getManagerClassName());
+        Manager manager = null;
+        ClassLoader oldCtxLoader = Thread.currentThread().getContextClassLoader();
+        try {
+            Thread.currentThread().setContextClassLoader(SimpleTcpCluster.class.getClassLoader());
+            manager = (Manager) getClass().getClassLoader().loadClass(getManagerClassName()).newInstance();
+        } catch (Exception x) {
+            log.error("Unable to load class for replication manager", x);
+            manager = new org.apache.catalina.ha.session.DeltaManager();
+        } finally {
+            Thread.currentThread().setContextClassLoader(oldCtxLoader);
+            if(manager != null) {
+                manager.setDistributable(true);
+                if (manager instanceof ClusterManager) {
+                    ClusterManager cmanager = (ClusterManager) manager ;
+                    cmanager.setDefaultMode(true);
+                    cmanager.setName(getManagerName(name,manager));
+                    cmanager.setCluster(this);
+                }
+            }
+        }
+        return manager;
+    }
+
+    /**
+     * remove an application form cluster replication bus
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#removeManager(java.lang.String,Manager)
+     */
+    public void removeManager(String name,Manager manager) {
+        if (manager != null) {
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(BEFORE_MANAGERUNREGISTER_EVENT,manager);
+            managers.remove(getManagerName(name,manager));
+            if (manager instanceof ClusterManager) ((ClusterManager) manager).setCluster(null);
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(AFTER_MANAGERUNREGISTER_EVENT, manager);
+        }
+    }
+
+    /**
+     * add an application to cluster replication bus
+     * 
+     * @param name
+     *            of the context
+     * @param manager
+     *            manager to register
+     * @see org.apache.catalina.ha.CatalinaCluster#addManager(java.lang.String,
+     *      org.apache.catalina.Manager)
+     */
+    public void addManager(String name, Manager manager) {
+        if (!manager.getDistributable()) {
+            log.warn("Manager with name " + name + " is not distributable, can't add as cluster manager");
+            return;
+        }
+        // Notify our interested LifecycleListeners
+        lifecycle.fireLifecycleEvent(BEFORE_MANAGERREGISTER_EVENT, manager);
+        String clusterName = getManagerName(name, manager);
+        if (manager instanceof ClusterManager) {
+            ClusterManager cmanager = (ClusterManager) manager ;
+            cmanager.setName(clusterName);
+            cmanager.setCluster(this);
+            if(cmanager.isDefaultMode()) transferProperty("manager",cmanager);
+        }
+        managers.put(clusterName, manager);
+        // Notify our interested LifecycleListeners
+        lifecycle.fireLifecycleEvent(AFTER_MANAGERREGISTER_EVENT, manager);
+    }
+
+    /**
+     * @param name
+     * @param manager
+     * @return
+     */
+    public String getManagerName(String name, Manager manager) {
+        String clusterName = name ;
+        if(getContainer() instanceof Engine) {
+            Container context = manager.getContainer() ;
+            if(context != null && context instanceof Context) {
+                Container host = ((Context)context).getParent();
+                if(host != null && host instanceof Host)
+                    clusterName = host.getName()  + name ;
+            }
+        }
+        return clusterName;
+    }
+
+    /*
+     * Get Manager
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#getManager(java.lang.String)
+     */
+    public Manager getManager(String name) {
+        return (Manager) managers.get(name);
+    }
+    
+    // ------------------------------------------------------ Lifecycle Methods
+
+    /**
+     * Execute a periodic task, such as reloading, etc. This method will be
+     * invoked inside the classloading context of this container. Unexpected
+     * throwables will be caught and logged.
+     * @see org.apache.catalina.ha.deploy.FarmWarDeployer#backgroundProcess()
+     * @see ReplicationTransmitter#backgroundProcess()
+     */
+    public void backgroundProcess() {
+        if (clusterDeployer != null) clusterDeployer.backgroundProcess();
+        //send a heartbeat through the channel
+        if ( channel !=null ) channel.heartbeat();
+    }
+
+    /**
+     * Add a lifecycle event listener to this component.
+     * 
+     * @param listener
+     *            The listener to add
+     */
+    public void addLifecycleListener(LifecycleListener listener) {
+        lifecycle.addLifecycleListener(listener);
+    }
+
+    /**
+     * Get the lifecycle listeners associated with this lifecycle. If this
+     * Lifecycle has no listeners registered, a zero-length array is returned.
+     */
+    public LifecycleListener[] findLifecycleListeners() {
+
+        return lifecycle.findLifecycleListeners();
+
+    }
+
+    /**
+     * Remove a lifecycle event listener from this component.
+     * 
+     * @param listener
+     *            The listener to remove
+     */
+    public void removeLifecycleListener(LifecycleListener listener) {
+        lifecycle.removeLifecycleListener(listener);
+    }
+
+    /**
+     * Use as base to handle start/stop/periodic Events from host. Currently
+     * only log the messages as trace level.
+     * 
+     * @see org.apache.catalina.LifecycleListener#lifecycleEvent(org.apache.catalina.LifecycleEvent)
+     */
+    public void lifecycleEvent(LifecycleEvent lifecycleEvent) {
+        if (log.isTraceEnabled())
+            log.trace(sm.getString("SimpleTcpCluster.event.log", lifecycleEvent.getType(), lifecycleEvent.getData()));
+    }
+
+    // ------------------------------------------------------ public
+
+    /**
+     * Prepare for the beginning of active use of the public methods of this
+     * component. This method should be called after <code>configure()</code>,
+     * and before any of the public methods of the component are utilized. <BR>
+     * Starts the cluster communication channel, this will connect with the
+     * other nodes in the cluster, and request the current session state to be
+     * transferred to this node.
+     * 
+     * @exception IllegalStateException
+     *                if this component has already been started
+     * @exception LifecycleException
+     *                if this component detects a fatal error that prevents this
+     *                component from being used
+     */
+    public void start() throws LifecycleException {
+        if (started)
+            throw new LifecycleException(sm.getString("cluster.alreadyStarted"));
+        if (log.isInfoEnabled()) log.info("Cluster is about to start");
+
+        // Notify our interested LifecycleListeners
+        lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, this);
+        try {
+            if ( clusterDeployer != null ) clusterDeployer.setCluster(this);
+            this.registerClusterValve();
+            if ( channel == null ) channel = new GroupChannel();
+            channel.addMembershipListener(this);
+            channel.addChannelListener(this);
+            channel.start(channel.DEFAULT);
+            if (clusterDeployer != null) clusterDeployer.start();
+            this.started = true;
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(AFTER_START_EVENT, this);
+        } catch (Exception x) {
+            log.error("Unable to start cluster.", x);
+            throw new LifecycleException(x);
+        }
+    }
+
+    /**
+     * register all cluster valve to host or engine
+     * @throws Exception
+     * @throws ClassNotFoundException
+     */
+    protected void registerClusterValve() throws Exception {
+        if(container != null ) {
+            for (Iterator iter = valves.iterator(); iter.hasNext();) {
+                ClusterValve valve = (ClusterValve) iter.next();
+                if (log.isDebugEnabled())
+                    log.debug("Invoking addValve on " + getContainer()
+                            + " with class=" + valve.getClass().getName());
+                if (valve != null) {
+                    IntrospectionUtils.callMethodN(getContainer(), "addValve",
+                            new Object[] { valve },
+                            new Class[] { org.apache.catalina.Valve.class });
+
+                }
+                valve.setCluster(this);
+            }
+        }
+    }
+
+    /**
+     * unregister all cluster valve to host or engine
+     * @throws Exception
+     * @throws ClassNotFoundException
+     */
+    protected void unregisterClusterValve() throws Exception {
+        for (Iterator iter = valves.iterator(); iter.hasNext();) {
+            ClusterValve valve = (ClusterValve) iter.next();
+            if (log.isDebugEnabled())
+                log.debug("Invoking removeValve on " + getContainer()
+                        + " with class=" + valve.getClass().getName());
+            if (valve != null) {
+                    IntrospectionUtils.callMethodN(getContainer(), "removeValve",
+                        new Object[] { valve }, new Class[] { org.apache.catalina.Valve.class });
+            }
+            valve.setCluster(this);
+        }
+    }
+
+    /**
+     * Gracefully terminate the active cluster component.<br/>
+     * This will disconnect the cluster communication channel, stop the
+     * listener and deregister the valves from host or engine.<br/><br/>
+     * <b>Note:</b><br/>The sub elements receiver, sender, membership,
+     * listener or valves are not removed. You can easily start the cluster again.
+     * 
+     * @exception IllegalStateException
+     *                if this component has not been started
+     * @exception LifecycleException
+     *                if this component detects a fatal error that needs to be
+     *                reported
+     */
+    public void stop() throws LifecycleException {
+
+        if (!started)
+            throw new IllegalStateException(sm.getString("cluster.notStarted"));
+        // Notify our interested LifecycleListeners
+        lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, this);
+
+        if (clusterDeployer != null) clusterDeployer.stop();
+        this.managers.clear();
+        try {
+            if ( clusterDeployer != null ) clusterDeployer.setCluster(null);
+            channel.stop(Channel.DEFAULT);
+            channel.removeChannelListener(this);
+            channel.removeMembershipListener(this);
+            this.unregisterClusterValve();
+        } catch (Exception x) {
+            log.error("Unable to stop cluster valve.", x);
+        }
+        started = false;
+        // Notify our interested LifecycleListeners
+        lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, this);
+   }
+
+    
+
+
+    /**
+     * send message to all cluster members
+     * @param msg message to transfer
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage)
+     */
+    public void send(ClusterMessage msg) {
+        send(msg, null);
+    }
+
+    /**
+     * send message to all cluster members same cluster domain
+     * 
+     * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage)
+     */
+    public void sendClusterDomain(ClusterMessage msg) {
+        send(msg,null);
+    } 
+
+    
+    /**
+     * send a cluster message to one member
+     * 
+     * @param msg message to transfer
+     * @param dest Receiver member
+     * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage,
+     *      org.apache.catalina.ha.Member)
+     */
+    public void send(ClusterMessage msg, Member dest) {
+        try {
+            msg.setAddress(getLocalMember());
+            if (dest != null) {
+                if (!getLocalMember().equals(dest)) {
+                    channel.send(new Member[] {dest}, msg,channelSendOptions);
+                } else
+                    log.error("Unable to send message to local member " + msg);
+            } else {
+                channel.send(channel.getMembers(),msg,channelSendOptions);
+            }
+        } catch (Exception x) {
+            log.error("Unable to send message through cluster sender.", x);
+        }
+    }
+
+    /**
+     * New cluster member is registered
+     * 
+     * @see org.apache.catalina.ha.MembershipListener#memberAdded(org.apache.catalina.ha.Member)
+     */
+    public void memberAdded(Member member) {
+        try {
+            hasMembers = channel.hasMembers();
+            if (log.isInfoEnabled()) log.info("Replication member added:" + member);
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(BEFORE_MEMBERREGISTER_EVENT, member);
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(AFTER_MEMBERREGISTER_EVENT, member);
+        } catch (Exception x) {
+            log.error("Unable to connect to replication system.", x);
+        }
+
+    }
+
+    /**
+     * Cluster member is gone
+     * 
+     * @see org.apache.catalina.ha.MembershipListener#memberDisappeared(org.apache.catalina.ha.Member)
+     */
+    public void memberDisappeared(Member member) {
+        try {
+            hasMembers = channel.hasMembers();            
+            if (log.isInfoEnabled()) log.info("Received member disappeared:" + member);
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(BEFORE_MEMBERUNREGISTER_EVENT, member);
+            // Notify our interested LifecycleListeners
+            lifecycle.fireLifecycleEvent(AFTER_MEMBERUNREGISTER_EVENT, member);
+        } catch (Exception x) {
+            log.error("Unable remove cluster node from replication system.", x);
+        }
+    }
+
+    // --------------------------------------------------------- receiver
+    // messages
+
+    /**
+     * notify all listeners from receiving a new message is not ClusterMessage
+     * emitt Failure Event to LifecylceListener
+     * 
+     * @param message
+     *            receveived Message
+     */
+    public boolean accept(Serializable msg, Member sender) {
+        return (msg instanceof ClusterMessage);
+    }
+    
+    
+    public void messageReceived(Serializable message, Member sender) {
+        ClusterMessage fwd = (ClusterMessage)message;
+        fwd.setAddress(sender);
+        messageReceived(fwd);
+    }
+
+    public void messageReceived(ClusterMessage message) {
+
+        long start = 0;
+        if (log.isDebugEnabled() && message != null)
+            log.debug("Assuming clocks are synched: Replication for "
+                    + message.getUniqueId() + " took="
+                    + (System.currentTimeMillis() - (message).getTimestamp())
+                    + " ms.");
+
+        //invoke all the listeners
+        boolean accepted = false;
+        if (message != null) {
+            for (Iterator iter = clusterListeners.iterator(); iter.hasNext();) {
+                ClusterListener listener = (ClusterListener) iter.next();
+                if (listener.accept(message)) {
+                    accepted = true;
+                    listener.messageReceived(message);
+                }
+            }
+        }
+        if (!accepted && log.isDebugEnabled()) {
+            if (notifyLifecycleListenerOnFailure) {
+                Member dest = message.getAddress();
+                // Notify our interested LifecycleListeners
+                lifecycle.fireLifecycleEvent(RECEIVE_MESSAGE_FAILURE_EVENT,
+                        new SendMessageData(message, dest, null));
+            }
+            log.debug("Message " + message.toString() + " from type "
+                    + message.getClass().getName()
+                    + " transfered but no listener registered");
+        }
+        return;
+    }
+
+    // --------------------------------------------------------- Logger
+
+    public Log getLogger() {
+        return log;
+    }
+
+
+
+    
+    // ------------------------------------------------------------- deprecated
+
+    /**
+     * 
+     * @see org.apache.catalina.Cluster#setProtocol(java.lang.String)
+     */
+    public void setProtocol(String protocol) {
+    }
+
+    /**
+     * @see org.apache.catalina.Cluster#getProtocol()
+     */
+    public String getProtocol() {
+        return null;
+    }
+
+    /**
+     * @see org.apache.catalina.Cluster#startContext(java.lang.String)
+     */
+    public void startContext(String contextPath) throws IOException {
+        
+    }
+
+    /**
+     * @see org.apache.catalina.Cluster#installContext(java.lang.String, java.net.URL)
+     */
+    public void installContext(String contextPath, URL war) {
+        
+    }
+
+    /**
+     * @see org.apache.catalina.Cluster#stop(java.lang.String)
+     */
+    public void stop(String contextPath) throws IOException {
+        
+    }
+}
diff --git a/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml b/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..ac04140
--- /dev/null
@@ -0,0 +1,1046 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mbeans-descriptors PUBLIC
+   "-//Apache Software Foundation//DTD Model MBeans Configuration File"
+   "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
+<mbeans-descriptors>
+
+  <mbean         name="SimpleTcpCluster"
+           description="Tcp Cluster implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.ha.tcp.SimpleTcpCluster">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="notifyLifecycleListenerOnFailure"
+          description="notify lifecycleListener from message transfer failure"
+                            is="true"
+                 type="boolean"/>                 
+    <attribute   name="clusterName"
+          description="name of cluster"
+                 type="java.lang.String"/>
+    <attribute   name="managerClassName"
+          description="session mananager classname"
+                 type="java.lang.String"/>
+    <attribute   name="clusterLogName"
+          description="Name of cluster transfer log device"
+                 type="java.lang.String"/>
+    <attribute   name="doClusterLog"
+                            is="true"
+          description="enable cluster log transfer logging"
+                 type="boolean"/>
+    <operation   name="setProperty"
+               description="set a property to all cluster managers (with prefix 'manager.')"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="key"
+                 description="Property name"
+                 type="java.lang.String"/>
+      <parameter name="value"
+                 description="Property value"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="send"
+               description="send message to all cluster members"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina.ha.ClusterMessage"/>
+    </operation>
+    
+    <operation   name="sendClusterDomain"
+               description="send message to all cluster members with same domain"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina.ha.ClusterMessage"/>
+    </operation>
+        
+    <operation   name="sendToMember"
+               description="send message to one cluster member"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina..cluster.ClusterMessage"/>
+      <parameter name="member"
+                 description="cluster member"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+  <mbean         name="ClusterReceiverBase"
+           description="Tcp Cluster ReplicationListener implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.ha.tcp.ClusterReceiverBase">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="tcpListenAddress"
+          description="tcp listener address"
+                 type="java.lang.String"/>
+    <attribute   name="tcpListenPort"
+          description="tcp listener port"
+                 type="int"/>
+    <attribute   name="tcpThreadCount"
+          description="number of tcp listener worker threads"
+                 type="int"/>
+    <attribute   name="tcpSelectorTimeout"
+          description="tcp listener Selector timeout"
+                 type="long"/>
+    <attribute   name="nrOfMsgsReceived"
+          description="number of messages received from other nodes"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedTime"
+          description="total time message send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedProcessingTime"
+          description="received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minReceivedProcessingTime"
+          description="minimal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgReceivedProcessingTime"
+          description="received processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxReceivedProcessingTime"
+          description="maximal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doReceivedProcessingStats"
+          description="create received processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="avgTotalReceivedBytes"
+          description="received totalReceivedBytes / nrOfMsgsReceived"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalReceivedBytes"
+          description="number of bytes received"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="sendAck"
+          description="send ack after data received"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="compress"
+          description="data received compressed"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="doListen"
+          description="is port really started"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+                 
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+  <mbean         name="SocketReplicationListener"
+           description="Tcp Cluster SocketReplicationListener implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.ha.tcp.SocketReplicationListener">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="tcpListenAddress"
+          description="tcp listener address"
+                 type="java.lang.String"/>
+    <attribute   name="tcpListenPort"
+          description="tcp listener port"
+                 type="int"/>
+    <attribute   name="tcpListenMaxPort"
+          description="max tcp listen used port"
+                 type="int"/>
+    <attribute   name="tcpListenTimeout"
+          description="max tcp listen timeout (sec) wait for ServerSocket start"
+                 type="int"/>                
+    <attribute   name="nrOfMsgsReceived"
+          description="number of messages received from other nodes"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedTime"
+          description="total time message send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedProcessingTime"
+          description="received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minReceivedProcessingTime"
+          description="minimal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgReceivedProcessingTime"
+          description="received processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxReceivedProcessingTime"
+          description="maximal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doReceivedProcessingStats"
+          description="create received processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="avgTotalReceivedBytes"
+          description="received totalReceivedBytes / nrOfMsgsReceived"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalReceivedBytes"
+          description="number of bytes received"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="sendAck"
+          description="send ack after data received"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+   <attribute   name="compress"
+          description="data received compressed"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+   <attribute   name="doListen"
+          description="is port really started"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+                 
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+  
+  <mbean         name="ReplicationTransmitter"
+          description="Tcp replication transmitter"
+               domain="Catalina"
+                group="ClusterSender"
+                 type="org.apache.catalina.ha.tcp.ReplicationTransmitter">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="replicationMode"
+          description="replication mode (synchnous,pooled.asynchnous,fastasyncqueue)"
+                 type="java.lang.String"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="autoConnect"
+          description="is sender disabled, fork a new socket"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doTransmitterProcessingStats"
+          description="create processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="failureCounter"
+          description="number of wrong transfers"
+                 type="long"
+                 writeable="false"/>
+       <attribute name="senderObjectNames"
+               description="get all sender object names"
+               type="[Ljavax.management.ObjectName;"
+               writeable="false"/>
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+       <operation name="checkKeepAlive"
+               description="Check all sender connection for close socket (keepalive)"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+  </mbean>
+
+  <mbean         name="AsyncSocketSender"
+          description="Async Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.ha.tcp.AsyncSocketSender">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>                 
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="queueSize"
+                 writeable="false"
+          description="queue size"
+                 type="int"/>
+    <attribute   name="queuedNrOfBytes"
+                 writeable="false"
+          description="number of bytes over all queued messages"
+                 type="long"/>
+    <attribute   name="messageTransferStarted"
+          description="message is in transfer"
+                 type="boolean"
+                 is="true"
+                 writeable="false"/>
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="keepAliveCount"
+          description="keep Alive request count"
+                 type="int"
+                 writeable="false"/>
+    <attribute   name="keepAliveConnectTime"
+          description="Connect time for keep alive"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doProcessingStats"
+          description="create processing time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitAckTime"
+          description="sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minWaitAckTime"
+          description="minimal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgWaitAckTime"
+          description="waitAck time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxWaitAckTime"
+          description="maximal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doWaitAckStats"
+          description="create waitAck time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenCounter"
+          description="counts open socket (KeepAlive and connects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenFailureCounter"
+          description="counts open socket failures"
+                 type="long"
+                 writeable="false"/>                                            
+    <attribute   name="socketCloseCounter"
+          description="counts closed socket (KeepAlive and disconnects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="missingAckCounter"
+          description="counts missing ack"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataResendCounter"
+          description="counts data resends"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataFailureCounter"
+          description="counts data send failures"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="inQueueCounter"
+          description="counts all queued messages"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="outQueueCounter"
+          description="counts all successfully sended messages"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="connect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="disconnect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="checkKeepAlive"
+               description="Check connection for close socket"
+               impact="ACTION"
+               returnType="boolean">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+
+ <mbean         name="FastAsyncSocketSender"
+          description="Fast Async Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.ha.tcp.FastAsyncSocketSender">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="threadPriority"
+          description="change queue thread priority"
+                 type="int"/>                 
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long" />
+    <attribute   name="queueSize"
+                 writeable="false"
+          description="queue size"
+                 type="int"/>
+    <attribute   name="queuedNrOfBytes"
+                 writeable="false"
+          description="number of bytes over all queued messages"
+                 type="long"/>
+    <attribute   name="messageTransferStarted"
+          description="message is in transfer"
+                 type="boolean"
+                 is="true"
+                 writeable="false"/>
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="queueAddWaitTimeout"
+          description="add wait timeout (default 10000 msec)"
+                 type="long"/>
+    <attribute   name="queueRemoveWaitTimeout"
+          description="remove wait timeout (default 30000 msec)"
+                 type="long"/>
+    <attribute   name="maxQueueLength"
+          description="max queue length"
+                 type="int"/>
+    <attribute   name="queueTimeWait"
+          description="remember queue wait times"
+                 is="true"
+                 type="boolean"/>
+    <attribute   name="queueCheckLock"
+          description="check to lost locks"
+                 is="true"
+                 type="boolean"/>
+    <attribute   name="queueDoStats"
+          description="activated queue stats"
+                 is="true"
+                 type="boolean"/>
+    <attribute   name="keepAliveCount"
+          description="keep Alive request count"
+                 type="int"
+                 writeable="false"/>
+    <attribute   name="keepAliveConnectTime"
+          description="Connect time for keep alive"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doProcessingStats"
+          description="create Processing time stats"
+                            is="true"
+                 type="boolean" />                 
+    <attribute   name="waitAckTime"
+          description="sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minWaitAckTime"
+          description="minimal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgWaitAckTime"
+          description="waitAck time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxWaitAckTime"
+          description="maximal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doWaitAckStats"
+          description="create waitAck time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenCounter"
+          description="counts open socket (KeepAlive and connects)"
+                 type="long"
+                 writeable="false"/>                            
+    <attribute   name="socketOpenFailureCounter"
+          description="counts open socket failures"
+                 type="long"
+                 writeable="false"/>                                            
+    <attribute   name="socketCloseCounter"
+          description="counts closed socket (KeepAlive and disconnects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="missingAckCounter"
+          description="counts missing ack"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataResendCounter"
+          description="counts data resends"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataFailureCounter"
+          description="counts data send failures"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="inQueueCounter"
+          description="counts all queued messages"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="outQueueCounter"
+          description="counts all successfully sended messages"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="queueAddWaitTime"
+          description="queue add wait time (tomcat thread waits)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="queueRemoveWaitTime"
+          description="queue remove wait time (queue thread waits)"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="connect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="disconnect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="checkKeepAlive"
+               description="Check connection for close socket"
+               impact="ACTION"
+               returnType="boolean">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+
+  <mbean         name="PooledSocketSender"
+          description="Pooled Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.ha.tcp.PooledSocketSender">
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="maxPoolSocketLimit"
+          description="Max parallel sockets"
+                 type="int"/>
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="start Queue to connect to ohter replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="stop Queue to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+
+  <mbean         name="SocketSender"
+          description="Sync Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.ha.tcp.SocketSender">
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="messageTransferStarted"
+          description="message is in transfer"
+                 type="boolean"
+                 is="true"
+                 writeable="false"/>
+    <attribute   name="keepAliveCount"
+          description="keep Alive request count"
+                 type="int"
+                 writeable="false"/>
+    <attribute   name="keepAliveConnectTime"
+          description="Connect time for keep alive"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doProcessingStats"
+          description="create Processing time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitAckTime"
+          description="sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minWaitAckTime"
+          description="minimal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgWaitAckTime"
+          description="waitAck time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxWaitAckTime"
+          description="maximal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doWaitAckStats"
+          description="create waitAck time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketCloseCounter"
+          description="counts closed socket (KeepAlive and disconnects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenFailureCounter"
+          description="counts open socket failures"
+                 type="long"
+                 writeable="false"/>                                            
+    <attribute   name="socketOpenCounter"
+          description="counts open socket (KeepAlive and connects)"
+                 type="long"
+                 writeable="false"/>                            
+    <attribute   name="missingAckCounter"
+          description="counts missing ack"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataResendCounter"
+          description="counts data resends"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataFailureCounter"
+          description="counts data send failures"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="connect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="disconnect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="checkKeepAlive"
+               description="Check connection for close socket"
+               impact="ACTION"
+               returnType="boolean">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+    
+  <mbean         name="ReplicationValve"
+          description="Valve for simple tcp replication"
+               domain="Catalina"
+                group="Valve"
+                 type="org.apache.catalina.ha.tcp.ReplicationValve">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="filter"
+          description="resource filter to disable session replication check"
+                 type="java.lang.String"/>
+    <attribute   name="primaryIndicator"
+                            is="true"
+          description="set indicator that request processing is at primary session node"
+                 type="boolean"/>
+    <attribute   name="primaryIndicatorName"
+          description="Request attribute name to indicate that request processing is at primary session node"
+                 type="java.lang.String"/>
+    <attribute   name="doProcessingStats"
+                            is="true"
+          description="active statistics counting"
+                 type="boolean"/>
+       <attribute   name="nrOfRequests"
+          description="number of replicated requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfFilterRequests"
+          description="number of filtered requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfSendRequests"
+          description="number of send requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfCrossContextSendRequests"
+          description="number of send cross context session requests"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalRequestTime"
+          description="total replicated request time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalSendTime"
+          description="total replicated send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="lastSendTime"
+          description="last replicated request time"
+                 type="long"
+                 writeable="false"/>
+    <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/ha/util/IDynamicProperty.java b/java/org/apache/catalina/ha/util/IDynamicProperty.java
new file mode 100644 (file)
index 0000000..b3a2ba2
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.ha.util;
+
+import java.util.Iterator;
+
+/**
+ * @author Peter Rossbach
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public interface IDynamicProperty {
+
+    /**
+     * set config attributes with reflect
+     * 
+     * @param name
+     * @param value
+     */
+    public void setProperty(String name, Object value) ;
+
+    /**
+     * get current config
+     * 
+     * @param key
+     * @return The property
+     */
+    public Object getProperty(String key) ;
+    /**
+     * Get all properties keys
+     * 
+     * @return An iterator over the property names
+     */
+    public Iterator getPropertyNames() ;
+
+    /**
+     * remove a configured property.
+     * 
+     * @param key
+     */
+    public void removeProperty(String key) ;
+
+}
diff --git a/java/org/apache/catalina/tribes/ByteMessage.java b/java/org/apache/catalina/tribes/ByteMessage.java
new file mode 100644 (file)
index 0000000..7ce371d
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.io.Serializable;
+import java.io.Externalizable;
+import java.io.ObjectInput;
+import java.io.IOException;
+import java.io.ObjectOutput;
+
+/**
+ * A byte message is not serialized and deserialized by the channel
+ * instead it is sent as a byte array<br>
+ * By default Tribes uses java serialization when it receives an object
+ * to be sent over the wire. Java serialization is not the most
+ * efficient of serializing data, and Tribes might not even
+ * have access to the correct class loaders to deserialize the object properly.
+ * <br>
+ * The ByteMessage class is a class where the channel when it receives it will
+ * not attempt to perform serialization, instead it will simply stream the <code>getMessage()</code>
+ * bytes.<br>
+ * If you are using multiple applications on top of Tribes you should add some sort of header
+ * so that you can decide with the <code>ChannelListener.accept()</code> whether this message was intended
+ * for you.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public class ByteMessage implements Serializable, Externalizable {
+    /**
+     * Storage for the message to be sent
+     */
+    private byte[] message;
+
+
+    /**
+     * Creates an empty byte message
+     * Constructor also for deserialization
+     */
+    public ByteMessage() {
+    }
+
+    /**
+     * Creates a byte message wit h
+     * @param data byte[] - the message contents
+     */
+    public ByteMessage(byte[] data) {
+        message = data;
+    }
+
+    /**
+     * Returns the message contents of this byte message
+     * @return byte[] - message contents, can be null
+     */
+    public byte[] getMessage() {
+        return message;
+    }
+
+    /**
+     * Sets the message contents of this byte message
+     * @param message byte[]
+     */
+    public void setMessage(byte[] message) {
+        this.message = message;
+    }
+
+    /**
+     * @see java.io.Externalizable#readExternal
+     * @param in ObjectInput
+     * @throws IOException
+     */
+    public void readExternal(ObjectInput in ) throws IOException {
+        int length = in.readInt();
+        message = new byte[length];
+        in.read(message,0,length);
+    }
+
+    /**
+     * @see java.io.Externalizable#writeExternal
+     * @param out ObjectOutput
+     * @throws IOException
+     */
+    public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeInt(message!=null?message.length:0);
+        if ( message!=null ) out.write(message,0,message.length);
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/Channel.java b/java/org/apache/catalina/tribes/Channel.java
new file mode 100644 (file)
index 0000000..1212ba1
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.io.Serializable;
+
+/**
+ * Channel interface<br>
+ * A channel is a representation of a group of nodes all participating in some sort of
+ * communication with each other.<br>
+ * The channel is the main API class for Tribes, this is essentially the only class
+ * that an application needs to be aware of. Through the channel the application can:<br>
+ * 1. send messages<br>
+ * 2. receive message (by registering a <code>ChannelListener</code><br>
+ * 3. get all members of the group <code>getMembers()</code><br>
+ * 4. receive notifications of members added and members disappeared by
+ *    registerering a <code>MembershipListener</code><br>
+ * <br>
+ * The channel has 5 major components:<br>
+ * 1. Data receiver, with a built in thread pool to receive messages from other peers<br>
+ * 2. Data sender, an implementation for sending data using NIO or java.io<br>
+ * 3. Membership listener,listens for membership broadcasts<br>
+ * 4. Membership broadcaster, broadcasts membership pings.<br>
+ * 5. Channel interceptors, the ability to manipulate messages as they are sent or arrive<br><br>
+ * The channel layout is:
+ * <pre><code>
+ *  ChannelListener_1..ChannelListener_N MembershipListener_1..MembershipListener_N [Application Layer]
+ *            \          \                  /                   /
+ *             \          \                /                   /
+ *              \          \              /                   /
+ *               \          \            /                   /
+ *                \          \          /                   /
+ *                 \          \        /                   /
+ *                  ---------------------------------------
+ *                                  |
+ *                                  |
+ *                               Channel
+ *                                  |
+ *                         ChannelInterceptor_1
+ *                                  |                                               [Channel stack]
+ *                         ChannelInterceptor_N
+ *                                  |
+ *                             Coordinator (implements MessageListener,MembershipListener,ChannelInterceptor)
+ *                          --------------------
+ *                         /        |           \ 
+ *                        /         |            \
+ *                       /          |             \
+ *                      /           |              \
+ *                     /            |               \
+ *           MembershipService ChannelSender ChannelReceiver                        [IO layer]
+ * </code></pre>
+ * 
+ * For example usage @see org.apache.catalina.tribes.group.GroupChannel
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public interface Channel {
+    
+    /**
+     * Start and stop sequences can be controlled by these constants
+     * This allows you to start separate components of the channel <br>
+     * DEFAULT - starts or stops all components in the channel
+     * @see #start(int)
+     * @see #stop(int)
+     */
+    public static final int DEFAULT = 15;
+
+    /**
+     * Start and stop sequences can be controlled by these constants
+     * This allows you to start separate components of the channel <br>
+     * SND_RX_SEQ - starts or stops the data receiver. Start means opening a server socket
+     * in case of a TCP implementation
+     * @see #start(int)
+     * @see #stop(int)
+     */
+    public static final int SND_RX_SEQ = 1;
+
+    /**
+     * Start and stop sequences can be controlled by these constants
+     * This allows you to start separate components of the channel <br>
+     * SND_TX_SEQ - starts or stops the data sender. This should not open any sockets,
+     * as sockets are opened on demand when a message is being sent
+     * @see #start(int)
+     * @see #stop(int)
+     */
+    public static final int SND_TX_SEQ = 2;
+
+    /**
+     * Start and stop sequences can be controlled by these constants
+     * This allows you to start separate components of the channel <br>
+     * MBR_RX_SEQ - starts or stops the membership listener. In a multicast implementation
+     * this will open a datagram socket and join a group and listen for membership messages
+     * members joining
+     * @see #start(int)
+     * @see #stop(int)
+     */
+    public static final int MBR_RX_SEQ = 4;
+
+    /**
+     * Start and stop sequences can be controlled by these constants
+     * This allows you to start separate components of the channel <br>
+     * MBR_TX_SEQ - starts or stops the membership broadcaster. In a multicast implementation
+     * this will open a datagram socket and join a group and broadcast the local member information
+     * @see #start(int)
+     * @see #stop(int)
+     */
+    public static final int MBR_TX_SEQ = 8;
+    
+    /**
+     * Send options, when a message is sent, it can have an option flag
+     * to trigger certain behavior. Most flags are used to trigger channel interceptors
+     * as the message passes through the channel stack. <br>
+     * However, there are four default flags that every channel implementation must implement<br>
+     * SEND_OPTIONS_BYTE_MESSAGE - The message is a pure byte message and no marshalling or unmarshalling will
+     * be performed.<br>
+     * 
+     * @see #send(Member[], Serializable , int)
+     * @see #send(Member[], Serializable, int, ErrorHandler)
+     */
+    public static final int SEND_OPTIONS_BYTE_MESSAGE = 0x0001;
+
+    /**
+     * Send options, when a message is sent, it can have an option flag
+     * to trigger certain behavior. Most flags are used to trigger channel interceptors
+     * as the message passes through the channel stack. <br>
+     * However, there are four default flags that every channel implementation must implement<br>
+     * SEND_OPTIONS_USE_ACK - Message is sent and an ACK is received when the message has been received by the recipient<br>
+     * If no ack is received, the message is not considered successful<br>
+     * @see #send(Member[], Serializable , int)
+     * @see #send(Member[], Serializable, int, ErrorHandler)
+     */
+    public static final int SEND_OPTIONS_USE_ACK = 0x0002;
+
+    /**
+     * Send options, when a message is sent, it can have an option flag
+     * to trigger certain behavior. Most flags are used to trigger channel interceptors
+     * as the message passes through the channel stack. <br>
+     * However, there are four default flags that every channel implementation must implement<br>
+     * SEND_OPTIONS_SYNCHRONIZED_ACK - Message is sent and an ACK is received when the message has been received and 
+     * processed by the recipient<br>
+     * If no ack is received, the message is not considered successful<br>
+     * @see #send(Member[], Serializable , int)
+     * @see #send(Member[], Serializable, int, ErrorHandler)
+     */
+    public static final int SEND_OPTIONS_SYNCHRONIZED_ACK = 0x0004;
+    
+    /**
+     * Send options, when a message is sent, it can have an option flag
+     * to trigger certain behavior. Most flags are used to trigger channel interceptors
+     * as the message passes through the channel stack. <br>
+     * However, there are four default flags that every channel implementation must implement<br>
+     * SEND_OPTIONS_ASYNCHRONOUS - Message is sent and an ACK is received when the message has been received and 
+     * processed by the recipient<br>
+     * If no ack is received, the message is not considered successful<br>
+     * @see #send(Member[], Serializable , int)
+     * @see #send(Member[], Serializable, int, ErrorHandler)
+     */
+    public static final int SEND_OPTIONS_ASYNCHRONOUS = 0x0008;
+    
+
+    /**
+     * Send options, when a message is sent, it can have an option flag
+     * to trigger certain behavior. Most flags are used to trigger channel interceptors
+     * as the message passes through the channel stack. <br>
+     * However, there are four default flags that every channel implementation must implement<br>
+     * SEND_OPTIONS_DEFAULT - the default sending options, just a helper variable. <br>
+     * The default is <code>int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK;</code><br>
+     * @see #SEND_OPTIONS_USE_ACK
+     * @see #send(Member[], Serializable , int)
+     * @see #send(Member[], Serializable, int, ErrorHandler)
+     */
+    public static final int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK;
+
+    
+    /**
+     * Adds an interceptor to the channel message chain.
+     * @param interceptor ChannelInterceptor
+     */
+    public void addInterceptor(ChannelInterceptor interceptor);
+    
+    /**
+     * Starts up the channel. This can be called multiple times for individual services to start
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will start all services <BR>
+     * MBR_RX_SEQ - starts the membership receiver <BR>
+     * MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * SND_TX_SEQ - starts the replication transmitter<BR>
+     * SND_RX_SEQ - starts the replication receiver<BR>
+     * <b>Note:</b> In order for the membership broadcaster to 
+     * transmit the correct information, it has to be started after the replication receiver.
+     * @throws ChannelException if a startup error occurs or the service is already started or an error occurs.
+     */
+    public void start(int svc) throws ChannelException;
+
+    /**
+     * Shuts down the channel. This can be called multiple times for individual services to shutdown
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will shutdown all services <BR>
+     * MBR_RX_SEQ - stops the membership receiver <BR>
+     * MBR_TX_SEQ - stops the membership broadcaster <BR>
+     * SND_TX_SEQ - stops the replication transmitter<BR>
+     * SND_RX_SEQ - stops the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already stopped or an error occurs.
+     */
+    public void stop(int svc) throws ChannelException;    
+    
+    /**
+     * Send a message to one or more members in the cluster
+     * @param destination Member[] - the destinations, can not be null or zero length, the reason for that
+     * is that a membership change can occur and at that time the application is uncertain what group the message
+     * actually got sent to.
+     * @param msg Serializable - the message to send, has to be serializable, or a <code>ByteMessage</code> to 
+     * send a pure byte array
+     * @param options int - sender options, see class documentation for each interceptor that is configured in order to trigger interceptors
+     * @return a unique Id that identifies the message that is sent
+     * @see ByteMessage
+     * @see #SEND_OPTIONS_USE_ACK
+     * @see #SEND_OPTIONS_ASYNCHRONOUS
+     * @see #SEND_OPTIONS_SYNCHRONIZED_ACK
+     */
+    public UniqueId send(Member[] destination, Serializable msg, int options) throws ChannelException;
+
+    /**
+     * Send a message to one or more members in the cluster
+     * @param destination Member[] - the destinations, null or zero length means all
+     * @param msg ClusterMessage - the message to send
+     * @param options int - sender options, see class documentation
+     * @param handler ErrorHandler - handle errors through a callback, rather than throw it
+     * @return a unique Id that identifies the message that is sent
+     * @exception ChannelException - if a serialization error happens.
+     */
+    public UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) throws ChannelException;
+    
+    /**
+     * Sends a heart beat through the interceptor stacks
+     * Use this method to alert interceptors and other components to 
+     * clean up garbage, timed out messages etc.<br>
+     * If you application has a background thread, then you can save one thread,
+     * by configuring your channel to not use an internal heartbeat thread
+     * and invoking this method.
+     * @see #setHeartbeat(boolean)
+     */
+    public void heartbeat();
+    
+    /**
+     * Enables or disables internal heartbeat.
+     * @param enable boolean - default value is implementation specific
+     * @see #heartbeat()
+     */
+    public void setHeartbeat(boolean enable);
+    
+    /**
+     * Add a membership listener, will get notified when a new member joins, leaves or crashes
+     * <br>If the membership listener implements the Heartbeat interface
+     * the <code>heartbeat()</code> method will be invoked when the heartbeat runs on the channel
+     * @param listener MembershipListener
+     * @see MembershipListener
+     */
+    public void addMembershipListener(MembershipListener listener);
+    
+    /**
+     * Add a channel listener, this is a callback object when messages are received
+     * <br>If the channel listener implements the Heartbeat interface
+     * the <code>heartbeat()</code> method will be invoked when the heartbeat runs on the channel
+     * @param listener ChannelListener
+     * @see ChannelListener
+     * @see Heartbeat
+     */
+    public void addChannelListener(ChannelListener listener);
+
+    /**
+     * remove a membership listener, listeners are removed based on Object.hashCode and Object.equals
+     * @param listener MembershipListener
+     * @see MembershipListener
+     */
+    public void removeMembershipListener(MembershipListener listener);
+    /**
+     * remove a channel listener, listeners are removed based on Object.hashCode and Object.equals
+     * @param listener ChannelListener
+     * @see ChannelListener
+     */
+    public void removeChannelListener(ChannelListener listener);
+    
+    /**
+     * Returns true if there are any members in the group,
+     * this call is the same as <code>getMembers().length>0</code>
+     * @return boolean - true if there are any members automatically discovered
+     */
+    public boolean hasMembers() ;
+
+    /**
+     * Get all current group members
+     * @return all members or empty array, never null 
+     */
+    public Member[] getMembers() ;
+
+    /**
+     * Return the member that represents this node. This is also the data
+     * that gets broadcasted through the membership broadcaster component
+     * @param incAlive - optimization, true if you want it to calculate alive time
+     * since the membership service started.
+     * @return Member
+     */
+    public Member getLocalMember(boolean incAlive);
+    
+    /**
+     * Returns the member from the membership service with complete and 
+     * recent data. Some implementations might serialize and send 
+     * membership information along with a message, and instead of sending
+     * complete membership details, only send the primary identifier for the member
+     * but not the payload or other information. When such message is received
+     * the application can retrieve the cached member through this call.<br>
+     * In most cases, this is not necessary.
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr);
+
+    
+}
diff --git a/java/org/apache/catalina/tribes/ChannelException.java b/java/org/apache/catalina/tribes/ChannelException.java
new file mode 100644 (file)
index 0000000..7d5f04c
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.util.ArrayList;
+
+/**
+ * Channel Exception<br>
+ * A channel exception is thrown when an internal error happens
+ * somewhere in the channel. <br>
+ * When a global error happens, the cause can be retrieved using <code>getCause()</code><br><br>
+ * If an application is sending a message and some of the recipients fail to receive it,
+ * the application can retrieve what recipients failed by using the <code>getFaultyMembers()</code>
+ * method. This way, an application will always know if a message was delivered successfully or not.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public class ChannelException extends Exception {
+    /*
+     * Holds a list of faulty members
+     */
+    private ArrayList faultyMembers=null;
+    
+    /**
+     * Constructor, creates a ChannelException
+     * @see java.lang.Exception#Exception()
+     */
+    public ChannelException() {
+        super();
+    }
+
+    /**
+     * Constructor, creates a ChannelException with an error message
+     * @see java.lang.Exception#Exception(String)
+     */
+    public ChannelException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor, creates a ChannelException with an error message and a cause
+     * @param message String
+     * @param cause Throwable
+     * @see java.lang.Exception#Exception(String,Throwable)
+     */
+    public ChannelException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructor, creates a ChannelException with a cause
+     * @param cause Throwable
+     * @see java.lang.Exception#Exception(Throwable)
+     */
+    public ChannelException(Throwable cause) {
+        super(cause);
+    }
+    
+    /**
+     * Returns the message for this exception
+     * @return String
+     * @see java.lang.Exception#getMessage()
+     */
+    public String getMessage() {
+        StringBuffer buf = new StringBuffer(super.getMessage());
+        if (faultyMembers==null || faultyMembers.size() == 0 ) {
+            buf.append("; No faulty members identified.");
+        } else {
+            buf.append("; Faulty members:");
+            for ( int i=0; i<faultyMembers.size(); i++ ) {
+                FaultyMember mbr = (FaultyMember)faultyMembers.get(i);
+                buf.append(mbr.getMember().getName());
+                buf.append("; ");
+            }
+        }
+        return buf.toString();
+    }
+    
+    /**
+     * Adds a faulty member, and the reason the member failed.
+     * @param mbr Member
+     * @param x Exception
+     */
+    public void addFaultyMember(Member mbr, Exception x ) {
+        addFaultyMember(new FaultyMember(mbr,x));
+    }
+    
+    /**
+     * Adds a list of faulty members
+     * @param mbrs FaultyMember[]
+     */
+    public void addFaultyMember(FaultyMember[] mbrs) {
+        for (int i=0; mbrs!=null && i<mbrs.length; i++ ) {
+            addFaultyMember(mbrs[i]);
+        }
+    }
+
+    /**
+     * Adds a faulty member
+     * @param mbr FaultyMember
+     */
+    public void addFaultyMember(FaultyMember mbr) {
+        if ( this.faultyMembers==null ) this.faultyMembers = new ArrayList();
+        faultyMembers.add(mbr);
+    }
+    
+    /**
+     * Returns an array of members that failed and the reason they failed.
+     * @return FaultyMember[]
+     */
+    public FaultyMember[] getFaultyMembers() {
+        if ( this.faultyMembers==null ) return new FaultyMember[0];
+        return (FaultyMember[])faultyMembers.toArray(new FaultyMember[faultyMembers.size()]);
+    }
+    
+    /**
+     * 
+     * <p>Title: FaultyMember class</p> 
+     * 
+     * <p>Description: Represent a failure to a specific member when a message was sent
+     * to more than one member</p> 
+     * 
+     * @author Filip Hanik
+     * @version 1.0
+     */
+    public static class FaultyMember {
+        protected Exception cause;
+        protected Member member;
+        public FaultyMember(Member mbr, Exception x) { 
+            this.member = mbr;
+            this.cause = x;
+        }
+        
+        public Member getMember() {
+            return member;
+        }
+        
+        public Exception getCause() {
+            return cause;
+        }
+        
+        public String toString() {
+            return "FaultyMember:"+member.toString();
+        }
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelInterceptor.java b/java/org/apache/catalina/tribes/ChannelInterceptor.java
new file mode 100644 (file)
index 0000000..14b8f11
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import org.apache.catalina.tribes.group.InterceptorPayload;
+
+/**
+ * A ChannelInterceptor is an interceptor that intercepts 
+ * messages and membership messages in the channel stack.
+ * This allows interceptors to modify the message or perform
+ * other actions when a message is sent or received.<br>
+ * Interceptors are tied together in a linked list.
+ * @see org.apache.catalina.tribes.group.ChannelInterceptorBase
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */   
+
+public interface ChannelInterceptor extends MembershipListener, Heartbeat {
+
+    /**
+     * An interceptor can react to a message based on a set bit on the 
+     * message options. <br>
+     * When a message is sent, the options can be retrieved from ChannelMessage.getOptions()
+     * and if the bit is set, this interceptor will react to it.<br>
+     * A simple evaluation if an interceptor should react to the message would be:<br>
+     * <code>boolean react = (getOptionFlag() == (getOptionFlag() & ChannelMessage.getOptions()));</code><br>
+     * The default option is 0, meaning there is no way for the application to trigger the
+     * interceptor. The interceptor itself will decide.<br>
+     * @return int
+     * @see ChannelMessage#getOptions()
+     */
+    public int getOptionFlag();
+    
+    /**
+     * Sets the option flag
+     * @param flag int
+     * @see #getOptionFlag()
+     */
+    public void setOptionFlag(int flag);
+
+    /**
+     * Set the next interceptor in the list of interceptors
+     * @param next ChannelInterceptor
+     */
+    public void setNext(ChannelInterceptor next) ;
+
+    /**
+     * Retrieve the next interceptor in the list
+     * @return ChannelInterceptor - returns the next interceptor in the list or null if no more interceptors exist
+     */
+    public ChannelInterceptor getNext();
+
+    /**
+     * Set the previous interceptor in the list
+     * @param previous ChannelInterceptor
+     */
+    public void setPrevious(ChannelInterceptor previous);
+
+    /**
+     * Retrieve the previous interceptor in the list
+     * @return ChannelInterceptor - returns the previous interceptor in the list or null if no more interceptors exist
+     */
+    public ChannelInterceptor getPrevious();
+
+    /**
+     * The <code>sendMessage</code> method is called when a message is being sent to one more destinations.
+     * The interceptor can modify any of the parameters and then pass on the message down the stack by
+     * invoking <code>getNext().sendMessage(destination,msg,payload)</code><br>
+     * Alternatively the interceptor can stop the message from being sent by not invoking 
+     * <code>getNext().sendMessage(destination,msg,payload)</code><br>
+     * If the message is to be sent asynchronous the application can be notified of completion and 
+     * errors by passing in an error handler attached to a payload object.<br>
+     * The ChannelMessage.getAddress contains Channel.getLocalMember, and can be overwritten 
+     * to simulate a message sent from another node.<br>
+     * @param destination Member[] - the destination for this message
+     * @param msg ChannelMessage - the message to be sent
+     * @param payload InterceptorPayload - the payload, carrying an error handler and future useful data, can be null
+     * @throws ChannelException
+     * @see ErrorHandler
+     * @see InterceptorPayload
+     */
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException;
+    
+    /**
+     * the <code>messageReceived</code> is invoked when a message is received.
+     * <code>ChannelMessage.getAddress()</code> is the sender, or the reply-to address
+     * if it has been overwritten.
+     * @param data ChannelMessage
+     */
+    public void messageReceived(ChannelMessage data);
+    
+    /**
+     * The <code>heartbeat()</code> method gets invoked periodically
+     * to allow interceptors to clean up resources, time out object and 
+     * perform actions that are unrelated to sending/receiving data.
+     */
+    public void heartbeat();
+    
+    /**
+     * Intercepts the <code>Channel.hasMembers()</code> method
+     * @return boolean - if the channel has members in its membership group
+     * @see Channel#hasMembers()
+     */
+    public boolean hasMembers() ;
+
+    /**
+     * Intercepts the code>Channel.getMembers()</code> method
+     * @return Member[]
+     * @see Channel#getMembers()
+     */
+    public Member[] getMembers() ;
+
+    /**
+     * Intercepts the code>Channel.getLocalMember(boolean)</code> method
+     * @param incAliveTime boolean
+     * @return Member
+     * @see Channel#getLocalMember(boolean)
+     */
+    public Member getLocalMember(boolean incAliveTime) ;
+
+    /**
+     * Intercepts the code>Channel.getMember(Member)</code> method
+     * @param mbr Member
+     * @return Member - the actual member information, including stay alive
+     * @see Channel#getMember(Member)
+     */
+    public Member getMember(Member mbr);
+    
+    /**
+     * Starts up the channel. This can be called multiple times for individual services to start
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * Channel.DEFAULT - will start all services <BR>
+     * Channel.MBR_RX_SEQ - starts the membership receiver <BR>
+     * Channel.MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * Channel.SND_TX_SEQ - starts the replication transmitter<BR>
+     * Channel.SND_RX_SEQ - starts the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     * @see Channel
+     */
+    public void start(int svc) throws ChannelException;
+
+    /**
+     * Shuts down the channel. This can be called multiple times for individual services to shutdown
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * Channel.DEFAULT - will shutdown all services <BR>
+     * Channel.MBR_RX_SEQ - stops the membership receiver <BR>
+     * Channel.MBR_TX_SEQ - stops the membership broadcaster <BR>
+     * Channel.SND_TX_SEQ - stops the replication transmitter<BR>
+     * Channel.SND_RX_SEQ - stops the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     * @see Channel
+     */
+    public void stop(int svc) throws ChannelException;
+    
+    public void fireInterceptorEvent(InterceptorEvent event);
+
+    interface InterceptorEvent {
+        int getEventType();
+        String getEventTypeDesc();
+        ChannelInterceptor getInterceptor();
+    }
+    
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelListener.java b/java/org/apache/catalina/tribes/ChannelListener.java
new file mode 100644 (file)
index 0000000..97819b6
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.io.Serializable;
+/**
+ * 
+ * <p>Title: ChannelListener</p> 
+ * 
+ * <p>Description: An interface to listens to incoming messages from a channel </p> 
+ * When a message is received, the Channel will invoke the channel listener in a conditional sequence.
+ * <code>if ( listener.accept(msg,sender) ) listener.messageReceived(msg,sender);</code><br>
+ * A ChannelListener implementation MUST NOT return true on <code>accept(Serializable, Member)</code>
+ * if it doesn't intend to process the message. The channel can this way track whether a message
+ * was processed by an above application or if it was just received and forgot about, a featuer required
+ * to support message-response(RPC) calls<br>
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+
+public interface ChannelListener {
+
+    /**
+     * Receive a message from the channel
+     * @param msg Serializable
+     * @param sender - the source of the message
+     */
+    public void messageReceived(Serializable msg, Member sender);
+
+    /**
+     * Invoked by the channel to determine if the listener will process this message or not.
+     * @param msg Serializable
+     * @param sender Member
+     * @return boolean
+     */
+    public boolean accept(Serializable msg, Member sender);
+
+    /**
+     * 
+     * @param listener Object
+     * @return boolean
+     * @see Object#equals(Object)
+     */
+    public boolean equals(Object listener);
+
+    /**
+     * 
+     * @return int
+     * @see Object#hashCode(int)
+     */
+    public int hashCode();
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelMessage.java b/java/org/apache/catalina/tribes/ChannelMessage.java
new file mode 100644 (file)
index 0000000..21a434b
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright 1999,2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.io.Serializable;
+import org.apache.catalina.tribes.io.XByteBuffer;
+
+/**
+ * Message that is passed through the interceptor stack after the 
+ * data serialized in the Channel object and then passed down to the 
+ * interceptor and eventually down to the ChannelSender component
+ * @author Filip Hanik
+ * 
+ */
+public interface ChannelMessage extends Serializable {
+    
+    
+    
+    
+    /**
+     * Get the address that this message originated from.  
+     * Almost always <code>Channel.getLocalMember(boolean)</code><br>
+     * This would be set to a different address 
+     * if the message was being relayed from a host other than the one
+     * that originally sent it.
+     * @return the source or reply-to address of this message
+     */
+    public Member getAddress();
+
+    /**
+     * Sets the source or reply-to address of this message
+     * @param member Member
+     */
+    public void setAddress(Member member);
+
+    /**
+     * Timestamp of when the message was created.
+     * @return long timestamp in milliseconds
+     */
+    public long getTimestamp();
+
+    /**
+     *
+     * Sets the timestamp of this message
+     * @param timestamp The timestamp
+     */
+    public void setTimestamp(long timestamp);
+
+    /**
+     * Each message must have a globally unique Id.
+     * interceptors heavily depend on this id for message processing
+     * @return byte
+     */
+    public byte[] getUniqueId();
+    
+    /**
+     * The byte buffer that contains the actual message payload
+     * @param buf XByteBuffer
+     */
+    public void setMessage(XByteBuffer buf);
+    
+    /**
+     * returns the byte buffer that contains the actual message payload
+     * @return XByteBuffer
+     */
+    public XByteBuffer getMessage();
+    
+    /**
+     * The message options is a 32 bit flag set
+     * that triggers interceptors and message behavior.
+     * @see Channel#send(Member[], Serializable, int) 
+     * @see ChannelInterceptor#getOptionFlag
+     * @return int - the option bits set for this message
+     */
+    public int getOptions();
+    
+    /**
+     * sets the option bits for this message
+     * @param options int
+     * @see #getOptions()
+     */
+    public void setOptions(int options);
+    
+    /**
+     * Shallow clone, what gets cloned depends on the implementation
+     * @return ChannelMessage
+     */
+    public Object clone();
+
+    /**
+     * Deep clone, all fields MUST get cloned
+     * @return ChannelMessage
+     */
+    public Object deepclone();
+}
diff --git a/java/org/apache/catalina/tribes/ChannelReceiver.java b/java/org/apache/catalina/tribes/ChannelReceiver.java
new file mode 100644 (file)
index 0000000..346092e
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+
+/**
+ * ChannelReceiver Interface<br>
+ * The <code>ChannelReceiver</code> interface is the data receiver component 
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
+ * This class may optionally implement a thread pool for parallel processing of incoming messages.
+ * @author Filip Hanik
+ * @version $Revision: 379904 $, $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $
+ */
+public interface ChannelReceiver extends Heartbeat {
+    /**
+     * Start listening for incoming messages on the host/port
+     * @throws java.io.IOException
+     */
+    public void start() throws java.io.IOException;
+
+    /**
+     * Stop listening for messages
+     */
+    public void stop();
+
+    /**
+     * String representation of the IPv4 or IPv6 address that this host is listening
+     * to.
+     * @return the host that this receiver is listening to
+     */
+    public String getHost();
+    
+    
+    /**
+     * Returns the listening port
+     * @return port
+     */
+    public int getPort();
+    
+    /**
+     * Sets the message listener to receive notification of incoming
+     * @param listener MessageListener
+     * @see MessageListener
+     */
+    public void setMessageListener(MessageListener listener);
+    
+    /**
+     * Returns the message listener that is associated with this receiver
+     * @return MessageListener
+     * @see MessageListener
+     */
+    public MessageListener getMessageListener();
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelSender.java b/java/org/apache/catalina/tribes/ChannelSender.java
new file mode 100644 (file)
index 0000000..fd245e1
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+
+/**
+ * ChannelReceiver Interface<br>
+ * The <code>ChannelSender</code> interface is the data sender component 
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).<br>
+ * The channel sender must support "silent" members, ie, be able to send a message to a member
+ * that is not in the membership, but is part of the destination parameter
+ * @author Filip Hanik
+ * @version $Revision: 379904 $, $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $
+ */
+public interface ChannelSender extends Heartbeat
+{
+    /**
+     * Notify the sender of a member being added to the group.<br>
+     * Optional. This can be an empty implementation, that does nothing
+     * @param member Member
+     */
+    public void add(Member member);
+    /**
+     * Notification that a member has been removed or crashed.
+     * Can be used to clean up open connections etc
+     * @param member Member
+     */
+    public void remove(Member member);
+    
+    /**
+     * Start the channel sender
+     * @throws IOException if preprocessing takes place and an error happens
+     */
+    public void start() throws java.io.IOException;
+    /**
+     * Stop the channel sender
+     */
+    public void stop();
+    
+    /**
+     * A channel heartbeat, use this method to clean up resources
+     */
+    public void heartbeat() ;
+    
+    /**
+     * Send a message to one or more recipients.
+     * @param message ChannelMessage - the message to be sent
+     * @param destination Member[] - the destinations
+     * @throws ChannelException - if an error happens, the ChannelSender MUST report
+     * individual send failures on a per member basis, using ChannelException.addFaultyMember
+     * @see ChannelException#addFaultyMember(Member,java.lang.Exception)
+     */
+    public void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException;
+}
diff --git a/java/org/apache/catalina/tribes/Constants.java b/java/org/apache/catalina/tribes/Constants.java
new file mode 100644 (file)
index 0000000..019d106
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.tribes;
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.tribes</code>
+ * package.
+ *
+ * @author Bip Thelin
+ * @author Filip Hanik
+ * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $
+ */
+
+public final class Constants {
+    public static final String Package = "org.apache.catalina.tribes";
+}
diff --git a/java/org/apache/catalina/tribes/ErrorHandler.java b/java/org/apache/catalina/tribes/ErrorHandler.java
new file mode 100644 (file)
index 0000000..6496ace
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+
+
+/**
+ * The <code>ErrorHandler</code> class is used when sending messages
+ * that are sent asynchronously and the application still needs to get 
+ * confirmation when the message was sent successfully or when a message errored out.
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public interface ErrorHandler {
+    
+    /**
+     * Invoked if the message is dispatched asynch, and an error occurs
+     * @param x ChannelException - the error that happened
+     * @param id - the unique id for the message
+     * @see Channel#send(Member[], Serializable, int, ErrorHandler)
+     */
+    public void handleError(ChannelException x, UniqueId id);
+    
+    /**
+     * Invoked when the message has been sent successfully.
+     * @param id - the unique id for the message
+     * @see Channel#send(Member[], Serializable, int, ErrorHandler)
+     */
+    public void handleCompletion(UniqueId id);
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/Heartbeat.java b/java/org/apache/catalina/tribes/Heartbeat.java
new file mode 100644 (file)
index 0000000..ba5db2b
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+/**
+ * Can be implemented by the ChannelListener and Membership listeners to receive heartbeat
+ * notifications from the Channel
+ * @author Filip Hanik
+ * @version 1.0
+ * @see Channel
+ * @see Channel#heartbeat()
+ */
+public interface Heartbeat {
+    
+    /**
+     * Heartbeat invokation for resources cleanup etc
+     */
+    public void heartbeat();
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/LocalStrings.properties b/java/org/apache/catalina/tribes/LocalStrings.properties
new file mode 100644 (file)
index 0000000..0dafa46
--- /dev/null
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/tribes/ManagedChannel.java b/java/org/apache/catalina/tribes/ManagedChannel.java
new file mode 100644 (file)
index 0000000..aa56d3a
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import java.util.Iterator;
+
+/**
+ * Channel interface
+ * A managed channel interface gives you access to the components of the channels
+ * such as senders, receivers, interceptors etc for configurations purposes
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public interface ManagedChannel extends Channel {
+
+    /**
+     * Sets the channel sender
+     * @param sender ChannelSender
+     * @see ChannelSender
+     */
+    public void setChannelSender(ChannelSender sender);
+
+    /**
+     * Sets the channel receiver
+     * @param receiver ChannelReceiver
+     * @see ChannelReceiver
+     */
+    public void setChannelReceiver(ChannelReceiver receiver);
+    
+    /**
+     * Sets the membership service
+     * @param service MembershipService
+     * @see MembershipService
+     */
+    public void setMembershipService(MembershipService service);
+
+    /**
+     * returns the channel sender
+     * @return ChannelSender
+     * @see ChannelSender
+     */
+    public ChannelSender getChannelSender();
+    
+    /**
+     * returns the channel receiver
+     * @return ChannelReceiver
+     * @see ChannelReceiver
+     */
+    public ChannelReceiver getChannelReceiver();
+    
+    /**
+     * Returns the membership service
+     * @return MembershipService
+     * @see MembershipService
+     */
+    public MembershipService getMembershipService();
+
+    /**
+     * Returns the interceptor stack
+     * @return Iterator
+     * @see Channel#addInterceptor(ChannelInterceptor)
+     */
+    public Iterator getInterceptors();
+}
diff --git a/java/org/apache/catalina/tribes/Member.java b/java/org/apache/catalina/tribes/Member.java
new file mode 100644 (file)
index 0000000..622bab8
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+/**
+ * The Member interface, defines a member in the group.
+ * Each member can carry a set of properties, defined by the actual implementation.<BR>
+ * A member is identified by the host/ip/uniqueId<br>
+ * The host is what interface the member is listening to, to receive data<br>
+ * The port is what port the member is listening to, to receive data<br>
+ * The uniqueId defines the session id for the member. This is an important feature
+ * since a member that has crashed and the starts up again on the same port/host is 
+ * not guaranteed to be the same member, so no state transfers will ever be confused
+ * @author Filip Hanik
+ * @version $Revision: 303950 $, $Date: 2005-06-09 15:38:30 -0500 (Thu, 09 Jun 2005) $
+ */
+
+
+public interface Member {
+    
+    /**
+     * When a member leaves the cluster, the payload of the memberDisappeared member
+     * will be the following bytes. This indicates a soft shutdown, and not a crash
+     */
+    public static final byte[] SHUTDOWN_PAYLOAD = new byte[] {66, 65, 66, 89, 45, 65, 76, 69, 88};
+    
+    /**
+     * Returns the name of this node, should be unique within the group.
+     */
+    public String getName();
+  
+    /**
+     * Returns the listen host for the ChannelReceiver implementation
+     * @return IPv4 or IPv6 representation of the host address this member listens to incoming data
+     * @see ChannelReceiver
+     */
+    public byte[] getHost();
+
+    /**
+     * Returns the listen port for the ChannelReceiver implementation
+     * @return IPv4 or IPv6 representation of the host address this member listens to incoming data
+     * @see ChannelReceiver
+     */
+    public int getPort();
+
+    /**
+     * Contains information on how long this member has been online.
+     * The result is the number of milli seconds this member has been
+     * broadcasting its membership to the group.
+     * @return nr of milliseconds since this member started.
+     */
+    public long getMemberAliveTime();
+    
+    /**
+     * The current state of the member
+     * @return boolean - true if the member is functioning correctly
+     */
+    public boolean isReady();
+    /**
+     * The current state of the member
+     * @return boolean - true if the member is suspect, but the crash has not been confirmed
+     */
+    public boolean isSuspect();
+    
+    /**
+     * 
+     * @return boolean - true if the member has been confirmed to malfunction 
+     */
+    public boolean isFailing();
+    
+    /**
+     * returns a UUID unique for this member over all sessions.
+     * If the member crashes and restarts, the uniqueId will be different.
+     * @return byte[]
+     */
+    public byte[] getUniqueId();
+    
+    /**
+     * returns the payload associated with this member
+     * @return byte[]
+     */
+    public byte[] getPayload();
+    
+    /**
+     * returns the command associated with this member
+     * @return byte[]
+     */
+    public byte[] getCommand();
+    
+    /**
+     * Domain for this cluster
+     * @return byte[]
+     */
+    public byte[] getDomain();
+}
diff --git a/java/org/apache/catalina/tribes/MembershipListener.java b/java/org/apache/catalina/tribes/MembershipListener.java
new file mode 100644 (file)
index 0000000..d21b386
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+/**
+ * The MembershipListener interface is used as a callback to the
+ * membership service. It has two methods that will notify the listener
+ * when a member has joined the group and when a member has disappeared (crashed)
+ *
+ * @author Filip Hanik
+ * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $
+ */
+
+
+public interface MembershipListener {
+    /**
+     * A member was added to the group
+     * @param member Member - the member that was added
+     */
+    public void memberAdded(Member member);
+    
+    /**
+     * A member was removed from the group<br>
+     * If the member left voluntarily, the payload will contain the Member.SHUTDOWN_PAYLOAD data
+     * @param member Member
+     * @see Member#SHUTDOWN_PAYLOAD
+     */
+    public void memberDisappeared(Member member);
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/MembershipService.java b/java/org/apache/catalina/tribes/MembershipService.java
new file mode 100644 (file)
index 0000000..023cb83
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes;
+
+
+/**
+ * MembershipService Interface<br>
+ * The <code>MembershipService</code> interface is the membership component 
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).<br>
+ * @author Filip Hanik
+ * @version $Revision: 378093 $, $Date: 2006-02-15 15:13:45 -0600 (Wed, 15 Feb 2006) $
+ */
+
+
+public interface MembershipService {
+    
+    public static final int MBR_RX = Channel.MBR_RX_SEQ;
+    public static final int MBR_TX = Channel.MBR_TX_SEQ;
+    
+    /**
+     * Sets the properties for the membership service. This must be called before
+     * the <code>start()</code> method is called.
+     * The properties are implementation specific.
+     * @param properties - to be used to configure the membership service.
+     */
+    public void setProperties(java.util.Properties properties);
+    /**
+     * Returns the properties for the configuration used.
+     */
+    public java.util.Properties getProperties();
+    /**
+     * Starts the membership service. If a membership listeners is added
+     * the listener will start to receive membership events.
+     * Performs a start level 1 and 2
+     * @throws java.lang.Exception if the service fails to start.
+     */
+    public void start() throws java.lang.Exception;
+
+    /**
+     * Starts the membership service. If a membership listeners is added
+     * the listener will start to receive membership events.
+     * @param level - level MBR_RX starts listening for members, level MBR_TX 
+     * starts broad casting the server
+     * @throws java.lang.Exception if the service fails to start.
+     * @throws java.lang.IllegalArgumentException if the level is incorrect.
+     */
+    public void start(int level) throws java.lang.Exception;
+
+
+    /**
+     * Starts the membership service. If a membership listeners is added
+     * the listener will start to receive membership events.
+     * @param level - level MBR_RX stops listening for members, level MBR_TX 
+     * stops broad casting the server
+     * @throws java.lang.Exception if the service fails to stop
+     * @throws java.lang.IllegalArgumentException if the level is incorrect.
+     */
+
+    public void stop(int level);
+    
+    /**
+     * @return true if the the group contains members
+     */
+    public boolean hasMembers();
+    
+    
+    /**
+     * 
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr);
+    /**
+     * Returns a list of all the members in the cluster.
+     */
+    
+    public Member[] getMembers();
+    
+    /**
+     * Returns the member object that defines this member
+     */
+    public Member getLocalMember(boolean incAliveTime);
+
+    /**
+     * Return all members by name
+     */
+    public String[] getMembersByName() ; 
+    
+    /**
+     * Return the member by name
+     */
+    public Member findMemberByName(String name) ;
+
+    /**
+     * Sets the local member properties for broadcasting
+     */
+    public void setLocalMemberProperties(String listenHost, int listenPort);
+    
+    /**
+     * Sets the membership listener, only one listener can be added.
+     * If you call this method twice, the last listener will be used.
+     * @param listener The listener
+     */
+    public void setMembershipListener(MembershipListener listener);
+    
+    /**
+     * removes the membership listener.
+     */
+    public void removeMembershipListener();
+    
+    /**
+     * Set a payload to be broadcasted with each membership 
+     * broadcast.
+     * @param payload byte[]
+     */
+    public void setPayload(byte[] payload);
+    
+    public void setDomain(byte[] domain);
+
+}
diff --git a/java/org/apache/catalina/tribes/MessageListener.java b/java/org/apache/catalina/tribes/MessageListener.java
new file mode 100644 (file)
index 0000000..a6da94a
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+/**
+ * 
+ * <p>Title: MessageListener</p> 
+ * 
+ * <p>Description: The listener to be registered with the ChannelReceiver, internal Tribes component</p> 
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+
+public interface MessageListener {
+    
+    /**
+     * Receive a message from the IO components in the Channel stack
+     * @param msg ChannelMessage
+     */
+    public void messageReceived(ChannelMessage msg);
+    
+    public boolean accept(ChannelMessage msg);
+    
+    public boolean equals(Object listener);
+    
+    public int hashCode();
+
+}
diff --git a/java/org/apache/catalina/tribes/RemoteProcessException.java b/java/org/apache/catalina/tribes/RemoteProcessException.java
new file mode 100644 (file)
index 0000000..b83e5ed
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.catalina.tribes;
+
+/**
+ * <p>Title: RemoteProcessException</p>
+ *
+ * <p>Description: Message thrown by a sender when USE_SYNC_ACK receives a FAIL_ACK_COMMAND.<br>
+ * This means that the message was received on the remote node but the processing of the message failed.
+ * This message will be embedded in a ChannelException.FaultyMember
+ * </p>
+ * @see ChannelException
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class RemoteProcessException extends RuntimeException {
+    public RemoteProcessException() {
+        super();
+    }
+
+    public RemoteProcessException(String message) {
+        super(message);
+    }
+
+    public RemoteProcessException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public RemoteProcessException(Throwable cause) {
+        super(cause);
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/UniqueId.java b/java/org/apache/catalina/tribes/UniqueId.java
new file mode 100644 (file)
index 0000000..3e58527
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes;
+
+import org.apache.catalina.tribes.util.Arrays;
+import java.io.Serializable;
+
+/**
+ * <p>Title: Represents a globabally unique Id</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public final class UniqueId implements Serializable{
+    protected byte[] id;
+    
+    public UniqueId() {
+    }
+
+    public UniqueId(byte[] id) {
+        this.id = id;
+    }
+    
+    public UniqueId(byte[] id, int offset, int length) {
+        this.id = new byte[length];
+        System.arraycopy(id,offset,this.id,0,length);
+    }
+    
+    public int hashCode() {
+        if ( id == null ) return 0;
+        return Arrays.hashCode(id);
+    }
+    
+    public boolean equals(Object other) {
+        boolean result = (other instanceof UniqueId);
+        if ( result ) {
+            UniqueId uid = (UniqueId)other;
+            if ( this.id == null && uid.id == null ) result = true;
+            else if ( this.id == null && uid.id != null ) result = false;
+            else if ( this.id != null && uid.id == null ) result = false;
+            else result = Arrays.equals(this.id,uid.id);
+        }//end if
+        return result;
+    }
+    
+    public byte[] getBytes() {
+        return id;
+    }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer("UniqueId");
+        buf.append(org.apache.catalina.tribes.util.Arrays.toString(id));
+        return buf.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/AbsoluteOrder.java b/java/org/apache/catalina/tribes/group/AbsoluteOrder.java
new file mode 100644 (file)
index 0000000..1542ff7
--- /dev/null
@@ -0,0 +1,100 @@
+package org.apache.catalina.tribes.group;
+
+import org.apache.catalina.tribes.Member;
+import java.util.Comparator;
+import java.util.Arrays;
+
+/**
+ * <p>Title: Membership - Absolute Order</p>
+ *
+ * <p>Description: A simple, yet agreeable and efficient way of ordering members</p>
+ * <p>
+ *    Ordering members can serve as a basis for electing a leader or coordinating efforts.<br>
+ *    This is stinky simple, it works on the basis of the <code>Member</code> interface
+ *    and orders members in the following format:
+ * 
+ *  <ol>
+ *     <li>IP comparison - byte by byte, lower byte higher rank</li>
+ *     <li>IPv4 addresses rank higher than IPv6, ie the lesser number of bytes, the higher rank</li>
+ *     <li>Port comparison - lower port, higher rank</li>
+ *     <li>UniqueId comparison- byte by byte, lower byte higher rank</li>
+ *  </ol>
+ *     
+ * </p>
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ * @see org.apache.catalina.tribes.Member
+ */
+public class AbsoluteOrder {
+    public static final AbsoluteComparator comp = new AbsoluteComparator();
+    
+    protected AbsoluteOrder() {
+        super();
+    }
+
+    
+    
+    public static void absoluteOrder(Member[] members) {
+        if ( members == null || members.length == 0 ) return;
+        Arrays.sort(members,comp);
+    }
+    
+    
+    public static class AbsoluteComparator implements Comparator {
+        public int compare(Object o1, Object o2) {
+            if ( !((o1 instanceof Member) && (o2 instanceof Member)) ) return 0;
+            return compareMembers((Member)o1,(Member)o2);
+        }
+        
+        public int compareMembers(Member m1, Member m2) {
+            int result = compareIps(m1,m2);
+            if ( result == 0 ) result = comparePorts(m1,m2);
+            if ( result == 0 ) result = compareIds(m1,m2);
+            return result;
+        }
+        
+        public int compareIps(Member m1, Member m2) {
+            return compareBytes(m1.getHost(),m2.getHost());
+        }
+        
+        public int comparePorts(Member m1, Member m2) {
+            return compareInts(m1.getPort(),m2.getPort());
+        }
+        
+        public int compareIds(Member m1, Member m2) {
+            return compareBytes(m1.getUniqueId(),m2.getUniqueId());
+        }
+        
+        protected int compareBytes(byte[] d1, byte[] d2) {
+            int result = 0;
+            if ( d1.length == d2.length ) {
+                for (int i=0; (result==0) && (i<d1.length); i++) {
+                    result = compareBytes(d1[i],d2[i]);
+                }
+            } else if ( d1.length < d2.length) {
+                result = -1;
+            } else {
+                result = 1;
+            }
+            return result;
+        }
+        
+        protected int compareBytes(byte b1, byte b2) {
+            return compareInts((int)b1,(int)b2);
+        }
+        
+        protected int compareInts(int b1, int b2) {
+            int result = 0;
+            if ( b1 == b2 ) {
+
+            } else if ( b1 < b2) {
+                result = -1;
+            } else {
+                result = 1;
+            }
+            return result;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/ChannelCoordinator.java b/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
new file mode 100644 (file)
index 0000000..8cb6b49
--- /dev/null
@@ -0,0 +1,317 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ChannelReceiver;
+import org.apache.catalina.tribes.ChannelSender;
+
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipService;
+import org.apache.catalina.tribes.MessageListener;
+import org.apache.catalina.tribes.transport.SenderState;
+import org.apache.catalina.tribes.transport.ReplicationTransmitter;
+import org.apache.catalina.tribes.membership.McastService;
+import org.apache.catalina.tribes.transport.nio.NioReceiver;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.util.Logs;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.util.Arrays;
+
+
+/**
+ * The channel coordinator object coordinates the membership service,
+ * the sender and the receiver.
+ * This is the last interceptor in the chain.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public class ChannelCoordinator extends ChannelInterceptorBase implements MessageListener {
+    private ChannelReceiver clusterReceiver = new NioReceiver();
+    private ChannelSender clusterSender = new ReplicationTransmitter();
+    private MembershipService membershipService = new McastService();
+    
+    //override optionflag
+    protected int optionFlag = Channel.SEND_OPTIONS_BYTE_MESSAGE|Channel.SEND_OPTIONS_USE_ACK|Channel.SEND_OPTIONS_SYNCHRONIZED_ACK;
+    public int getOptionFlag() {return optionFlag;}
+    public void setOptionFlag(int flag) {optionFlag=flag;}
+    
+    private int startLevel = 0;
+
+    public ChannelCoordinator() {
+        
+    }
+    
+    public ChannelCoordinator(ChannelReceiver receiver,
+                              ChannelSender sender,
+                              MembershipService service) {
+        this();
+        this.setClusterReceiver(receiver);
+        this.setClusterSender(sender);
+        this.setMembershipService(service);
+    }
+    
+    /**
+     * Send a message to one or more members in the cluster
+     * @param destination Member[] - the destinations, null or zero length means all
+     * @param msg ClusterMessage - the message to send
+     * @param options int - sender options, see class documentation
+     * @return ClusterMessage[] - the replies from the members, if any.
+     */
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        if ( destination == null ) destination = membershipService.getMembers();
+        clusterSender.sendMessage(msg,destination);
+        if ( Logs.MESSAGES.isTraceEnabled() ) {
+            Logs.MESSAGES.trace("ChannelCoordinator - Sent msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " to "+Arrays.toNameString(destination));
+        }
+    }
+    
+
+    /**
+     * Starts up the channel. This can be called multiple times for individual services to start
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will start all services <BR>
+     * MBR_RX_SEQ - starts the membership receiver <BR>
+     * MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * SND_TX_SEQ - starts the replication transmitter<BR>
+     * SND_RX_SEQ - starts the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    public void start(int svc) throws ChannelException {
+        this.internalStart(svc);
+    }
+
+    /**
+     * Shuts down the channel. This can be called multiple times for individual services to shutdown
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will shutdown all services <BR>
+     * MBR_RX_SEQ - stops the membership receiver <BR>
+     * MBR_TX_SEQ - stops the membership broadcaster <BR>
+     * SND_TX_SEQ - stops the replication transmitter<BR>
+     * SND_RX_SEQ - stops the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    public void stop(int svc) throws ChannelException {
+        this.internalStop(svc);
+    }    
+
+
+    /**
+     * Starts up the channel. This can be called multiple times for individual services to start
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will start all services <BR>
+     * MBR_RX_SEQ - starts the membership receiver <BR>
+     * MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * SND_TX_SEQ - starts the replication transmitter<BR>
+     * SND_RX_SEQ - starts the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    protected synchronized void internalStart(int svc) throws ChannelException {
+        try {
+            boolean valid = false;
+            //make sure we don't pass down any flags that are unrelated to the bottom layer
+            svc = svc & Channel.DEFAULT;
+
+            if (startLevel == Channel.DEFAULT) return; //we have already started up all components
+            if (svc == 0 ) return;//nothing to start
+            
+            if (svc == (svc & startLevel)) throw new ChannelException("Channel already started for level:"+svc);
+
+            //must start the receiver first so that we can coordinate the port it
+            //listens to with the local membership settings
+            if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) {
+                clusterReceiver.setMessageListener(this);
+                clusterReceiver.start();
+                //synchronize, big time FIXME
+                membershipService.setLocalMemberProperties(getClusterReceiver().getHost(), getClusterReceiver().getPort());
+                valid = true;
+            }
+            if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) {
+                clusterSender.start();
+                valid = true;
+            }
+            
+            if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) {
+                membershipService.setMembershipListener(this);
+                membershipService.start(MembershipService.MBR_RX);
+                valid = true;
+            }
+            if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) {
+                membershipService.start(MembershipService.MBR_TX);
+                valid = true;
+            }
+            
+            if ( !valid) {
+                throw new IllegalArgumentException("Invalid start level, valid levels are:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ");
+            }
+            startLevel = (startLevel | svc);
+        }catch ( ChannelException cx ) {
+            throw cx;
+        }catch ( Exception x ) {
+            throw new ChannelException(x);
+        }
+    }
+
+    /**
+     * Shuts down the channel. This can be called multiple times for individual services to shutdown
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will shutdown all services <BR>
+     * MBR_RX_SEQ - starts the membership receiver <BR>
+     * MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * SND_TX_SEQ - starts the replication transmitter<BR>
+     * SND_RX_SEQ - starts the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    protected synchronized void internalStop(int svc) throws ChannelException {
+        try {
+            //make sure we don't pass down any flags that are unrelated to the bottom layer
+            svc = svc & Channel.DEFAULT;
+
+            if (startLevel == 0) return; //we have already stopped up all components
+            if (svc == 0 ) return;//nothing to stop
+
+            boolean valid = false;
+            if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) {
+                clusterReceiver.stop();
+                clusterReceiver.setMessageListener(null);
+                valid = true;
+            }
+            if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) {
+                clusterSender.stop();
+                valid = true;
+            }
+
+            if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) {
+                membershipService.stop(MembershipService.MBR_RX);
+                membershipService.setMembershipListener(null);
+                valid = true;
+                
+            }
+            if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) {
+                valid = true;
+                membershipService.stop(MembershipService.MBR_TX);
+            }            
+            if ( !valid) {
+                throw new IllegalArgumentException("Invalid start level, valid levels are:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ");
+            }
+
+            startLevel = (startLevel & (~svc));
+            
+        }catch ( Exception x ) {
+            throw new ChannelException(x);
+        } finally {
+            
+        }
+
+    }
+    
+    public void memberAdded(Member member){
+        SenderState.getSenderState(member);
+        if ( clusterSender!=null ) clusterSender.add(member);
+        super.memberAdded(member);
+    }
+    
+    public void memberDisappeared(Member member){
+        SenderState.removeSenderState(member);
+        if ( clusterSender!=null ) clusterSender.remove(member);
+        super.memberDisappeared(member);
+    }
+    
+    public void messageReceived(ChannelMessage msg) {
+        if ( Logs.MESSAGES.isTraceEnabled() ) {
+            Logs.MESSAGES.trace("ChannelCoordinator - Received msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " from "+msg.getAddress().getName());
+        }
+        super.messageReceived(msg);
+    }
+
+
+    public ChannelReceiver getClusterReceiver() {
+        return clusterReceiver;
+    }
+
+    public ChannelSender getClusterSender() {
+        return clusterSender;
+    }
+
+    public MembershipService getMembershipService() {
+        return membershipService;
+    }
+
+    public void setClusterReceiver(ChannelReceiver clusterReceiver) {
+        if ( clusterReceiver != null ) {
+            this.clusterReceiver = clusterReceiver;
+            this.clusterReceiver.setMessageListener(this);
+        } else {
+            if  (this.clusterReceiver!=null ) this.clusterReceiver.setMessageListener(null);
+            this.clusterReceiver = null;
+        }
+    }
+
+    public void setClusterSender(ChannelSender clusterSender) {
+        this.clusterSender = clusterSender;
+    }
+
+    public void setMembershipService(MembershipService membershipService) {
+        this.membershipService = membershipService;
+        this.membershipService.setMembershipListener(this);
+    }
+    
+    public void hearbeat() {
+        if ( clusterSender!=null ) clusterSender.heartbeat();
+        super.heartbeat();
+    }
+    
+    /**
+     * has members
+     */
+    public boolean hasMembers() {
+        return this.getMembershipService().hasMembers();
+    }
+
+    /**
+     * Get all current cluster members
+     * @return all members or empty array
+     */
+    public Member[] getMembers() {
+        return this.getMembershipService().getMembers();
+    }
+
+    /**
+     * 
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr){
+        return this.getMembershipService().getMember(mbr);
+    }
+
+
+    /**
+     * Return the member that represents this node.
+     *
+     * @return Member
+     */
+    public Member getLocalMember(boolean incAlive) {
+        return this.getMembershipService().getLocalMember(incAlive);
+    }
+
+   
+}
diff --git a/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java
new file mode 100644 (file)
index 0000000..e67199c
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+
+/**
+ * Abstract class for the interceptor base class.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public abstract class ChannelInterceptorBase implements ChannelInterceptor {
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(
+        ChannelInterceptorBase.class);
+
+    private ChannelInterceptor next;
+    private ChannelInterceptor previous;
+    //default value, always process
+    protected int optionFlag = 0;
+
+    public ChannelInterceptorBase() {
+
+    }
+    
+    public boolean okToProcess(int messageFlags) { 
+        if (this.optionFlag == 0 ) return true;
+        return ((optionFlag&messageFlags) == optionFlag);
+    }
+
+    public final void setNext(ChannelInterceptor next) {
+        this.next = next;
+    }
+
+    public final ChannelInterceptor getNext() {
+        return next;
+    }
+
+    public final void setPrevious(ChannelInterceptor previous) {
+        this.previous = previous;
+    }
+
+    public void setOptionFlag(int optionFlag) {
+        this.optionFlag = optionFlag;
+    }
+
+    public final ChannelInterceptor getPrevious() {
+        return previous;
+    }
+
+    public int getOptionFlag() {
+        return optionFlag;
+    }
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws
+        ChannelException {
+        if (getNext() != null) getNext().sendMessage(destination, msg, payload);
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        if (getPrevious() != null) getPrevious().messageReceived(msg);
+    }
+
+    public boolean accept(ChannelMessage msg) {
+        return true;
+    }
+
+    public void memberAdded(Member member) {
+        //notify upwards
+        if (getPrevious() != null) getPrevious().memberAdded(member);
+    }
+
+    public void memberDisappeared(Member member) {
+        //notify upwards
+        if (getPrevious() != null) getPrevious().memberDisappeared(member);
+    }
+
+    public void heartbeat() {
+        if (getNext() != null) getNext().heartbeat();
+    }
+
+    /**
+     * has members
+     */
+    public boolean hasMembers() {
+        if ( getNext()!=null )return getNext().hasMembers();
+        else return false;
+    }
+
+    /**
+     * Get all current cluster members
+     * @return all members or empty array
+     */
+    public Member[] getMembers() {
+        if ( getNext()!=null ) return getNext().getMembers();
+        else return null;
+    }
+
+    /**
+     *
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr) {
+        if ( getNext()!=null) return getNext().getMember(mbr);
+        else return null;
+    }
+
+    /**
+     * Return the member that represents this node.
+     *
+     * @return Member
+     */
+    public Member getLocalMember(boolean incAlive) {
+        if ( getNext()!=null ) return getNext().getLocalMember(incAlive);
+        else return null;
+    }
+    
+    /**
+     * Starts up the channel. This can be called multiple times for individual services to start
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will start all services <BR>
+     * MBR_RX_SEQ - starts the membership receiver <BR>
+     * MBR_TX_SEQ - starts the membership broadcaster <BR>
+     * SND_TX_SEQ - starts the replication transmitter<BR>
+     * SND_RX_SEQ - starts the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    public void start(int svc) throws ChannelException {
+        if ( getNext()!=null ) getNext().start(svc);
+    }
+
+    /**
+     * Shuts down the channel. This can be called multiple times for individual services to shutdown
+     * The svc parameter can be the logical or value of any constants
+     * @param svc int value of <BR>
+     * DEFAULT - will shutdown all services <BR>
+     * MBR_RX_SEQ - stops the membership receiver <BR>
+     * MBR_TX_SEQ - stops the membership broadcaster <BR>
+     * SND_TX_SEQ - stops the replication transmitter<BR>
+     * SND_RX_SEQ - stops the replication receiver<BR>
+     * @throws ChannelException if a startup error occurs or the service is already started.
+     */
+    public void stop(int svc) throws ChannelException {
+        if (getNext() != null) getNext().stop(svc);
+    }
+    
+    public void fireInterceptorEvent(InterceptorEvent event) {
+        //empty operation
+    }
+
+
+}
diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java b/java/org/apache/catalina/tribes/group/GroupChannel.java
new file mode 100644 (file)
index 0000000..ff4a08e
--- /dev/null
@@ -0,0 +1,666 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.apache.catalina.tribes.ByteMessage;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ChannelReceiver;
+import org.apache.catalina.tribes.ChannelSender;
+import org.apache.catalina.tribes.ErrorHandler;
+import org.apache.catalina.tribes.ManagedChannel;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.MembershipService;
+import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.Heartbeat;
+import org.apache.catalina.tribes.io.BufferPool;
+import java.io.IOException;
+import org.apache.catalina.tribes.RemoteProcessException;
+import org.apache.catalina.tribes.util.Logs;
+import org.apache.catalina.tribes.util.Arrays;
+
+/**
+ * The default implementation of a Channel.<br>
+ * The GroupChannel manages the replication channel. It coordinates
+ * message being sent and received with membership announcements.
+ * The channel has an chain of interceptors that can modify the message or perform other logic.<br>
+ * It manages a complete group, both membership and replication.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public class GroupChannel extends ChannelInterceptorBase implements ManagedChannel {
+    /**
+     * Flag to determine if the channel manages its own heartbeat
+     * If set to true, the channel will start a local thread for the heart beat.
+     */
+    protected boolean heartbeat = true;
+    /**
+     * If <code>heartbeat == true</code> then how often do we want this
+     * heartbeat to run. default is one minute
+     */
+    protected long heartbeatSleeptime = 5*1000;//every 5 seconds
+
+    /**
+     * Internal heartbeat thread
+     */
+    protected HeartbeatThread hbthread = null;
+
+    /**
+     * The  <code>ChannelCoordinator</code> coordinates the bottom layer components:<br>
+     * - MembershipService<br>
+     * - ChannelSender <br>
+     * - ChannelReceiver<br>
+     */
+    protected ChannelCoordinator coordinator = new ChannelCoordinator();
+
+    /**
+     * The first interceptor in the inteceptor stack.
+     * The interceptors are chained in a linked list, so we only need a reference to the
+     * first one
+     */
+    protected ChannelInterceptor interceptors = null;
+
+    /**
+     * A list of membership listeners that subscribe to membership announcements
+     */
+    protected ArrayList membershipListeners = new ArrayList();
+
+    /**
+     * A list of channel listeners that subscribe to incoming messages
+     */
+    protected ArrayList channelListeners = new ArrayList();
+
+    /**
+     * If set to true, the GroupChannel will check to make sure that
+     */
+    protected boolean optionCheck = false;
+
+    /**
+     * Creates a GroupChannel. This constructor will also
+     * add the first interceptor in the GroupChannel.<br>
+     * The first interceptor is always the channel itself.
+     */
+    public GroupChannel() {
+        addInterceptor(this);
+    }
+
+
+    /**
+     * Adds an interceptor to the stack for message processing<br>
+     * Interceptors are ordered in the way they are added.<br>
+     * <code>channel.addInterceptor(A);</code><br>
+     * <code>channel.addInterceptor(C);</code><br>
+     * <code>channel.addInterceptor(B);</code><br>
+     * Will result in a interceptor stack like this:<br>
+     * <code>A -> C -> B</code><br>
+     * The complete stack will look like this:<br>
+     * <code>Channel -> A -> C -> B -> ChannelCoordinator</code><br>
+     * @param interceptor ChannelInterceptorBase
+     */
+    public void addInterceptor(ChannelInterceptor interceptor) {
+        if ( interceptors == null ) {
+            interceptors = interceptor;
+            interceptors.setNext(coordinator);
+            interceptors.setPrevious(null);
+            coordinator.setPrevious(interceptors);
+        } else {
+            ChannelInterceptor last = interceptors;
+            while ( last.getNext() != coordinator ) {
+                last = last.getNext();
+            }
+            last.setNext(interceptor);
+            interceptor.setNext(coordinator);
+            interceptor.setPrevious(last);
+            coordinator.setPrevious(interceptor);
+        }
+    }
+
+    /**
+     * Sends a heartbeat through the interceptor stack.<br>
+     * Invoke this method from the application on a periodic basis if
+     * you have turned off internal heartbeats <code>channel.setHeartbeat(false)</code>
+     */
+    public void heartbeat() {
+        super.heartbeat();
+        Iterator i = membershipListeners.iterator();
+        while ( i.hasNext() ) {
+            Object o = i.next();
+            if ( o instanceof Heartbeat ) ((Heartbeat)o).heartbeat();
+        }
+        i = channelListeners.iterator();
+        while ( i.hasNext() ) {
+            Object o = i.next();
+            if ( o instanceof Heartbeat ) ((Heartbeat)o).heartbeat();
+        }
+
+    }
+
+
+    /**
+     * Send a message to the destinations specified
+     * @param destination Member[] - destination.length > 1
+     * @param msg Serializable - the message to send
+     * @param options int - sender options, options can trigger guarantee levels and different interceptors to
+     * react to the message see class documentation for the <code>Channel</code> object.<br>
+     * @return UniqueId - the unique Id that was assigned to this message
+     * @throws ChannelException - if an error occurs processing the message
+     * @see org.apache.catalina.tribes.Channel
+     */
+    public UniqueId send(Member[] destination, Serializable msg, int options) throws ChannelException {
+        return send(destination,msg,options,null);
+    }
+
+    /**
+     *
+     * @param destination Member[] - destination.length > 1
+     * @param msg Serializable - the message to send
+     * @param options int - sender options, options can trigger guarantee levels and different interceptors to
+     * react to the message see class documentation for the <code>Channel</code> object.<br>
+     * @param handler - callback object for error handling and completion notification, used when a message is
+     * sent asynchronously using the <code>Channel.SEND_OPTIONS_ASYNCHRONOUS</code> flag enabled.
+     * @return UniqueId - the unique Id that was assigned to this message
+     * @throws ChannelException - if an error occurs processing the message
+     * @see org.apache.catalina.tribes.Channel
+     */
+    public UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) throws ChannelException {
+        if ( msg == null ) throw new ChannelException("Cant send a NULL message");
+        XByteBuffer buffer = null;
+        try {
+            if ( destination == null || destination.length == 0) throw new ChannelException("No destination given");
+            ChannelData data = new ChannelData(true);//generates a unique Id
+            data.setAddress(getLocalMember(false));
+            data.setTimestamp(System.currentTimeMillis());
+            byte[] b = null;
+            if ( msg instanceof ByteMessage ){
+                b = ((ByteMessage)msg).getMessage();
+                options = options | SEND_OPTIONS_BYTE_MESSAGE;
+            } else {
+                b = XByteBuffer.serialize(msg);
+                options = options & (~SEND_OPTIONS_BYTE_MESSAGE);
+            }
+            data.setOptions(options);
+            //XByteBuffer buffer = new XByteBuffer(b.length+128,false);
+            buffer = BufferPool.getBufferPool().getBuffer(b.length+128, false);
+            buffer.append(b,0,b.length);
+            data.setMessage(buffer);
+            InterceptorPayload payload = null;
+            if ( handler != null ) {
+                payload = new InterceptorPayload();
+                payload.setErrorHandler(handler);
+            }
+            getFirstInterceptor().sendMessage(destination, data, payload);
+            if ( Logs.MESSAGES.isTraceEnabled() ) {
+                Logs.MESSAGES.trace("GroupChannel - Sent msg:" + new UniqueId(data.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " to "+Arrays.toNameString(destination));
+                Logs.MESSAGES.trace("GroupChannel - Send Message:" + new UniqueId(data.getUniqueId()) + " is " +msg);
+            }
+
+            return new UniqueId(data.getUniqueId());
+        }catch ( Exception x ) {
+            if ( x instanceof ChannelException ) throw (ChannelException)x;
+            throw new ChannelException(x);
+        } finally {
+            if ( buffer != null ) BufferPool.getBufferPool().returnBuffer(buffer);
+        }
+    }
+
+
+    /**
+     * Callback from the interceptor stack. <br>
+     * When a message is received from a remote node, this method will be invoked by
+     * the previous interceptor.<br>
+     * This method can also be used to send a message to other components within the same application,
+     * but its an extreme case, and you're probably better off doing that logic between the applications itself.
+     * @param msg ChannelMessage
+     */
+    public void messageReceived(ChannelMessage msg) {
+        if ( msg == null ) return;
+        try {
+            if ( Logs.MESSAGES.isTraceEnabled() ) {
+                Logs.MESSAGES.trace("GroupChannel - Received msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " from "+msg.getAddress().getName());
+            }
+
+            Serializable fwd = null;
+            if ( (msg.getOptions() & SEND_OPTIONS_BYTE_MESSAGE) == SEND_OPTIONS_BYTE_MESSAGE ) {
+                fwd = new ByteMessage(msg.getMessage().getBytes());
+            } else {
+                fwd = XByteBuffer.deserialize(msg.getMessage().getBytesDirect(),0,msg.getMessage().getLength());
+            }
+            if ( Logs.MESSAGES.isTraceEnabled() ) {
+                Logs.MESSAGES.trace("GroupChannel - Receive Message:" + new UniqueId(msg.getUniqueId()) + " is " +fwd);
+            }
+
+            //get the actual member with the correct alive time
+            Member source = msg.getAddress();
+            boolean rx = false;
+            boolean delivered = false;
+            for ( int i=0; i<channelListeners.size(); i++ ) {
+                ChannelListener channelListener = (ChannelListener)channelListeners.get(i);
+                if (channelListener != null && channelListener.accept(fwd, source)) {
+                    channelListener.messageReceived(fwd, source);
+                    delivered = true;
+                    //if the message was accepted by an RPC channel, that channel
+                    //is responsible for returning the reply, otherwise we send an absence reply
+                    if ( channelListener instanceof RpcChannel ) rx = true;
+                }
+            }//for
+            if ((!rx) && (fwd instanceof RpcMessage)) {
+                //if we have a message that requires a response,
+                //but none was given, send back an immediate one
+                sendNoRpcChannelReply((RpcMessage)fwd,source);
+            }
+            if ( Logs.MESSAGES.isTraceEnabled() ) {
+                Logs.MESSAGES.trace("GroupChannel delivered["+delivered+"] id:"+new UniqueId(msg.getUniqueId()));
+            }
+
+        } catch ( Exception x ) {
+            if ( log.isDebugEnabled() ) log.error("Unable to process channel:IOException.",x);
+            throw new RemoteProcessException("IOException:"+x.getMessage(),x);
+        }
+    }
+
+    /**
+     * Sends a <code>NoRpcChannelReply</code> message to a member<br>
+     * This method gets invoked by the channel if a RPC message comes in
+     * and no channel listener accepts the message. This avoids timeout
+     * @param msg RpcMessage
+     * @param destination Member - the destination for the reply
+     */
+    protected void sendNoRpcChannelReply(RpcMessage msg, Member destination) {
+        try {
+            //avoid circular loop
+            if ( msg instanceof RpcMessage.NoRpcChannelReply) return;
+            RpcMessage.NoRpcChannelReply reply = new RpcMessage.NoRpcChannelReply(msg.rpcId,msg.uuid);
+            send(new Member[]{destination},reply,Channel.SEND_OPTIONS_ASYNCHRONOUS);
+        } catch ( Exception x ) {
+            log.error("Unable to find rpc channel, failed to send NoRpcChannelReply.",x);
+        }
+    }
+
+    /**
+     * memberAdded gets invoked by the interceptor below the channel
+     * and the channel will broadcast it to the membership listeners
+     * @param member Member - the new member
+     */
+    public void memberAdded(Member member) {
+        //notify upwards
+        for (int i=0; i<membershipListeners.size(); i++ ) {
+            MembershipListener membershipListener = (MembershipListener)membershipListeners.get(i);
+            if (membershipListener != null) membershipListener.memberAdded(member);
+        }
+    }
+
+    /**
+     * memberDisappeared gets invoked by the interceptor below the channel
+     * and the channel will broadcast it to the membership listeners
+     * @param member Member - the member that left or crashed
+     */
+    public void memberDisappeared(Member member) {
+        //notify upwards
+        for (int i=0; i<membershipListeners.size(); i++ ) {
+            MembershipListener membershipListener = (MembershipListener)membershipListeners.get(i);
+            if (membershipListener != null) membershipListener.memberDisappeared(member);
+        }
+    }
+
+    /**
+     * Sets up the default implementation interceptor stack
+     * if no interceptors have been added
+     * @throws ChannelException
+     */
+    protected synchronized void setupDefaultStack() throws ChannelException {
+
+        if ( getFirstInterceptor() != null &&
+             ((getFirstInterceptor().getNext() instanceof ChannelCoordinator))) {
+            ChannelInterceptor interceptor = null;
+            Class clazz = null;
+            try {
+                clazz = Class.forName("org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor",
+                                      true,GroupChannel.class.getClassLoader());
+                clazz.newInstance();
+            } catch ( Throwable x ) {
+                clazz = MessageDispatchInterceptor.class;
+            }//catch
+            try {
+                interceptor = (ChannelInterceptor) clazz.newInstance();
+            } catch (Exception x) {
+                throw new ChannelException("Unable to add MessageDispatchInterceptor to interceptor chain.",x);
+            }
+            this.addInterceptor(interceptor);
+        }
+    }
+
+    /**
+     * Validates the option flags that each interceptor is using and reports
+     * an error if two interceptor share the same flag.
+     * @throws ChannelException
+     */
+    protected void checkOptionFlags() throws ChannelException {
+        StringBuffer conflicts = new StringBuffer();
+        ChannelInterceptor first = interceptors;
+        while ( first != null ) {
+            int flag = first.getOptionFlag();
+            if ( flag != 0 ) {
+                ChannelInterceptor next = first.getNext();
+                while ( next != null ) {
+                    int nflag = next.getOptionFlag();
+                    if (nflag!=0 && (((flag & nflag) == flag ) || ((flag & nflag) == nflag)) ) {
+                        conflicts.append("[");
+                        conflicts.append(first.getClass().getName());
+                        conflicts.append(":");
+                        conflicts.append(flag);
+                        conflicts.append(" == ");
+                        conflicts.append(next.getClass().getName());
+                        conflicts.append(":");
+                        conflicts.append(nflag);
+                        conflicts.append("] ");
+                    }//end if
+                    next = next.getNext();
+                }//while
+            }//end if
+            first = first.getNext();
+        }//while
+        if ( conflicts.length() > 0 ) throw new ChannelException("Interceptor option flag conflict: "+conflicts.toString());
+
+    }
+
+    /**
+     * Starts the channel
+     * @param svc int - what service to start
+     * @throws ChannelException
+     * @see org.apache.catalina.tribes.Channel#start(int)
+     */
+    public synchronized void start(int svc) throws ChannelException {
+        setupDefaultStack();
+        if (optionCheck) checkOptionFlags();
+        super.start(svc);
+        if ( hbthread == null && heartbeat ) {
+            hbthread = new HeartbeatThread(this,heartbeatSleeptime);
+            hbthread.start();
+        }
+    }
+
+    /**
+     * Stops the channel
+     * @param svc int
+     * @throws ChannelException
+     * @see org.apache.catalina.tribes.Channel#stop(int)
+     */
+    public synchronized void stop(int svc) throws ChannelException {
+        if (hbthread != null) {
+            hbthread.stopHeartbeat();
+            hbthread = null;
+        }
+        super.stop(svc);
+    }
+
+    /**
+     * Returns the first interceptor of the stack. Useful for traversal.
+     * @return ChannelInterceptor
+     */
+    public ChannelInterceptor getFirstInterceptor() {
+        if (interceptors != null) return interceptors;
+        else return coordinator;
+    }
+
+    /**
+     * Returns the channel receiver component
+     * @return ChannelReceiver
+     */
+    public ChannelReceiver getChannelReceiver() {
+        return coordinator.getClusterReceiver();
+    }
+
+    /**
+     * Returns the channel sender component
+     * @return ChannelSender
+     */
+    public ChannelSender getChannelSender() {
+        return coordinator.getClusterSender();
+    }
+
+    /**
+     * Returns the membership service component
+     * @return MembershipService
+     */
+    public MembershipService getMembershipService() {
+        return coordinator.getMembershipService();
+    }
+
+    /**
+     * Sets the channel receiver component
+     * @param clusterReceiver ChannelReceiver
+     */
+    public void setChannelReceiver(ChannelReceiver clusterReceiver) {
+        coordinator.setClusterReceiver(clusterReceiver);
+    }
+
+    /**
+     * Sets the channel sender component
+     * @param clusterSender ChannelSender
+     */
+    public void setChannelSender(ChannelSender clusterSender) {
+        coordinator.setClusterSender(clusterSender);
+    }
+
+    /**
+     * Sets the membership component
+     * @param membershipService MembershipService
+     */
+    public void setMembershipService(MembershipService membershipService) {
+        coordinator.setMembershipService(membershipService);
+    }
+
+    /**
+     * Adds a membership listener to the channel.<br>
+     * Membership listeners are uniquely identified using the equals(Object) method
+     * @param membershipListener MembershipListener
+     */
+    public void addMembershipListener(MembershipListener membershipListener) {
+        if (!this.membershipListeners.contains(membershipListener) )
+            this.membershipListeners.add(membershipListener);
+    }
+
+    /**
+     * Removes a membership listener from the channel.<br>
+     * Membership listeners are uniquely identified using the equals(Object) method
+     * @param membershipListener MembershipListener
+     */
+
+    public void removeMembershipListener(MembershipListener membershipListener) {
+        membershipListeners.remove(membershipListener);
+    }
+
+    /**
+     * Adds a channel listener to the channel.<br>
+     * Channel listeners are uniquely identified using the equals(Object) method
+     * @param channelListener ChannelListener
+     */
+    public void addChannelListener(ChannelListener channelListener) {
+        if (!this.channelListeners.contains(channelListener) ) {
+            this.channelListeners.add(channelListener);
+        } else {
+            throw new IllegalArgumentException("Listener already exists:"+channelListener);
+        }
+    }
+
+    /**
+     *
+     * Removes a channel listener from the channel.<br>
+     * Channel listeners are uniquely identified using the equals(Object) method
+     * @param channelListener ChannelListener
+     */
+    public void removeChannelListener(ChannelListener channelListener) {
+        channelListeners.remove(channelListener);
+    }
+
+    /**
+     * Returns an iterator of all the interceptors in this stack
+     * @return Iterator
+     */
+    public Iterator getInterceptors() {
+        return new InterceptorIterator(this.getNext(),this.coordinator);
+    }
+
+    /**
+     * Enables/disables the option check<br>
+     * Setting this to true, will make the GroupChannel perform a conflict check
+     * on the interceptors. If two interceptors are using the same option flag
+     * and throw an error upon start.
+     * @param optionCheck boolean
+     */
+    public void setOptionCheck(boolean optionCheck) {
+        this.optionCheck = optionCheck;
+    }
+
+    /**
+     * Configure local heartbeat sleep time<br>
+     * Only used when <code>getHeartbeat()==true</code>
+     * @param heartbeatSleeptime long - time in milliseconds to sleep between heartbeats
+     */
+    public void setHeartbeatSleeptime(long heartbeatSleeptime) {
+        this.heartbeatSleeptime = heartbeatSleeptime;
+    }
+
+    /**
+     * Enables or disables local heartbeat.
+     * if <code>setHeartbeat(true)</code> is invoked then the channel will start an internal
+     * thread to invoke <code>Channel.heartbeat()</code> every <code>getHeartbeatSleeptime</code> milliseconds
+     * @param heartbeat boolean
+     */
+    public void setHeartbeat(boolean heartbeat) {
+        this.heartbeat = heartbeat;
+    }
+
+    /**
+     * @see #setOptionCheck(boolean)
+     * @return boolean
+     */
+    public boolean getOptionCheck() {
+        return optionCheck;
+    }
+
+    /**
+     * @see #setHeartbeat(boolean)
+     * @return boolean
+     */
+    public boolean getHeartbeat() {
+        return heartbeat;
+    }
+
+    /**
+     * Returns the sleep time in milliseconds that the internal heartbeat will
+     * sleep in between invokations of <code>Channel.heartbeat()</code>
+     * @return long
+     */
+    public long getHeartbeatSleeptime() {
+        return heartbeatSleeptime;
+    }
+
+    /**
+     *
+     * <p>Title: Interceptor Iterator</p>
+     *
+     * <p>Description: An iterator to loop through the interceptors in a channel</p>
+     *
+     * @version 1.0
+     */
+    public static class InterceptorIterator implements Iterator {
+        private ChannelInterceptor end;
+        private ChannelInterceptor start;
+        public InterceptorIterator(ChannelInterceptor start, ChannelInterceptor end) {
+            this.end = end;
+            this.start = start;
+        }
+
+        public boolean hasNext() {
+            return start!=null && start != end;
+        }
+
+        public Object next() {
+            Object result = null;
+            if ( hasNext() ) {
+                result = start;
+                start = start.getNext();
+            }
+            return result;
+        }
+
+        public void remove() {
+            //empty operation
+        }
+    }
+
+    /**
+     *
+     * <p>Title: Internal heartbeat thread</p>
+     *
+     * <p>Description: if <code>Channel.getHeartbeat()==true</code> then a thread of this class
+     * is created</p>
+     *
+     * @version 1.0
+     */
+    public static class HeartbeatThread extends Thread {
+        protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(HeartbeatThread.class);
+        protected static int counter = 1;
+        protected static synchronized int inc() {
+            return counter++;
+        }
+
+        protected boolean doRun = true;
+        protected GroupChannel channel;
+        protected long sleepTime;
+        public HeartbeatThread(GroupChannel channel, long sleepTime) {
+            super();
+            this.setPriority(MIN_PRIORITY);
+            setName("GroupChannel-Heartbeat-"+inc());
+            setDaemon(true);
+            this.channel = channel;
+            this.sleepTime = sleepTime;
+        }
+        public void stopHeartbeat() {
+            doRun = false;
+            interrupt();
+        }
+
+        public void run() {
+            while (doRun) {
+                try {
+                    Thread.sleep(sleepTime);
+                    channel.heartbeat();
+                } catch ( InterruptedException x ) {
+                    interrupted();
+                } catch ( Exception x ) {
+                    log.error("Unable to send heartbeat through Tribes interceptor stack. Will try to sleep again.",x);
+                }//catch
+            }//while
+        }//run
+    }//HeartbeatThread
+
+
+
+}
diff --git a/java/org/apache/catalina/tribes/group/InterceptorPayload.java b/java/org/apache/catalina/tribes/group/InterceptorPayload.java
new file mode 100644 (file)
index 0000000..96902ef
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import org.apache.catalina.tribes.ErrorHandler;
+
+/**
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class InterceptorPayload  {
+    private ErrorHandler errorHandler;
+    
+    public ErrorHandler getErrorHandler() {
+        return errorHandler;
+    }
+
+    public void setErrorHandler(ErrorHandler errorHandler) {
+        this.errorHandler = errorHandler;
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/Response.java b/java/org/apache/catalina/tribes/group/Response.java
new file mode 100644 (file)
index 0000000..1a7f88f
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import java.io.Serializable;
+
+import org.apache.catalina.tribes.Member;
+
+/**
+ * A response object holds a message from a responding partner.
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class Response {
+    private Member source;
+    private Serializable message;
+    public Response() {
+    }
+    
+    public Response(Member source, Serializable message) {
+        this.source = source;
+        this.message = message;
+    }
+
+    public void setSource(Member source) {
+        this.source = source;
+    }
+
+    public void setMessage(Serializable message) {
+        this.message = message;
+    }
+
+    public Member getSource() {
+        return source;
+    }
+
+    public Serializable getMessage() {
+        return message;
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/RpcCallback.java b/java/org/apache/catalina/tribes/group/RpcCallback.java
new file mode 100644 (file)
index 0000000..4309a24
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import java.io.Serializable;
+
+import org.apache.catalina.tribes.Member;
+
+/**
+ * The RpcCallback interface is an interface for the Tribes channel to request a
+ * response object to a request that came in.
+ * @author not attributable
+ * @version 1.0
+ */
+public interface RpcCallback {
+    
+    /**
+     * 
+     * @param msg Serializable
+     * @return Serializable - null if no reply should be sent
+     */
+    public Serializable replyRequest(Serializable msg, Member sender);
+    
+    /**
+     * If the reply has already been sent to the requesting thread,
+     * the rpc callback can handle any data that comes in after the fact.
+     * @param msg Serializable
+     * @param sender Member
+     */
+    public void leftOver(Serializable msg, Member sender);
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/RpcChannel.java b/java/org/apache/catalina/tribes/group/RpcChannel.java
new file mode 100644 (file)
index 0000000..e73dcb6
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+
+/**
+ * A channel to handle RPC messaging
+ * @author Filip Hanik
+ */
+public class RpcChannel implements ChannelListener{
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(RpcChannel.class);
+    
+    public static final int FIRST_REPLY = 1;
+    public static final int MAJORITY_REPLY = 2;
+    public static final int ALL_REPLY = 3;
+    public static final int NO_REPLY = 4;
+    
+    private Channel channel;
+    private RpcCallback callback;
+    private byte[] rpcId;
+    
+    private HashMap responseMap = new HashMap();
+
+    /**
+     * Create an RPC channel. You can have several RPC channels attached to a group
+     * all separated out by the uniqueness
+     * @param rpcId - the unique Id for this RPC group
+     * @param channel Channel
+     * @param callback RpcCallback
+     */
+    public RpcChannel(byte[] rpcId, Channel channel, RpcCallback callback) {
+        this.channel = channel;
+        this.callback = callback;
+        this.rpcId = rpcId;
+        channel.addChannelListener(this);
+    }
+    
+    
+    /**
+     * Send a message and wait for the response.
+     * @param destination Member[] - the destination for the message, and the members you request a reply from
+     * @param message Serializable - the message you are sending out
+     * @param options int - FIRST_REPLY, MAJORITY_REPLY or ALL_REPLY
+     * @param timeout long - timeout in milliseconds, if no reply is received within this time null is returned
+     * @return Response[] - an array of response objects.
+     * @throws ChannelException
+     */
+    public Response[] send(Member[] destination, 
+                           Serializable message,
+                           int rpcOptions, 
+                           int channelOptions,
+                           long timeout) throws ChannelException {
+        
+        if ( destination==null || destination.length == 0 ) return new Response[0];
+        
+        //avoid dead lock
+        channelOptions = channelOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK;
+        
+        RpcCollectorKey key = new RpcCollectorKey(UUIDGenerator.randomUUID(false));
+        RpcCollector collector = new RpcCollector(key,rpcOptions,destination.length,timeout);
+        try {
+            synchronized (collector) {
+                if ( rpcOptions != NO_REPLY ) responseMap.put(key, collector);
+                RpcMessage rmsg = new RpcMessage(rpcId, key.id, message);
+                channel.send(destination, rmsg, channelOptions);
+                if ( rpcOptions != NO_REPLY ) collector.wait(timeout);
+            }
+        } catch ( InterruptedException ix ) {
+            Thread.currentThread().interrupted();
+            //throw new ChannelException(ix);
+        }finally {
+            responseMap.remove(key);
+        }
+        return collector.getResponses();
+    }
+    
+    public void messageReceived(Serializable msg, Member sender) {
+        RpcMessage rmsg = (RpcMessage)msg;
+        RpcCollectorKey key = new RpcCollectorKey(rmsg.uuid);
+        if ( rmsg.reply ) {
+            RpcCollector collector = (RpcCollector)responseMap.get(key);
+            if (collector == null) {
+                callback.leftOver(rmsg.message, sender);
+            } else {
+                synchronized (collector) {
+                    //make sure it hasn't been removed
+                    if ( responseMap.containsKey(key) ) {
+                        if ( (rmsg instanceof RpcMessage.NoRpcChannelReply) ) 
+                            collector.destcnt--;
+                        else 
+                            collector.addResponse(rmsg.message, sender);
+                        if (collector.isComplete()) collector.notifyAll();
+                    } else {
+                        if (! (rmsg instanceof RpcMessage.NoRpcChannelReply) ) 
+                            callback.leftOver(rmsg.message, sender);
+                    }
+                }//synchronized
+            }//end if
+        } else{
+            Serializable reply = callback.replyRequest(rmsg.message,sender);
+            rmsg.reply = true;
+            rmsg.message = reply;
+            try {
+                channel.send(new Member[] {sender}, rmsg,0);
+            }catch ( Exception x )  {
+                log.error("Unable to send back reply in RpcChannel.",x);
+            }
+        }//end if
+    }
+    
+    public void breakdown() {
+        channel.removeChannelListener(this);
+    }
+    
+    public void finalize() {
+        breakdown();
+    }
+    
+    public boolean accept(Serializable msg, Member sender) {
+        if ( msg instanceof RpcMessage ) {
+            RpcMessage rmsg = (RpcMessage)msg;
+            return Arrays.equals(rmsg.rpcId,rpcId);
+        }else return false;
+    }
+    
+    public Channel getChannel() {
+        return channel;
+    }
+
+    public RpcCallback getCallback() {
+        return callback;
+    }
+
+    public byte[] getRpcId() {
+        return rpcId;
+    }
+
+    public void setChannel(Channel channel) {
+        this.channel = channel;
+    }
+
+    public void setCallback(RpcCallback callback) {
+        this.callback = callback;
+    }
+
+    public void setRpcId(byte[] rpcId) {
+        this.rpcId = rpcId;
+    }
+    
+
+
+    /**
+     * 
+     * Class that holds all response.
+     * @author not attributable
+     * @version 1.0
+     */
+    public static class RpcCollector {
+        public ArrayList responses = new ArrayList(); 
+        public RpcCollectorKey key;
+        public int options;
+        public int destcnt;
+        public long timeout;
+        
+        public RpcCollector(RpcCollectorKey key, int options, int destcnt, long timeout) {
+            this.key = key;
+            this.options = options;
+            this.destcnt = destcnt;
+            this.timeout = timeout;
+        }
+        
+        public void addResponse(Serializable message, Member sender){
+            Response resp = new Response(sender,message);
+            responses.add(resp);
+        }
+        
+        public boolean isComplete() {
+            if ( destcnt <= 0 ) return true;
+            switch (options) {
+                case ALL_REPLY:
+                    return destcnt == responses.size();
+                case MAJORITY_REPLY:
+                {
+                    float perc = ((float)responses.size()) / ((float)destcnt);
+                    return perc >= 0.50f;
+                }
+                case FIRST_REPLY:
+                    return responses.size()>0;
+                default:
+                    return false;
+            }
+        }
+        
+        public int hashCode() {
+            return key.hashCode();
+        }
+        
+        public boolean equals(Object o) {
+            if ( o instanceof RpcCollector ) {
+                RpcCollector r = (RpcCollector)o;
+                return r.key.equals(this.key);
+            } else return false;
+        }
+        
+        public Response[] getResponses() {
+            return (Response[])responses.toArray(new Response[responses.size()]);
+        }
+    }
+    
+    public static class RpcCollectorKey {
+        byte[] id;
+        public RpcCollectorKey(byte[] id) {
+            this.id = id;
+        }
+        
+        public int hashCode() {
+            return id[0]+id[1]+id[2]+id[3];
+        }
+
+        public boolean equals(Object o) {
+            if ( o instanceof RpcCollectorKey ) {
+                RpcCollectorKey r = (RpcCollectorKey)o;
+                return Arrays.equals(id,r.id);
+            } else return false;
+        }
+        
+    }
+    
+    protected static String bToS(byte[] data) {
+        StringBuffer buf = new StringBuffer(4*16);
+        buf.append("{");
+        for (int i=0; data!=null && i<data.length; i++ ) buf.append(String.valueOf(data[i])).append(" ");
+        buf.append("}");
+        return buf.toString();
+    }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/RpcMessage.java b/java/org/apache/catalina/tribes/group/RpcMessage.java
new file mode 100644 (file)
index 0000000..3f9d15a
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.group;
+
+import java.io.ObjectInput;
+import java.io.Serializable;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectOutput;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class RpcMessage implements Externalizable {
+
+    protected Serializable message;
+    protected byte[] uuid;
+    protected byte[] rpcId;
+    protected boolean reply = false;
+
+    public RpcMessage() {
+        //for serialization
+    }
+
+    public RpcMessage(byte[] rpcId, byte[] uuid, Serializable message) {
+        this.rpcId = rpcId;
+        this.uuid = uuid;
+        this.message = message;
+    }
+
+    public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
+        reply = in.readBoolean();
+        int length = in.readInt();
+        uuid = new byte[length];
+        in.read(uuid, 0, length);
+        length = in.readInt();
+        rpcId = new byte[length];
+        in.read(rpcId, 0, length);
+        message = (Serializable)in.readObject();
+    }
+
+    public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeBoolean(reply);
+        out.writeInt(uuid.length);
+        out.write(uuid, 0, uuid.length);
+        out.writeInt(rpcId.length);
+        out.write(rpcId, 0, rpcId.length);
+        out.writeObject(message);
+    }
+    
+    public static class NoRpcChannelReply extends RpcMessage {
+        public NoRpcChannelReply() {
+            
+        }
+
+        public NoRpcChannelReply(byte[] rpcid, byte[] uuid) {
+            super(rpcid,uuid,null);
+            reply = true;
+        }
+
+        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+            reply = true;
+            int length = in.readInt();
+            uuid = new byte[length];
+            in.read(uuid, 0, length);
+            length = in.readInt();
+            rpcId = new byte[length];
+            in.read(rpcId, 0, length);
+        }
+
+        public void writeExternal(ObjectOutput out) throws IOException {
+            out.writeInt(uuid.length);
+            out.write(uuid, 0, uuid.length);
+            out.writeInt(rpcId.length);
+            out.write(rpcId, 0, rpcId.length);
+        }
+    }    
+
+
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java
new file mode 100644 (file)
index 0000000..ebe4e94
--- /dev/null
@@ -0,0 +1,101 @@
+/*\r
+ * Copyright 1999,2004 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ */\r
+package org.apache.catalina.tribes.group.interceptors;\r
+\r
+import org.apache.catalina.tribes.ChannelMessage;\r
+import org.apache.catalina.tribes.Member;\r
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;\r
+import org.apache.catalina.tribes.membership.MemberImpl;\r
+import org.apache.catalina.tribes.membership.Membership;\r
+import java.util.Arrays;\r
+\r
+/**\r
+ * <p>Title: Member domain filter interceptor </p>\r
+ *\r
+ * <p>Description: Filters membership based on domain.\r
+ * </p>\r
+ *\r
+ * @author Filip Hanik\r
+ * @version 1.0\r
+ */\r
+public class DomainFilterInterceptor extends ChannelInterceptorBase {\r
+\r
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( DomainFilterInterceptor.class );\r
+\r
+    protected Membership membership = null;\r
+    \r
+    protected byte[] domain = new byte[0];\r
+\r
+    public void messageReceived(ChannelMessage msg) {\r
+        //should we filter incoming based on domain?\r
+        super.messageReceived(msg);\r
+    }//messageReceived\r
+\r
+\r
+    public void memberAdded(Member member) {\r
+        if ( membership == null ) setupMembership();\r
+        boolean notify = false;\r
+        synchronized (membership) {\r
+            notify = Arrays.equals(domain,member.getDomain());\r
+            if ( notify ) notify = membership.memberAlive((MemberImpl)member);\r
+        }\r
+        if ( notify ) super.memberAdded(member);\r
+    }\r
+\r
+    public void memberDisappeared(Member member) {\r
+        if ( membership == null ) setupMembership();\r
+        boolean notify = false;\r
+        synchronized (membership) {\r
+            notify = Arrays.equals(domain,member.getDomain());\r
+            membership.removeMember((MemberImpl)member);\r
+        }\r
+        if ( notify ) super.memberDisappeared(member);\r
+    }\r
+\r
+    public boolean hasMembers() {\r
+        if ( membership == null ) setupMembership();\r
+        return membership.hasMembers();\r
+    }\r
+\r
+    public Member[] getMembers() {\r
+        if ( membership == null ) setupMembership();\r
+        return membership.getMembers();\r
+    }\r
+\r
+    public Member getMember(Member mbr) {\r
+        if ( membership == null ) setupMembership();\r
+        return membership.getMember(mbr);\r
+    }\r
+\r
+    public Member getLocalMember(boolean incAlive) {\r
+        return super.getLocalMember(incAlive);\r
+    }\r
+\r
+\r
+    protected synchronized void setupMembership() {\r
+        if ( membership == null ) {\r
+            membership = new Membership((MemberImpl)super.getLocalMember(true));\r
+        }\r
+\r
+    }\r
+\r
+    public byte[] getDomain() {\r
+        return domain;\r
+    }\r
+\r
+    public void setDomain(byte[] domain) {\r
+        this.domain = domain;\r
+    }\r
+}\r
diff --git a/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java
new file mode 100644 (file)
index 0000000..cdd615b
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Set;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.XByteBuffer;
+
+/**
+ *
+ * The fragmentation interceptor splits up large messages into smaller messages and assembles them on the other end.
+ * This is very useful when you don't want large messages hogging the sending sockets
+ * and smaller messages can make it through.
+ * 
+ * <br><b>Configuration Options</b><br>
+ * OrderInteceptor.expire=<milliseconds> - how long do we keep the fragments in memory and wait for the rest to arrive<b>default=60,000ms -> 60seconds</b>
+ * This setting is useful to avoid OutOfMemoryErrors<br>
+ * OrderInteceptor.maxSize=<max message size> - message size in bytes <b>default=1024*100 (around a tenth of a MB)</b><br>
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class FragmentationInterceptor extends ChannelInterceptorBase {
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( FragmentationInterceptor.class );
+    
+    protected HashMap fragpieces = new HashMap();
+    private int maxSize = 1024*100;
+    private long expire = 1000 * 60; //one minute expiration
+    protected boolean deepclone = true;
+
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        int size = msg.getMessage().getLength();
+        boolean frag = (size>maxSize) && okToProcess(msg.getOptions());
+        if ( frag ) {
+            frag(destination, msg, payload);
+        } else {
+            msg.getMessage().append(frag);
+            super.sendMessage(destination, msg, payload);
+        }
+    }
+    
+    public void messageReceived(ChannelMessage msg) {
+        boolean isFrag = XByteBuffer.toBoolean(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-1);
+        msg.getMessage().trim(1);
+        if ( isFrag ) {
+            defrag(msg);
+        } else {
+            super.messageReceived(msg);
+        }
+    }
+
+    
+    public FragCollection getFragCollection(FragKey key, ChannelMessage msg) {
+        FragCollection coll = (FragCollection)fragpieces.get(key);
+        if ( coll == null ) {
+            synchronized (fragpieces) {
+                coll = (FragCollection)fragpieces.get(key);
+                if ( coll == null ) {
+                    coll = new FragCollection(msg);
+                    fragpieces.put(key, coll);
+                }
+            }
+        } 
+        return coll;
+    }
+    
+    public void removeFragCollection(FragKey key) {
+        fragpieces.remove(key);
+    }
+    
+    public void defrag(ChannelMessage msg ) { 
+        FragKey key = new FragKey(msg.getUniqueId());
+        FragCollection coll = getFragCollection(key,msg);
+        coll.addMessage((ChannelMessage)msg.deepclone());
+
+        if ( coll.complete() ) {
+            removeFragCollection(key);
+            ChannelMessage complete = coll.assemble();
+            super.messageReceived(complete);
+            
+        }
+    }
+
+    public void frag(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        int size = msg.getMessage().getLength();
+
+        int count = ((size / maxSize )+(size%maxSize==0?0:1));
+        ChannelMessage[] messages = new ChannelMessage[count];
+        int remaining = size;
+        for ( int i=0; i<count; i++ ) {
+            ChannelMessage tmp = (ChannelMessage)msg.clone();
+            int offset = (i*maxSize);
+            int length = Math.min(remaining,maxSize);
+            tmp.getMessage().clear();
+            tmp.getMessage().append(msg.getMessage().getBytesDirect(),offset,length);
+            //add the msg nr
+            //tmp.getMessage().append(XByteBuffer.toBytes(i),0,4);
+            tmp.getMessage().append(i);
+            //add the total nr of messages
+            //tmp.getMessage().append(XByteBuffer.toBytes(count),0,4);
+            tmp.getMessage().append(count);
+            //add true as the frag flag
+            //byte[] flag = XByteBuffer.toBytes(true);
+            //tmp.getMessage().append(flag,0,flag.length);
+            tmp.getMessage().append(true);
+            messages[i] = tmp;
+            remaining -= length;
+            
+        }
+        for ( int i=0; i<messages.length; i++ ) {
+            super.sendMessage(destination,messages[i],payload);
+        }
+    }
+    
+    public void heartbeat() {
+        try {
+            Set set = fragpieces.keySet(); 
+            Object[] keys = set.toArray();
+            for ( int i=0; i<keys.length; i++ ) {
+                FragKey key = (FragKey)keys[i];
+                if ( key != null && key.expired(getExpire()) ) 
+                    removeFragCollection(key);
+            }
+        }catch ( Exception x ) {
+            if ( log.isErrorEnabled() ) {
+                log.error("Unable to perform heartbeat clean up in the frag interceptor",x);
+            }
+        }
+        super.heartbeat();
+    }
+
+    
+
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setMaxSize(int maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public static class FragCollection {
+        private long received = System.currentTimeMillis();
+        private ChannelMessage msg;
+        private XByteBuffer[] frags;
+        public FragCollection(ChannelMessage msg) {
+            //get the total messages
+            int count = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4);
+            frags = new XByteBuffer[count];
+            this.msg = msg;
+        }
+        
+        public void addMessage(ChannelMessage msg) {
+            //remove the total messages
+            msg.getMessage().trim(4);
+            //get the msg nr
+            int nr = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4);
+            //remove the msg nr
+            msg.getMessage().trim(4);
+            frags[nr] = msg.getMessage();
+            
+        }
+        
+        public boolean complete() {
+            boolean result = true;
+            for ( int i=0; (i<frags.length) && (result); i++ ) result = (frags[i] != null);
+            return result;
+        }
+        
+        public ChannelMessage assemble() {
+            if ( !complete() ) throw new IllegalStateException("Fragments are missing.");
+            int buffersize = 0;
+            for (int i=0; i<frags.length; i++ ) buffersize += frags[i].getLength();
+            XByteBuffer buf = new XByteBuffer(buffersize,false);
+            msg.setMessage(buf);
+            for ( int i=0; i<frags.length; i++ ) {
+                msg.getMessage().append(frags[i].getBytesDirect(),0,frags[i].getLength());
+            }
+            return msg;
+        }
+        
+        public boolean expired(long expire) {
+            return (System.currentTimeMillis()-received)>expire;
+        }
+
+        
+        
+    }
+    
+    public static class FragKey {
+        private byte[] uniqueId;
+        private long received = System.currentTimeMillis();
+        public FragKey(byte[] id ) {
+            this.uniqueId = id;
+        }
+        public int hashCode() {
+            return XByteBuffer.toInt(uniqueId,0);
+        }
+        
+        public boolean equals(Object o ) {
+            if ( o instanceof FragKey ) {
+            return Arrays.equals(uniqueId,((FragKey)o).uniqueId);
+        } else return false;
+
+        }
+        
+        public boolean expired(long expire) {
+            return (System.currentTimeMillis()-received)>expire;
+        }
+
+    }
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java
new file mode 100644 (file)
index 0000000..18b3024
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+
+
+
+/**
+ *
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class GzipInterceptor extends ChannelInterceptorBase {
+    public static final int DEFAULT_BUFFER_SIZE = 2048;
+    
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        try {
+            byte[] data = compress(msg.getMessage().getBytes());
+            msg.getMessage().trim(msg.getMessage().getLength());
+            msg.getMessage().append(data,0,data.length);
+            getNext().sendMessage(destination, msg, payload);
+        } catch ( IOException x ) {
+            log.error("Unable to compress byte contents");
+            throw new ChannelException(x);
+        }
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        try {
+            byte[] data = decompress(msg.getMessage().getBytes());
+            msg.getMessage().trim(msg.getMessage().getLength());
+            msg.getMessage().append(data,0,data.length);
+            getPrevious().messageReceived(msg);
+        } catch ( IOException x ) {
+            log.error("Unable to decompress byte contents",x);
+        }
+    }
+    
+    public static byte[] compress(byte[] data) throws IOException {
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        GZIPOutputStream gout = new GZIPOutputStream(bout);
+        gout.write(data);
+        gout.flush();
+        gout.close();
+        return bout.toByteArray();
+    }
+    
+    /**
+     * @todo Fix to create an automatically growing buffer.
+     * @param data byte[]
+     * @return byte[]
+     * @throws IOException
+     */
+    public static byte[] decompress(byte[] data) throws IOException {
+        ByteArrayInputStream bin = new ByteArrayInputStream(data);
+        GZIPInputStream gin = new GZIPInputStream(bin);
+        byte[] tmp = new byte[DEFAULT_BUFFER_SIZE];
+        int length = gin.read(tmp);
+        byte[] result = new byte[length];
+        System.arraycopy(tmp,0,result,0,length);
+        return result;
+    }
+    
+    public static void main(String[] arg) throws Exception {
+        byte[] data = new byte[1024];
+        Arrays.fill(data,(byte)1);
+        byte[] compress = compress(data);
+        byte[] decompress = decompress(compress);
+        System.out.println("Debug test");
+        
+    }
+    
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java
new file mode 100644 (file)
index 0000000..c0198ba
--- /dev/null
@@ -0,0 +1,111 @@
+/*\r
+ * Copyright 1999,2004 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ */\r
+package org.apache.catalina.tribes.group.interceptors;\r
+\r
+import java.util.concurrent.LinkedBlockingQueue;\r
+import java.util.concurrent.ThreadPoolExecutor;\r
+import java.util.concurrent.atomic.AtomicLong;\r
+\r
+import org.apache.catalina.tribes.ChannelMessage;\r
+import org.apache.catalina.tribes.Member;\r
+import org.apache.catalina.tribes.group.InterceptorPayload;\r
+import org.apache.catalina.tribes.transport.bio.util.LinkObject;\r
+import java.util.concurrent.TimeUnit;\r
+\r
+/**\r
+ * \r
+ * Same implementation as the MessageDispatchInterceptor\r
+ * except is ues an atomic long for the currentSize calculation\r
+ * and uses a thread pool for message sending.\r
+ * \r
+ * @author Filip Hanik\r
+ * @version 1.0\r
+ */\r
+\r
+public class MessageDispatch15Interceptor extends MessageDispatchInterceptor {\r
+\r
+    protected AtomicLong currentSize = new AtomicLong(0);\r
+    protected ThreadPoolExecutor executor = null;\r
+    protected int maxThreads = 10;\r
+    protected int maxSpareThreads = 2;\r
+    protected long keepAliveTime = 5000;\r
+    protected LinkedBlockingQueue<Runnable> runnablequeue = new LinkedBlockingQueue<Runnable>();\r
+\r
+    public long getCurrentSize() {\r
+        return currentSize.get();\r
+    }\r
+\r
+    public long addAndGetCurrentSize(long inc) {\r
+        return currentSize.addAndGet(inc);\r
+    }\r
+\r
+    public long setAndGetCurrentSize(long value) {\r
+        currentSize.set(value);\r
+        return value;\r
+    }\r
+    \r
+    public boolean addToQueue(ChannelMessage msg, Member[] destination, InterceptorPayload payload) {\r
+        final LinkObject obj = new LinkObject(msg,destination,payload);\r
+        Runnable r = new Runnable() {\r
+            public void run() {\r
+                sendAsyncData(obj);\r
+            }\r
+        };\r
+        executor.execute(r);\r
+        return true;\r
+    }\r
+\r
+    public LinkObject removeFromQueue() {\r
+        return null; //not used, thread pool contains its own queue.\r
+    }\r
+\r
+    public void startQueue() {\r
+        if ( run ) return;\r
+        executor = new ThreadPoolExecutor(maxSpareThreads,maxThreads,keepAliveTime,TimeUnit.MILLISECONDS,runnablequeue);\r
+        run = true;\r
+    }\r
+\r
+    public void stopQueue() {\r
+        run = false;\r
+        executor.shutdownNow();\r
+        setAndGetCurrentSize(0);\r
+        runnablequeue.clear();\r
+    }\r
+\r
+    public long getKeepAliveTime() {\r
+        return keepAliveTime;\r
+    }\r
+\r
+    public int getMaxSpareThreads() {\r
+        return maxSpareThreads;\r
+    }\r
+\r
+    public int getMaxThreads() {\r
+        return maxThreads;\r
+    }\r
+\r
+    public void setKeepAliveTime(long keepAliveTime) {\r
+        this.keepAliveTime = keepAliveTime;\r
+    }\r
+\r
+    public void setMaxSpareThreads(int maxSpareThreads) {\r
+        this.maxSpareThreads = maxSpareThreads;\r
+    }\r
+\r
+    public void setMaxThreads(int maxThreads) {\r
+        this.maxThreads = maxThreads;\r
+    }\r
+\r
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java
new file mode 100644 (file)
index 0000000..a9eb917
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+
+package org.apache.catalina.tribes.group.interceptors;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.transport.bio.util.FastQueue;
+import org.apache.catalina.tribes.transport.bio.util.LinkObject;
+import org.apache.catalina.tribes.UniqueId;
+
+/**
+ *
+ * The message dispatcher is a way to enable asynchronous communication
+ * through a channel. The dispatcher will look for the <code>Channel.SEND_OPTIONS_ASYNCHRONOUS</code>
+ * flag to be set, if it is, it will queue the message for delivery and immediately return to the sender.
+ * 
+ * 
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class MessageDispatchInterceptor extends ChannelInterceptorBase implements Runnable {
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(MessageDispatchInterceptor.class);
+
+    protected long maxQueueSize = 1024*1024*64; //64MB
+    protected FastQueue queue = new FastQueue();
+    protected boolean run = false;
+    protected Thread msgDispatchThread = null;
+    protected long currentSize = 0;
+    protected boolean useDeepClone = true;
+    protected boolean alwaysSend = true;
+
+    public MessageDispatchInterceptor() {
+        setOptionFlag(Channel.SEND_OPTIONS_ASYNCHRONOUS);
+    }
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        boolean async = (msg.getOptions() & Channel.SEND_OPTIONS_ASYNCHRONOUS) == Channel.SEND_OPTIONS_ASYNCHRONOUS;
+        if ( async && run ) {
+            if ( (getCurrentSize()+msg.getMessage().getLength()) > maxQueueSize ) {
+                if ( alwaysSend ) {
+                    super.sendMessage(destination,msg,payload);
+                    return;
+                } else {
+                    throw new ChannelException("Asynchronous queue is full, reached its limit of " + maxQueueSize +" bytes, current:" + getCurrentSize() + " bytes.");
+                }//end if
+            }//end if
+            //add to queue
+            if ( useDeepClone ) msg = (ChannelMessage)msg.deepclone();
+            if (!addToQueue(msg, destination, payload) ) {
+                throw new ChannelException("Unable to add the message to the async queue, queue bug?");
+            }
+            addAndGetCurrentSize(msg.getMessage().getLength());
+        } else {
+            super.sendMessage(destination, msg, payload);
+        }
+    }
+    
+    public boolean addToQueue(ChannelMessage msg, Member[] destination, InterceptorPayload payload) {
+        return queue.add(msg,destination,payload);
+    }
+    
+    public LinkObject removeFromQueue() {
+        return queue.remove();
+    }
+    
+    public void startQueue() {
+        msgDispatchThread = new Thread(this);
+        msgDispatchThread.setName("MessageDispatchInterceptor.MessageDispatchThread");
+        msgDispatchThread.setDaemon(true);
+        msgDispatchThread.setPriority(Thread.MAX_PRIORITY);
+        queue.setEnabled(true);
+        run = true;
+        msgDispatchThread.start();
+    }
+    
+    public void stopQueue() {
+        run = false;
+        msgDispatchThread.interrupt();
+        queue.setEnabled(false);
+        setAndGetCurrentSize(0);
+    }
+    
+    
+    public void setOptionFlag(int flag) {
+        if ( flag != Channel.SEND_OPTIONS_ASYNCHRONOUS ) log.warn("Warning, you are overriding the asynchronous option flag, this will disable the Channel.SEND_OPTIONS_ASYNCHRONOUS that other apps might use.");
+        super.setOptionFlag(flag);
+    }
+
+    public void setMaxQueueSize(long maxQueueSize) {
+        this.maxQueueSize = maxQueueSize;
+    }
+
+    public void setUseDeepClone(boolean useDeepClone) {
+        this.useDeepClone = useDeepClone;
+    }
+
+    public long getMaxQueueSize() {
+        return maxQueueSize;
+    }
+
+    public boolean getUseDeepClone() {
+        return useDeepClone;
+    }
+    
+    public long getCurrentSize() {
+        return currentSize;
+    }
+    
+    public synchronized long addAndGetCurrentSize(long inc) {
+        currentSize += inc;
+        return currentSize;
+    }
+    
+    public synchronized long setAndGetCurrentSize(long value) {
+        currentSize = value;
+        return value;
+    }
+
+    public void start(int svc) throws ChannelException {
+        //start the thread
+        if (!run ) {
+            synchronized (this) {
+                if ( !run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ) ) {//only start with the sender
+                    startQueue();
+                }//end if
+            }//sync
+        }//end if
+        super.start(svc);
+    }
+
+    
+    public void stop(int svc) throws ChannelException {
+        //stop the thread
+        if ( run ) {
+            synchronized (this) {
+                if ( run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ)) {
+                    stopQueue();
+                }//end if
+            }//sync
+        }//end if
+
+        super.stop(svc);
+    }
+    
+    public void run() {
+        while ( run ) {
+            LinkObject link = removeFromQueue();
+            if ( link == null ) continue; //should not happen unless we exceed wait time
+            while ( link != null && run ) {
+                link = sendAsyncData(link);
+            }//while
+        }//while
+    }//run
+
+    protected LinkObject sendAsyncData(LinkObject link) {
+        ChannelMessage msg = link.data();
+        Member[] destination = link.getDestination();
+        try {
+            super.sendMessage(destination,msg,null);
+            try {
+                if ( link.getHandler() != null ) link.getHandler().handleCompletion(new UniqueId(msg.getUniqueId())); 
+            } catch ( Exception ex ) {
+                log.error("Unable to report back completed message.",ex);
+            }
+        } catch ( Exception x ) {
+            ChannelException cx = null;
+            if ( x instanceof ChannelException ) cx = (ChannelException)x;
+            else cx = new ChannelException(x);
+            if ( log.isDebugEnabled() ) log.debug("Error while processing async message.",x);
+            try {
+                if (link.getHandler() != null) link.getHandler().handleError(cx, new UniqueId(msg.getUniqueId()));
+            } catch ( Exception ex ) {
+                log.error("Unable to report back error message.",ex);
+            }
+        } finally {
+            addAndGetCurrentSize(-msg.getMessage().getLength());
+            link = link.next();
+        }//try
+        return link;
+    }
+
+
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java b/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java
new file mode 100644 (file)
index 0000000..8267f8f
--- /dev/null
@@ -0,0 +1,839 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelInterceptor.InterceptorEvent;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.group.AbsoluteOrder;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.membership.Membership;
+import org.apache.catalina.tribes.util.Arrays;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+
+/**
+ * <p>Title: Auto merging leader election algorithm</p>
+ *
+ * <p>Description: Implementation of a simple coordinator algorithm that not only selects a coordinator,
+ *    it also merges groups automatically when members are discovered that werent part of the 
+ *    </p>
+ * <p>This algorithm is non blocking meaning it allows for transactions while the coordination phase is going on
+ * </p>
+ * <p>This implementation is based on a home brewed algorithm that uses the AbsoluteOrder of a membership
+ * to pass a token ring of the current membership.<br>
+ * This is not the same as just using AbsoluteOrder! Consider the following scenario:<br>
+ * Nodes, A,B,C,D,E on a network, in that priority. AbsoluteOrder will only work if all
+ * nodes are receiving pings from all the other nodes. 
+ * meaning, that node{i} receives pings from node{all}-node{i}<br>
+ * but the following could happen if a multicast problem occurs.
+ * A has members {B,C,D}<br>
+ * B has members {A,C}<br>
+ * C has members {D,E}<br>
+ * D has members {A,B,C,E}<br>
+ * E has members {A,C,D}<br>
+ * Because the default Tribes membership implementation, relies on the multicast packets to 
+ * arrive at all nodes correctly, there is nothing guaranteeing that it will.<br>
+ * <br>
+ * To best explain how this algorithm works, lets take the above example:
+ * For simplicity we assume that a send operation is O(1) for all nodes, although this algorithm will work
+ * where messages overlap, as they all depend on absolute order<br>
+ * Scenario 1: A,B,C,D,E all come online at the same time
+ * Eval phase, A thinks of itself as leader, B thinks of A as leader,
+ * C thinks of itself as leader, D,E think of A as leader<br>
+ * Token phase:<br>
+ * (1) A sends out a message X{A-ldr, A-src, mbrs-A,B,C,D} to B where X is the id for the message(and the view)<br>
+ * (1) C sends out a message Y{C-ldr, C-src, mbrs-C,D,E} to D where Y is the id for the message(and the view)<br>
+ * (2) B receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D} to C <br>
+ * (2) D receives Y{C-ldr, C-src, mbrs-C,D,E} D is aware of A,B, sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to E<br>
+ * (3) C receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to D<br>
+ * (3) E receives Y{A-ldr, C-src, mbrs-A,B,C,D,E} sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to A<br>
+ * (4) D receives X{A-ldr, A-src, mbrs-A,B,C,D,E} sends sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to A<br>
+ * (4) A receives Y{A-ldr, C-src, mbrs-A,B,C,D,E}, holds the message, add E to its list of members<br>
+ * (5) A receives X{A-ldr, A-src, mbrs-A,B,C,D,E} <br>
+ * At this point, the state looks like<br>
+ * A - {A-ldr, mbrs-A,B,C,D,E, id=X}<br>
+ * B - {A-ldr, mbrs-A,B,C,D, id=X}<br>
+ * C - {A-ldr, mbrs-A,B,C,D,E, id=X}<br>
+ * D - {A-ldr, mbrs-A,B,C,D,E, id=X}<br>
+ * E - {A-ldr, mbrs-A,B,C,D,E, id=Y}<br>
+ * <br>
+ * A message doesn't stop until it reaches its original sender, unless its dropped by a higher leader.
+ * As you can see, E still thinks the viewId=Y, which is not correct. But at this point we have 
+ * arrived at the same membership and all nodes are informed of each other.<br>
+ * To synchronize the rest we simply perform the following check at A when A receives X:<br>
+ * Original X{A-ldr, A-src, mbrs-A,B,C,D} == Arrived X{A-ldr, A-src, mbrs-A,B,C,D,E}<br>
+ * Since the condition is false, A, will resend the token, and A sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to B
+ * When A receives X again, the token is complete. <br>
+ * Optionally, A can send a message X{A-ldr, A-src, mbrs-A,B,C,D,E confirmed} to A,B,C,D,E who then
+ * install and accept the view.
+ * </p>
+ * <p>
+ * Lets assume that C1 arrives, C1 has lower priority than C, but higher priority than D.<br>
+ * Lets also assume that C1 sees the following view {B,D,E}<br>
+ * C1 waits for a token to arrive. When the token arrives, the same scenario as above will happen.<br>
+ * In the scenario where C1 sees {D,E} and A,B,C can not see C1, no token will ever arrive.<br>
+ * In this case, C1 sends a Z{C1-ldr, C1-src, mbrs-C1,D,E} to D<br>
+ * D receives Z{C1-ldr, C1-src, mbrs-C1,D,E} and sends Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} to E<br>
+ * E receives Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} and sends it to A<br>
+ * A sends Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E} to B and the chain continues until A receives the token again.
+ * At that time A optionally sends out Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E, confirmed} to A,B,C,C1,D,E
+ * </p>
+ * <p>To ensure that the view gets implemented at all nodes at the same time, 
+ *    A will send out a VIEW_CONF message, this is the 'confirmed' message that is optional above.
+ * <p>Ideally, the interceptor below this one would be the TcpFailureDetector to ensure correct memberships</p>
+ *
+ * <p>The example above, of course can be simplified with a finite statemachine:<br>
+ * But I suck at writing state machines, my head gets all confused. One day I will document this algorithm though.<br>
+ * Maybe I'll do a state diagram :)
+ * </p>
+ * <h2>State Diagrams</h2>
+ * <a href="http://people.apache.org/~fhanik/tribes/docs/leader-election-initiate-election.jpg">Initiate an election</a><br><br>
+ * <a href="http://people.apache.org/~fhanik/tribes/docs/leader-election-message-arrives.jpg">Receive an election message</a><br><br>
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ * 
+ * 
+ * 
+ */
+public class NonBlockingCoordinator extends ChannelInterceptorBase {
+    
+    /**
+     * header for a coordination message
+     */
+    protected static final byte[] COORD_HEADER = new byte[] {-86, 38, -34, -29, -98, 90, 65, 63, -81, -122, -6, -110, 99, -54, 13, 63};
+    /**
+     * Coordination request
+     */
+    protected static final byte[] COORD_REQUEST = new byte[] {104, -95, -92, -42, 114, -36, 71, -19, -79, 20, 122, 101, -1, -48, -49, 30};
+    /**
+     * Coordination confirmation, for blocking installations
+     */
+    protected static final byte[] COORD_CONF = new byte[] {67, 88, 107, -86, 69, 23, 76, -70, -91, -23, -87, -25, -125, 86, 75, 20};
+    
+    /**
+     * Alive message
+     */
+    protected static final byte[] COORD_ALIVE = new byte[] {79, -121, -25, -15, -59, 5, 64, 94, -77, 113, -119, -88, 52, 114, -56, -46,
+                                                            -18, 102, 10, 34, -127, -9, 71, 115, -70, 72, -101, 88, 72, -124, 127, 111,
+                                                            74, 76, -116, 50, 111, 103, 65, 3, -77, 51, -35, 0, 119, 117, 9, -26,
+                                                            119, 50, -75, -105, -102, 36, 79, 37, -68, -84, -123, 15, -22, -109, 106, -55};
+    /**
+     * Time to wait for coordination timeout
+     */
+    protected long waitForCoordMsgTimeout = 15000;
+    /**
+     * Our current view
+     */
+    protected Membership view = null;
+    /**
+     * Out current viewId
+     */
+    protected UniqueId viewId;
+
+    /**
+     * Our nonblocking membership
+     */
+    protected Membership membership = null;
+    
+    /**
+     * indicates that we are running an election 
+     * and this is the one we are running
+     */
+    protected UniqueId suggestedviewId;
+    protected Membership suggestedView;
+    
+    protected boolean started = false;
+    protected final int startsvc = 0xFFFF;
+    
+    protected Object electionMutex = new Object();
+    
+    protected AtomicBoolean coordMsgReceived = new AtomicBoolean(false);
+    
+    public NonBlockingCoordinator() {
+        super();
+    }
+    
+//============================================================================================================    
+//              COORDINATION HANDLING
+//============================================================================================================
+    
+    public void startElection(boolean force) throws ChannelException {
+        synchronized (electionMutex) {
+            MemberImpl local = (MemberImpl)getLocalMember(false);
+            MemberImpl[] others = (MemberImpl[])membership.getMembers();
+            fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT,this,"Election initated"));
+            if ( others.length == 0 ) {
+                this.viewId = new UniqueId(UUIDGenerator.randomUUID(false));
+                this.view = new Membership(local,AbsoluteOrder.comp, true);
+                this.handleViewConf(this.createElectionMsg(local,others,local),local,view);
+                return; //the only member, no need for an election
+            }
+            if ( suggestedviewId != null ) {
+                
+                if ( view != null && Arrays.diff(view,suggestedView,local).length == 0 &&  Arrays.diff(suggestedView,view,local).length == 0) {
+                    suggestedviewId = null;
+                    suggestedView = null;
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, running election matches view"));
+                } else {
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, election running"));
+                }
+                return; //election already running, I'm not allowed to have two of them
+            }
+            if ( view != null && Arrays.diff(view,membership,local).length == 0 &&  Arrays.diff(membership,view,local).length == 0) {
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, view matches membership"));
+                return; //already have this view installed
+            }            
+            int prio = AbsoluteOrder.comp.compare(local,others[0]);
+            MemberImpl leader = ( prio < 0 )?local:others[0];//am I the leader in my view?
+            if ( local.equals(leader) || force ) {
+                CoordinationMessage msg = createElectionMsg(local, others, leader);
+                suggestedviewId = msg.getId();
+                suggestedView = new Membership(local,AbsoluteOrder.comp,true);
+                Arrays.fill(suggestedView,msg.getMembers());
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_PROCESS_ELECT,this,"Election, sending request"));
+                sendElectionMsg(local,others[0],msg);
+            } else {
+                try {
+                    coordMsgReceived.set(false);
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_WAIT_FOR_MSG,this,"Election, waiting for request"));
+                    electionMutex.wait(waitForCoordMsgTimeout);
+                }catch ( InterruptedException x ) {
+                    Thread.currentThread().interrupted();
+                }
+                if ( suggestedviewId == null && (!coordMsgReceived.get())) {
+                    //no message arrived, send the coord msg
+//                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_WAIT_FOR_MSG,this,"Election, waiting timed out."));
+//                    startElection(true);
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, waiting timed out."));
+                } else {
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, received a message"));
+                }
+            }//end if
+            
+        }
+    }
+
+    private CoordinationMessage createElectionMsg(MemberImpl local, MemberImpl[] others, MemberImpl leader) {
+        Membership m = new Membership(local,AbsoluteOrder.comp,true);
+        Arrays.fill(m,others);
+        MemberImpl[] mbrs = m.getMembers();
+        m.reset(); 
+        CoordinationMessage msg = new CoordinationMessage(leader, local, mbrs,new UniqueId(UUIDGenerator.randomUUID(true)), this.COORD_REQUEST);
+        return msg;
+    }
+
+    protected void sendElectionMsg(MemberImpl local, MemberImpl next, CoordinationMessage msg) throws ChannelException {
+        fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_SEND_MSG,this,"Sending election message to("+next.getName()+")"));
+        super.sendMessage(new Member[] {next}, createData(msg, local), null);
+    }
+    
+    protected void sendElectionMsgToNextInline(MemberImpl local, CoordinationMessage msg) throws ChannelException { 
+        int next = Arrays.nextIndex(local,msg.getMembers());
+        int current = next;
+        msg.leader = msg.getMembers()[0];
+        boolean sent =  false;
+        while ( !sent && current >= 0 ) {
+            try {
+                sendElectionMsg(local, (MemberImpl) msg.getMembers()[current], msg);
+                sent = true;
+            }catch ( ChannelException x  ) {
+                log.warn("Unable to send election message to:"+msg.getMembers()[current]);
+                current = Arrays.nextIndex(msg.getMembers()[current],msg.getMembers());
+                if ( current == next ) throw x;
+            }
+        }
+    }
+    
+    public Member getNextInLine(MemberImpl local, MemberImpl[] others) {
+        MemberImpl result = null;
+        for ( int i=0; i<others.length; i++ ) {
+            
+        }
+        return result;
+    }
+    
+    public ChannelData createData(CoordinationMessage msg, MemberImpl local) {
+        msg.write();
+        ChannelData data = new ChannelData(true);
+        data.setAddress(local);
+        data.setMessage(msg.getBuffer());
+        data.setOptions(Channel.SEND_OPTIONS_USE_ACK);
+        data.setTimestamp(System.currentTimeMillis());
+        return data;
+    }
+    
+    protected void viewChange(UniqueId viewId, Member[] view) {
+        //invoke any listeners
+    }
+    
+    protected boolean alive(Member mbr) {
+        return TcpFailureDetector.memberAlive(mbr,
+                                              COORD_ALIVE,
+                                              false,
+                                              false,
+                                              waitForCoordMsgTimeout,
+                                              waitForCoordMsgTimeout,
+                                              getOptionFlag());
+    }
+    
+    protected Membership mergeOnArrive(CoordinationMessage msg, Member sender) {
+        fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_PRE_MERGE,this,"Pre merge"));
+        MemberImpl local = (MemberImpl)getLocalMember(false);
+        Membership merged = new Membership(local,AbsoluteOrder.comp,true);
+        Arrays.fill(merged,msg.getMembers());
+        Arrays.fill(merged,getMembers());
+        Member[] diff = Arrays.diff(merged,membership,local);
+        for ( int i=0; i<diff.length; i++ ) {
+            if (!alive(diff[i])) merged.removeMember((MemberImpl)diff[i]);
+            else memberAdded(diff[i],false);
+        }
+        fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_POST_MERGE,this,"Post merge"));
+        return merged;
+    }
+    
+    protected void processCoordMessage(CoordinationMessage msg, Member sender) throws ChannelException {
+        if ( !coordMsgReceived.get() ) {
+            coordMsgReceived.set(true);
+            synchronized (electionMutex) { electionMutex.notifyAll();}
+        } 
+        msg.timestamp = System.currentTimeMillis();
+        Membership merged = mergeOnArrive(msg, sender);
+        if (isViewConf(msg)) handleViewConf(msg, sender, merged);
+        else handleToken(msg, sender, merged);
+        ClassLoader loader;
+
+    }
+    
+    protected void handleToken(CoordinationMessage msg, Member sender,Membership merged) throws ChannelException {
+        MemberImpl local = (MemberImpl)getLocalMember(false);
+        if ( local.equals(msg.getSource()) ) {
+            //my message msg.src=local
+            handleMyToken(local, msg, sender,merged);
+        } else {
+            handleOtherToken(local, msg, sender,merged);
+        }
+    }
+    
+    protected void handleMyToken(MemberImpl local, CoordinationMessage msg, Member sender,Membership merged) throws ChannelException {
+        if ( local.equals(msg.getLeader()) ) {
+            //no leadership change
+            if ( Arrays.sameMembers(msg.getMembers(),merged.getMembers()) ) {
+                msg.type = COORD_CONF;
+                super.sendMessage(Arrays.remove(msg.getMembers(),local),createData(msg,local),null);
+                handleViewConf(msg,local,merged);
+            } else {
+                //membership change
+                suggestedView = new Membership(local,AbsoluteOrder.comp,true);
+                suggestedviewId = msg.getId();
+                Arrays.fill(suggestedView,merged.getMembers());
+                msg.view = (MemberImpl[])merged.getMembers();
+                sendElectionMsgToNextInline(local,msg);
+            }
+        } else {
+            //leadership change
+            suggestedView = null;
+            suggestedviewId = null;
+            msg.view = (MemberImpl[])merged.getMembers();
+            sendElectionMsgToNextInline(local,msg);
+        }
+    }
+    
+    protected void handleOtherToken(MemberImpl local, CoordinationMessage msg, Member sender,Membership merged) throws ChannelException {
+        if ( local.equals(msg.getLeader()) ) {
+            //I am the new leader
+            //startElection(false);
+        } else {
+            msg.view = (MemberImpl[])merged.getMembers();
+            sendElectionMsgToNextInline(local,msg);
+        }
+    }
+    
+    protected void handleViewConf(CoordinationMessage msg, Member sender,Membership merged) throws ChannelException {
+        if ( viewId != null && msg.getId().equals(viewId) ) return;//we already have this view
+        view = new Membership((MemberImpl)getLocalMember(false),AbsoluteOrder.comp,true);
+        Arrays.fill(view,msg.getMembers());
+        viewId = msg.getId();
+        
+        if ( viewId.equals(suggestedviewId) ) {
+            suggestedView = null;
+            suggestedviewId = null;
+        }
+        
+        if (suggestedView != null && AbsoluteOrder.comp.compare(suggestedView.getMembers()[0],merged.getMembers()[0])<0 ) {
+            suggestedView = null;
+            suggestedviewId = null;
+        }
+        
+        viewChange(viewId,view.getMembers());
+        fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_CONF_RX,this,"Accepted View"));
+        
+        if ( suggestedviewId == null && hasHigherPriority(merged.getMembers(),membership.getMembers()) ) {
+            startElection(false);
+        }
+    }
+    
+    protected boolean isViewConf(CoordinationMessage msg) {
+        return Arrays.contains(msg.getType(),0,COORD_CONF,0,COORD_CONF.length);
+    }
+    
+    protected boolean hasHigherPriority(Member[] complete, Member[] local) {
+        if ( local == null || local.length == 0 ) return false;
+        if ( complete == null || complete.length == 0 ) return true;
+        AbsoluteOrder.absoluteOrder(complete);
+        AbsoluteOrder.absoluteOrder(local);
+        return (AbsoluteOrder.comp.compare(complete[0],local[0]) > 0);
+        
+    }
+
+    
+    /**
+     * Returns coordinator if one is available
+     * @return Member
+     */
+    public Member getCoordinator() {
+        return (view != null && view.hasMembers()) ? view.getMembers()[0] : null;
+    }
+    
+    public Member[] getView() {
+        return (view != null && view.hasMembers()) ? view.getMembers() : new Member[0];
+    }
+    
+    public UniqueId getViewId() {
+        return viewId;
+    }
+    
+    /**
+    * Block in/out messages while a election is going on
+    */
+   protected void halt() {
+
+   }
+
+   /**
+    * Release lock for in/out messages election is completed
+    */
+   protected void release() {
+
+   }
+
+   /**
+    * Wait for an election to end
+    */
+   protected void waitForRelease() {
+
+   }
+
+    
+//============================================================================================================    
+//              OVERRIDDEN METHODS FROM CHANNEL INTERCEPTOR BASE    
+//============================================================================================================
+    public void start(int svc) throws ChannelException {
+            if (membership == null) setupMembership();
+            if (started)return;
+            fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "Before start"));
+            super.start(startsvc);
+            started = true;
+            if (view == null) view = new Membership( (MemberImpl)super.getLocalMember(true), AbsoluteOrder.comp, true);
+            fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "After start"));
+            startElection(false);
+    }
+
+    public void stop(int svc) throws ChannelException {
+        try {
+            halt();
+            synchronized (electionMutex) {
+                if (!started)return;
+                started = false;
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "Before stop"));
+                super.stop(startsvc);
+                this.view = null;
+                this.viewId = null;
+                this.suggestedView = null;
+                this.suggestedviewId = null;
+                this.membership.reset();
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "After stop"));
+            }
+        }finally {
+            release();
+        }
+    }
+    
+    
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        waitForRelease();
+        super.sendMessage(destination, msg, payload);
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_ALIVE,0,COORD_ALIVE.length) ) {
+            //ignore message, its an alive message
+            fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Alive Message"));
+
+        } else if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_HEADER,0,COORD_HEADER.length) ) {
+            try {
+                CoordinationMessage cmsg = new CoordinationMessage(msg.getMessage());
+                Member[] cmbr = cmsg.getMembers();
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Coord Msg Arrived("+Arrays.toNameString(cmbr)+")"));
+                processCoordMessage(cmsg, msg.getAddress());
+            }catch ( ChannelException x ) {
+                log.error("Error processing coordination message. Could be fatal.",x);
+            }
+        } else {
+            super.messageReceived(msg);
+        }
+    }
+
+    public boolean accept(ChannelMessage msg) {
+        return super.accept(msg);
+    }
+
+    public void memberAdded(Member member) {
+        memberAdded(member,true);
+    }
+
+    public void memberAdded(Member member,boolean elect) {
+        try {
+            if ( membership == null ) setupMembership();
+            if ( membership.memberAlive((MemberImpl)member) ) super.memberAdded(member);
+            try {
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_ADD,this,"Member add("+member.getName()+")"));
+                if (started && elect) startElection(false);
+            }catch ( ChannelException x ) {
+                log.error("Unable to start election when member was added.",x);
+            }
+        }finally {
+        }
+        
+    }
+
+    public void memberDisappeared(Member member) {
+        try {
+            
+            membership.removeMember((MemberImpl)member);
+            super.memberDisappeared(member);
+            try {
+                fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_DEL,this,"Member remove("+member.getName()+")"));
+                if ( started && (isCoordinator() || isHighest()) ) 
+                    startElection(true); //to do, if a member disappears, only the coordinator can start
+            }catch ( ChannelException x ) {
+                log.error("Unable to start election when member was removed.",x);
+            }
+        }finally {
+        }
+    }
+    
+    public boolean isHighest() {
+        Member local = getLocalMember(false);
+        if ( membership.getMembers().length == 0 ) return true;
+        else return AbsoluteOrder.comp.compare(local,membership.getMembers()[0])<=0;
+    }
+    
+    public boolean isCoordinator() {
+        Member coord = getCoordinator();
+        return coord != null && getLocalMember(false).equals(coord);
+    }
+
+    public void heartbeat() {
+        try {
+            MemberImpl local = (MemberImpl)getLocalMember(false);
+            if ( view != null && (Arrays.diff(view,membership,local).length != 0 ||  Arrays.diff(membership,view,local).length != 0) ) {
+                if ( isHighest() ) {
+                    fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT, this,
+                                                               "Heartbeat found inconsistency, restart election"));
+                    startElection(true);
+                }            
+            }
+        } catch ( Exception x  ){
+            log.error("Unable to perform heartbeat.",x);
+        } finally {
+            super.heartbeat();
+        }
+    }
+
+    /**
+     * has members
+     */
+    public boolean hasMembers() {
+        
+        return membership.hasMembers();
+    }
+
+    /**
+     * Get all current cluster members
+     * @return all members or empty array
+     */
+    public Member[] getMembers() {
+        
+        return membership.getMembers();
+    }
+
+    /**
+     *
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr) {
+        
+        return membership.getMember(mbr);
+    }
+
+    /**
+     * Return the member that represents this node.
+     *
+     * @return Member
+     */
+    public Member getLocalMember(boolean incAlive) {
+        Member local = super.getLocalMember(incAlive);
+        if ( view == null && (local != null)) setupMembership();
+        return local;
+    }
+    
+    protected synchronized void setupMembership() {
+        if ( membership == null ) {
+            membership  = new Membership((MemberImpl)super.getLocalMember(true),AbsoluteOrder.comp,false);
+        }
+    }
+    
+    
+//============================================================================================================    
+//              HELPER CLASSES FOR COORDINATION
+//============================================================================================================
+    
+    
+   
+    
+    public static class CoordinationMessage {
+        //X{A-ldr, A-src, mbrs-A,B,C,D}
+        protected XByteBuffer buf;
+        protected MemberImpl leader;
+        protected MemberImpl source;
+        protected MemberImpl[] view;
+        protected UniqueId id;
+        protected byte[] type;
+        protected long timestamp = System.currentTimeMillis();
+        
+        public CoordinationMessage(XByteBuffer buf) {
+            this.buf = buf;
+            parse();
+        }
+
+        public CoordinationMessage(MemberImpl leader,
+                                   MemberImpl source, 
+                                   MemberImpl[] view,
+                                   UniqueId id,
+                                   byte[] type) {
+            this.buf = new XByteBuffer(4096,false);
+            this.leader = leader;
+            this.source = source;
+            this.view = view;
+            this.id = id;
+            this.type = type;
+            this.write();
+        }
+        
+
+        public byte[] getHeader() {
+            return NonBlockingCoordinator.COORD_HEADER;
+        }
+        
+        public MemberImpl getLeader() {
+            if ( leader == null ) parse();
+            return leader;
+        }
+        
+        public MemberImpl getSource() {
+            if ( source == null ) parse();
+            return source;
+        }
+        
+        public UniqueId getId() {
+            if ( id == null ) parse();
+            return id;
+        }
+        
+        public MemberImpl[] getMembers() {
+            if ( view == null ) parse();
+            return view;
+        }
+        
+        public byte[] getType() {
+            if (type == null ) parse();
+            return type;
+        }
+        
+        public XByteBuffer getBuffer() {
+            return this.buf;
+        }
+        
+        public void parse() {
+            //header
+            int offset = 16;
+            //leader
+            int ldrLen = buf.toInt(buf.getBytesDirect(),offset);
+            offset += 4;
+            byte[] ldr = new byte[ldrLen];
+            System.arraycopy(buf.getBytesDirect(),offset,ldr,0,ldrLen);
+            leader = MemberImpl.getMember(ldr);
+            offset += ldrLen;
+            //source
+            int srcLen = buf.toInt(buf.getBytesDirect(),offset);
+            offset += 4;
+            byte[] src = new byte[srcLen];
+            System.arraycopy(buf.getBytesDirect(),offset,src,0,srcLen);
+            source = MemberImpl.getMember(src);
+            offset += srcLen;
+            //view
+            int mbrCount = buf.toInt(buf.getBytesDirect(),offset);
+            offset += 4;
+            view = new MemberImpl[mbrCount];
+            for (int i=0; i<view.length; i++ ) {
+                int mbrLen = buf.toInt(buf.getBytesDirect(),offset);
+                offset += 4;
+                byte[] mbr = new byte[mbrLen];
+                System.arraycopy(buf.getBytesDirect(), offset, mbr, 0, mbrLen);
+                view[i] = MemberImpl.getMember(mbr);
+                offset += mbrLen;
+            }
+            //id
+            this.id = new UniqueId(buf.getBytesDirect(),offset,16);
+            offset += 16;
+            type = new byte[16];
+            System.arraycopy(buf.getBytesDirect(), offset, type, 0, type.length);
+            offset += 16;
+            
+        }
+        
+        public void write() {
+            buf.reset();
+            //header
+            buf.append(COORD_HEADER,0,COORD_HEADER.length);
+            //leader
+            byte[] ldr = leader.getData(false,false);
+            buf.append(ldr.length);
+            buf.append(ldr,0,ldr.length);
+            ldr = null;
+            //source
+            byte[] src = source.getData(false,false);
+            buf.append(src.length);
+            buf.append(src,0,src.length);
+            src = null;
+            //view
+            buf.append(view.length);
+            for (int i=0; i<view.length; i++ ) {
+                byte[] mbr = view[i].getData(false,false);
+                buf.append(mbr.length);
+                buf.append(mbr,0,mbr.length);
+            }
+            //id
+            buf.append(id.getBytes(),0,id.getBytes().length);
+            buf.append(type,0,type.length);
+        }
+    }
+    
+    public void fireInterceptorEvent(InterceptorEvent event) {
+        if (event instanceof CoordinationEvent &&
+            ((CoordinationEvent)event).type == CoordinationEvent.EVT_CONF_RX) 
+            log.info(event);
+    }
+    
+    public static class CoordinationEvent implements InterceptorEvent {
+        public static final int EVT_START = 1;
+        public static final int EVT_MBR_ADD = 2;
+        public static final int EVT_MBR_DEL = 3;
+        public static final int EVT_START_ELECT = 4;
+        public static final int EVT_PROCESS_ELECT = 5;
+        public static final int EVT_MSG_ARRIVE = 6;
+        public static final int EVT_PRE_MERGE = 7;
+        public static final int EVT_POST_MERGE = 8;
+        public static final int EVT_WAIT_FOR_MSG = 9;
+        public static final int EVT_SEND_MSG = 10;
+        public static final int EVT_STOP = 11;
+        public static final int EVT_CONF_RX = 12;
+        public static final int EVT_ELECT_ABANDONED = 13;
+        
+        int type;
+        ChannelInterceptor interceptor;
+        Member coord; 
+        Member[] mbrs;
+        String info;
+        Membership view;
+        Membership suggestedView;
+        public CoordinationEvent(int type,ChannelInterceptor interceptor, String info) {
+            this.type = type;
+            this.interceptor = interceptor;
+            this.coord = ((NonBlockingCoordinator)interceptor).getCoordinator();
+            this.mbrs = ((NonBlockingCoordinator)interceptor).membership.getMembers();
+            this.info = info;
+            this.view = ((NonBlockingCoordinator)interceptor).view;
+            this.suggestedView = ((NonBlockingCoordinator)interceptor).suggestedView;
+        }
+        
+        public int getEventType() {
+            return type;
+        }
+        
+        public String getEventTypeDesc() {
+            switch (type) {
+                case  EVT_START: return "EVT_START:"+info;
+                case  EVT_MBR_ADD: return "EVT_MBR_ADD:"+info;
+                case  EVT_MBR_DEL: return "EVT_MBR_DEL:"+info;
+                case  EVT_START_ELECT: return "EVT_START_ELECT:"+info;
+                case  EVT_PROCESS_ELECT: return "EVT_PROCESS_ELECT:"+info;
+                case  EVT_MSG_ARRIVE: return "EVT_MSG_ARRIVE:"+info;
+                case  EVT_PRE_MERGE: return "EVT_PRE_MERGE:"+info;
+                case  EVT_POST_MERGE: return "EVT_POST_MERGE:"+info;
+                case  EVT_WAIT_FOR_MSG: return "EVT_WAIT_FOR_MSG:"+info;
+                case  EVT_SEND_MSG: return "EVT_SEND_MSG:"+info;
+                case  EVT_STOP: return "EVT_STOP:"+info;
+                case  EVT_CONF_RX: return "EVT_CONF_RX:"+info;
+                case EVT_ELECT_ABANDONED: return "EVT_ELECT_ABANDONED:"+info;
+                default: return "Unknown";
+            }
+        }
+        
+        public ChannelInterceptor getInterceptor() {
+            return interceptor;
+        }
+        
+        public String toString() {
+            StringBuffer buf = new StringBuffer("CoordinationEvent[type=");
+            buf.append(type).append("\n\tLocal:");
+            Member local = interceptor.getLocalMember(false);
+            buf.append(local!=null?local.getName():"").append("\n\tCoord:");
+            buf.append(coord!=null?coord.getName():"").append("\n\tView:");
+            buf.append(Arrays.toNameString(view!=null?view.getMembers():null)).append("\n\tSuggested View:");
+            buf.append(Arrays.toNameString(suggestedView!=null?suggestedView.getMembers():null)).append("\n\tMembers:");
+            buf.append(Arrays.toNameString(mbrs)).append("\n\tInfo:");
+            buf.append(info).append("]");
+            return buf.toString();
+        }
+    }
+
+    
+
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/interceptors/OrderInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/OrderInterceptor.java
new file mode 100644 (file)
index 0000000..c4b1661
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.util.HashMap;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.XByteBuffer;
+
+
+
+/**
+ *
+ * The order interceptor guarantees that messages are received in the same order they were 
+ * sent.
+ * This interceptor works best with the ack=true setting. <br>
+ * There is no point in 
+ * using this with the replicationMode="fastasynchqueue" as this mode guarantees ordering.<BR>
+ * If you are using the mode ack=false replicationMode=pooled, and have a lot of concurrent threads,
+ * this interceptor can really slow you down, as many messages will be completely out of order
+ * and the queue might become rather large. If this is the case, then you might want to set 
+ * the value OrderInterceptor.maxQueue = 25 (meaning that we will never keep more than 25 messages in our queue)
+ * <br><b>Configuration Options</b><br>
+ * OrderInteceptor.expire=<milliseconds> - if a message arrives out of order, how long before we act on it <b>default=3000ms</b><br>
+ * OrderInteceptor.maxQueue=<max queue size> - how much can the queue grow to ensure ordering. 
+ *   This setting is useful to avoid OutOfMemoryErrors<b>default=Integer.MAX_VALUE</b><br>
+ * OrderInterceptor.forwardExpired=<boolean> - this flag tells the interceptor what to 
+ * do when a message has expired or the queue has grown larger than the maxQueue value.
+ * true means that the message is sent up the stack to the receiver that will receive and out of order message
+ * false means, forget the message and reset the message counter. <b>default=true</b>
+ * 
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class OrderInterceptor extends ChannelInterceptorBase {
+    private HashMap outcounter = new HashMap();
+    private HashMap incounter = new HashMap();
+    private HashMap incoming = new HashMap();
+    private long expire = 3000;
+    private boolean forwardExpired = true;
+    private int maxQueue = Integer.MAX_VALUE;
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        for ( int i=0; i<destination.length; i++ ) {
+            int nr = incCounter(destination[i]);
+            //reduce byte copy
+            msg.getMessage().append(nr);
+            try {
+                getNext().sendMessage(new Member[] {destination[i]}, msg, payload);
+            }finally {
+                msg.getMessage().trim(4);
+            }
+        }
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        int msgnr = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4);
+        msg.getMessage().trim(4);
+        MessageOrder order = new MessageOrder(msgnr,(ChannelMessage)msg.deepclone());
+        if ( processIncoming(order) ) processLeftOvers(msg.getAddress(),false);
+    }
+    
+    public synchronized void processLeftOvers(Member member, boolean force) {
+        MessageOrder tmp = (MessageOrder)incoming.get(member);
+        if ( force ) {
+            Counter cnt = getInCounter(member);
+            cnt.setCounter(Integer.MAX_VALUE);
+        }
+        if ( tmp!= null ) processIncoming(tmp);
+    }
+    /**
+     * 
+     * @param order MessageOrder
+     * @return boolean - true if a message expired and was processed
+     */
+    public synchronized boolean processIncoming(MessageOrder order) {
+        boolean result = false;
+        Member member = order.getMessage().getAddress();
+        Counter cnt = getInCounter(member);
+        
+        MessageOrder tmp = (MessageOrder)incoming.get(member);
+        if ( tmp != null ) {
+            order = MessageOrder.add(tmp,order);
+        }
+        
+        
+        while ( (order!=null) && (order.getMsgNr() <= cnt.getCounter())  ) {
+            //we are right on target. process orders
+            if ( order.getMsgNr() == cnt.getCounter() ) cnt.inc();
+            else if ( order.getMsgNr() > cnt.getCounter() ) cnt.setCounter(order.getMsgNr());
+            super.messageReceived(order.getMessage());
+            order.setMessage(null);
+            order = order.next;
+        }
+        MessageOrder head = order;
+        MessageOrder prev = null;
+        tmp = order;
+        //flag to empty out the queue when it larger than maxQueue
+        boolean empty = order!=null?order.getCount()>=maxQueue:false;
+        while ( tmp != null ) {
+            //process expired messages or empty out the queue
+            if ( tmp.isExpired(expire) || empty ) {
+                //reset the head
+                if ( tmp == head ) head = tmp.next;
+                cnt.setCounter(tmp.getMsgNr()+1);
+                if ( getForwardExpired() ) super.messageReceived(tmp.getMessage());
+                tmp.setMessage(null);
+                tmp = tmp.next;
+                if ( prev != null ) prev.next = tmp;  
+                result = true;
+            } else {
+                prev = tmp;
+                tmp = tmp.next;
+            }
+        }
+        if ( head == null ) incoming.remove(member);
+        else incoming.put(member, head);
+        return result;
+    }
+    
+    public void memberAdded(Member member) {
+        //notify upwards
+        getInCounter(member);
+        getOutCounter(member);
+        super.memberAdded(member);
+    }
+
+    public void memberDisappeared(Member member) {
+        //notify upwards
+        outcounter.remove(member);
+        incounter.remove(member);
+        //clear the remaining queue
+        processLeftOvers(member,true);
+        super.memberDisappeared(member);
+    }
+    
+    public int incCounter(Member mbr) { 
+        Counter cnt = getOutCounter(mbr);
+        return cnt.inc();
+    }
+    
+    public synchronized Counter getInCounter(Member mbr) {
+        Counter cnt = (Counter)incounter.get(mbr);
+        if ( cnt == null ) {
+            cnt = new Counter();
+            cnt.inc(); //always start at 1 for incoming
+            incounter.put(mbr,cnt);
+        }
+        return cnt;
+    }
+
+    public synchronized Counter getOutCounter(Member mbr) {
+        Counter cnt = (Counter)outcounter.get(mbr);
+        if ( cnt == null ) {
+            cnt = new Counter();
+            outcounter.put(mbr,cnt);
+        }
+        return cnt;
+    }
+
+    public static class Counter {
+        private int value = 0;
+        
+        public int getCounter() {
+            return value;
+        }
+        
+        public synchronized void setCounter(int counter) {
+            this.value = counter;
+        }
+        
+        public synchronized int inc() {
+            return ++value;
+        }
+    }
+    
+    public static class MessageOrder {
+        private long received = System.currentTimeMillis();
+        private MessageOrder next;
+        private int msgNr;
+        private ChannelMessage msg = null;
+        public MessageOrder(int msgNr,ChannelMessage msg) {
+            this.msgNr = msgNr;
+            this.msg = msg;
+        }
+        
+        public boolean isExpired(long expireTime) {
+            return (System.currentTimeMillis()-received) > expireTime;
+        }
+        
+        public ChannelMessage getMessage() {
+            return msg;
+        }
+        
+        public void setMessage(ChannelMessage msg) {
+            this.msg = msg;
+        }
+        
+        public void setNext(MessageOrder order) {
+            this.next = order;
+        }
+        public MessageOrder getNext() {
+            return next;
+        }
+        
+        public int getCount() {
+            int counter = 1;
+            MessageOrder tmp = next;
+            while ( tmp != null ) {
+                counter++;
+                tmp = tmp.next;
+            }
+            return counter;
+        }
+        
+        public static MessageOrder add(MessageOrder head, MessageOrder add) {
+            if ( head == null ) return add;
+            if ( add == null ) return head;
+            if ( head == add ) return add;
+
+            if ( head.getMsgNr() > add.getMsgNr() ) {
+                add.next = head;
+                return add;
+            }
+            
+            MessageOrder iter = head;
+            MessageOrder prev = null;
+            while ( iter.getMsgNr() < add.getMsgNr() && (iter.next !=null ) ) {
+                prev = iter;
+                iter = iter.next;
+            }
+            if ( iter.getMsgNr() < add.getMsgNr() ) {
+                //add after
+                add.next = iter.next;
+                iter.next = add;
+            } else if (iter.getMsgNr() > add.getMsgNr()) {
+                //add before
+                prev.next = add;
+                add.next = iter;
+                
+            } else {
+                throw new ArithmeticException("Message added has the same counter, synchronization bug. Disable the order interceptor");
+            }
+            
+            return head;
+        }
+        
+        public int getMsgNr() {
+            return msgNr;
+        }
+        
+        
+        
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public void setForwardExpired(boolean forwardExpired) {
+        this.forwardExpired = forwardExpired;
+    }
+
+    public void setMaxQueue(int maxQueue) {
+        this.maxQueue = maxQueue;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public boolean getForwardExpired() {
+        return forwardExpired;
+    }
+
+    public int getMaxQueue() {
+        return maxQueue;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java
new file mode 100644 (file)
index 0000000..6ee8736
--- /dev/null
@@ -0,0 +1,101 @@
+package org.apache.catalina.tribes.group.interceptors;
+
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.Member;
+import java.util.ArrayList;
+import org.apache.catalina.tribes.group.AbsoluteOrder;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.Channel;
+
+public class StaticMembershipInterceptor
+    extends ChannelInterceptorBase {
+    protected ArrayList members = new ArrayList();
+    protected Member localMember = null;
+
+    public StaticMembershipInterceptor() {
+        super();
+    }
+
+    public void addStaticMember(Member member) {
+        synchronized (members) {
+            if (!members.contains(member)) members.add(member);
+        }
+    }
+
+    public void removeStaticMember(Member member) {
+        synchronized (members) {
+            if (members.contains(member)) members.remove(member);
+        }
+    }
+
+    public void setLocalMember(Member member) {
+        this.localMember = member;
+    }
+
+    /**
+     * has members
+     */
+    public boolean hasMembers() {
+        return super.hasMembers() || (members.size()>0);
+    }
+
+    /**
+     * Get all current cluster members
+     * @return all members or empty array
+     */
+    public Member[] getMembers() {
+        if ( members.size() == 0 ) return super.getMembers();
+        else {
+            synchronized (members) {
+                Member[] others = super.getMembers();
+                Member[] result = new Member[members.size() + others.length];
+                for (int i = 0; i < others.length; i++) result[i] = others[i];
+                for (int i = 0; i < members.size(); i++) result[i + others.length] = (Member) members.get(i);
+                AbsoluteOrder.absoluteOrder(result);
+                return result;
+            }//sync
+        }//end if
+    }
+
+    /**
+     *
+     * @param mbr Member
+     * @return Member
+     */
+    public Member getMember(Member mbr) {
+        if ( members.contains(mbr) ) return (Member)members.get(members.indexOf(mbr));
+        else return super.getMember(mbr);
+    }
+
+    /**
+     * Return the member that represents this node.
+     *
+     * @return Member
+     */
+    public Member getLocalMember(boolean incAlive) {
+        if (this.localMember != null ) return localMember;
+        else return super.getLocalMember(incAlive);
+    }
+    
+    /**
+     * Send notifications upwards
+     * @param svc int
+     * @throws ChannelException
+     */
+    public void start(int svc) throws ChannelException {
+        if ( (Channel.SND_RX_SEQ&svc)==Channel.SND_RX_SEQ ) super.start(Channel.SND_RX_SEQ); 
+        if ( (Channel.SND_TX_SEQ&svc)==Channel.SND_TX_SEQ ) super.start(Channel.SND_TX_SEQ); 
+        final Member[] mbrs = (Member[])members.toArray(new Member[members.size()]);
+        final ChannelInterceptorBase base = this;
+        Thread t = new Thread() {
+            public void run() {
+                for (int i=0; i<mbrs.length; i++ ) {
+                    base.memberAdded(mbrs[i]);
+                }
+            }
+        };
+        t.start();
+        super.start(svc & (~Channel.SND_RX_SEQ) & (~Channel.SND_TX_SEQ));
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
new file mode 100644 (file)
index 0000000..10418e7
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelException.FaultyMember;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.RemoteProcessException;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.membership.Membership;
+import java.net.ConnectException;
+
+/**
+ * <p>Title: A perfect failure detector </p>
+ *
+ * <p>Description: The TcpFailureDetector is a useful interceptor
+ * that adds reliability to the membership layer.</p>
+ * <p>
+ * If the network is busy, or the system is busy so that the membership receiver thread
+ * is not getting enough time to update its table, members can be &quot;timed out&quot;
+ * This failure detector will intercept the memberDisappeared message(unless its a true shutdown message)
+ * and connect to the member using TCP.
+ * </p>
+ * <p>
+ * The TcpFailureDetector works in two ways. <br>
+ * 1. It intercepts memberDisappeared events
+ * 2. It catches send errors 
+ * </p>
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class TcpFailureDetector extends ChannelInterceptorBase {
+    
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( TcpFailureDetector.class );
+    
+    protected static byte[] TCP_FAIL_DETECT = new byte[] {
+        79, -89, 115, 72, 121, -126, 67, -55, -97, 111, -119, -128, -95, 91, 7, 20,
+        125, -39, 82, 91, -21, -15, 67, -102, -73, 126, -66, -113, -127, 103, 30, -74,
+        55, 21, -66, -121, 69, 126, 76, -88, -65, 10, 77, 19, 83, 56, 21, 50,
+        85, -10, -108, -73, 58, -6, 64, 120, -111, 4, 125, -41, 114, -124, -64, -43};      
+    
+    protected boolean performConnectTest = true;
+
+    protected long connectTimeout = 1000;//1 second default
+    
+    protected boolean performSendTest = true;
+
+    protected boolean performReadTest = false;
+    
+    protected long readTestTimeout = 5000;//5 seconds
+    
+    protected Membership membership = null;
+    
+    protected HashMap removeSuspects = new HashMap();
+    
+    protected HashMap addSuspects = new HashMap();
+    
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        try {
+            super.sendMessage(destination, msg, payload);
+        }catch ( ChannelException cx ) {
+            FaultyMember[] mbrs = cx.getFaultyMembers();
+            for ( int i=0; i<mbrs.length; i++ ) {
+                if ( mbrs[i].getCause()!=null &&  
+                     (!(mbrs[i].getCause() instanceof RemoteProcessException)) ) {//RemoteProcessException's are ok
+                    this.memberDisappeared(mbrs[i].getMember());
+                }//end if
+            }//for
+            throw cx;
+        }
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        //catch incoming 
+        boolean process = true;
+        if ( okToProcess(msg.getOptions()) ) {
+            //check to see if it is a testMessage, if so, process = false
+            process = ( (msg.getMessage().getLength() != TCP_FAIL_DETECT.length) ||
+                        (!Arrays.equals(TCP_FAIL_DETECT,msg.getMessage().getBytes()) ) );
+        }//end if
+            
+        //ignore the message, it doesnt have the flag set
+        if ( process ) super.messageReceived(msg);
+        else if ( log.isDebugEnabled() ) log.debug("Received a failure detector packet:"+msg);
+    }//messageReceived
+    
+    
+    public void memberAdded(Member member) {
+        if ( membership == null ) setupMembership();
+        boolean notify = false;
+        synchronized (membership) {
+            if (removeSuspects.containsKey(member)) {
+                //previously marked suspect, system below picked up the member again
+                removeSuspects.remove(member);
+            } else if (membership.getMember( (MemberImpl) member) == null){
+                //if we add it here, then add it upwards too
+                //check to see if it is alive
+                if (memberAlive(member)) {
+                    membership.memberAlive( (MemberImpl) member);
+                    notify = true;
+                } else {
+                    addSuspects.put(member, new Long(System.currentTimeMillis()));
+                }
+            }
+        }
+        if ( notify ) super.memberAdded(member);
+    }
+
+    public void memberDisappeared(Member member) {
+        if ( membership == null ) setupMembership();
+        boolean notify = false;
+        boolean shutdown = Arrays.equals(member.getCommand(),Member.SHUTDOWN_PAYLOAD);
+        if ( !shutdown ) log.info("Received memberDisappeared["+member+"] message. Will verify.");
+        synchronized (membership) {
+            //check to see if the member really is gone
+            //if the payload is not a shutdown message
+            if (shutdown || !memberAlive(member)) {
+                //not correct, we need to maintain the map
+                membership.removeMember( (MemberImpl) member);
+                removeSuspects.remove(member);
+                notify = true;
+            } else {
+                //add the member as suspect
+                removeSuspects.put(member, new Long(System.currentTimeMillis()));
+            }
+        }
+        if ( notify ) {
+            log.info("Verification complete. Member disappeared["+member+"]");
+            super.memberDisappeared(member);
+        } else {
+            log.info("Verification complete. Member still alive["+member+"]");
+
+        }
+    }
+    
+    public boolean hasMembers() {
+        if ( membership == null ) setupMembership();
+        return membership.hasMembers();
+    }
+
+    public Member[] getMembers() {
+        if ( membership == null ) setupMembership();
+        return membership.getMembers();
+    }
+
+    public Member getMember(Member mbr) {
+        if ( membership == null ) setupMembership();
+        return membership.getMember(mbr);
+    }
+
+    public Member getLocalMember(boolean incAlive) {
+        return super.getLocalMember(incAlive);
+    }
+    
+    public void heartbeat() {
+        try {
+            if (membership == null) setupMembership();
+            synchronized (membership) {
+                //update all alive times
+                Member[] members = super.getMembers();
+                for (int i = 0; members != null && i < members.length; i++) {
+                    if (membership.memberAlive( (MemberImpl) members[i])) {
+                        //we don't have this one in our membership, check to see if he/she is alive
+                        if (memberAlive(members[i])) {
+                            log.warn("Member added, even though we werent notified:" + members[i]);
+                            super.memberAdded(members[i]);
+                        } else {
+                            membership.removeMember( (MemberImpl) members[i]);
+                        } //end if
+                    } //end if
+                } //for
+
+                //check suspect members if they are still alive,
+                //if not, simply issue the memberDisappeared message
+                MemberImpl[] keys = (MemberImpl[]) removeSuspects.keySet().toArray(new MemberImpl[removeSuspects.size()]);
+                for (int i = 0; i < keys.length; i++) {
+                    MemberImpl m = (MemberImpl) keys[i];
+                    if (membership.getMember(m) != null && (!memberAlive(m))) {
+                        membership.removeMember(m);
+                        super.memberDisappeared(m);
+                        removeSuspects.remove(m);
+                        log.info("Suspect member, confirmed dead.["+m+"]");
+                    } //end if
+                }
+
+                //check add suspects members if they are alive now,
+                //if they are, simply issue the memberAdded message
+                keys = (MemberImpl[]) addSuspects.keySet().toArray(new MemberImpl[addSuspects.size()]);
+                for (int i = 0; i < keys.length; i++) {
+                    MemberImpl m = (MemberImpl) keys[i];
+                    if ( membership.getMember(m) == null && (memberAlive(m))) {
+                        membership.memberAlive(m);
+                        super.memberAdded(m);
+                        addSuspects.remove(m);
+                        log.info("Suspect member, confirmed alive.["+m+"]");
+                    } //end if
+                }
+            }
+        }catch ( Exception x ) {
+            log.warn("Unable to perform heartbeat on the TcpFailureDetector.",x);
+        } finally {
+            super.heartbeat();
+        }
+    }
+    
+    protected synchronized void setupMembership() {
+        if ( membership == null ) {
+            membership = new Membership((MemberImpl)super.getLocalMember(true));
+        }
+        
+    }
+    
+    protected boolean memberAlive(Member mbr) {
+        return memberAlive(mbr,TCP_FAIL_DETECT,performSendTest,performReadTest,readTestTimeout,connectTimeout,getOptionFlag());
+    }
+    
+    protected static boolean memberAlive(Member mbr, byte[] msgData, 
+                                         boolean sendTest, boolean readTest,
+                                         long readTimeout, long conTimeout,
+                                         int optionFlag) {
+        //could be a shutdown notification
+        if ( Arrays.equals(mbr.getCommand(),Member.SHUTDOWN_PAYLOAD) ) return false;
+        
+        Socket socket = new Socket();        
+        try {
+            InetAddress ia = InetAddress.getByAddress(mbr.getHost());
+            InetSocketAddress addr = new InetSocketAddress(ia, mbr.getPort());
+            socket.setSoTimeout((int)readTimeout);
+            socket.connect(addr, (int) conTimeout);
+            if ( sendTest ) {
+                ChannelData data = new ChannelData(true);
+                data.setAddress(mbr);
+                data.setMessage(new XByteBuffer(msgData,false));
+                data.setTimestamp(System.currentTimeMillis());
+                int options = optionFlag | Channel.SEND_OPTIONS_BYTE_MESSAGE;
+                if ( readTest ) options = (options | Channel.SEND_OPTIONS_USE_ACK);
+                else options = (options & (~Channel.SEND_OPTIONS_USE_ACK));
+                data.setOptions(options);
+                byte[] message = XByteBuffer.createDataPackage(data);
+                socket.getOutputStream().write(message);
+                if ( readTest ) {
+                    int length = socket.getInputStream().read(message);
+                    return length > 0;
+                }
+            }//end if
+            return true;
+        } catch ( SocketTimeoutException sx) {
+            //do nothing, we couldn't connect
+        } catch ( ConnectException cx) {
+            //do nothing, we couldn't connect
+        }catch (Exception x ) {
+            log.error("Unable to perform failure detection check, assuming member down.",x);
+        } finally {
+            try {socket.close(); } catch ( Exception ignore ){}
+        }
+        return false;
+    }
+
+
+
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java
new file mode 100644 (file)
index 0000000..972a7c5
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ */
+
+package org.apache.catalina.tribes.group.interceptors;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import java.text.DecimalFormat;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+
+/**
+ *
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class ThroughputInterceptor extends ChannelInterceptorBase {
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ThroughputInterceptor.class);
+
+    double mbTx = 0;
+    double mbAppTx = 0;
+    double mbRx = 0;
+    double timeTx = 0;
+    double lastCnt = 0;
+    AtomicLong msgTxCnt = new AtomicLong(1);
+    AtomicLong msgRxCnt = new AtomicLong(0);
+    AtomicLong msgTxErr = new AtomicLong(0);
+    int interval = 10000;
+    AtomicInteger access = new AtomicInteger(0);
+    long txStart = 0;
+    long rxStart = 0;
+    DecimalFormat df = new DecimalFormat("#0.00");
+
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+        if ( access.addAndGet(1) == 1 ) txStart = System.currentTimeMillis();
+        long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength());
+        try {
+            super.sendMessage(destination, msg, payload);
+        }catch ( ChannelException x ) {
+            msgTxErr.addAndGet(1);
+            access.addAndGet(-1);
+            throw x;
+        } 
+        mbTx += ((double)(bytes*destination.length))/(1024d*1024d);
+        mbAppTx += ((double)(bytes))/(1024d*1024d);
+        if ( access.addAndGet(-1) == 0 ) {
+            long stop = System.currentTimeMillis();
+            timeTx += ( (double) (stop - txStart)) / 1000d;
+            if ((msgTxCnt.get() / interval) >= lastCnt) {
+                lastCnt++;
+                report(timeTx);
+            }
+        }
+        msgTxCnt.addAndGet(1);
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        if ( rxStart == 0 ) rxStart = System.currentTimeMillis();
+        long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength());
+        mbRx += ((double)bytes)/(1024d*1024d);
+        msgRxCnt.addAndGet(1);
+        if ( msgRxCnt.get() % interval == 0 ) report(timeTx);
+        super.messageReceived(msg);
+        
+    }
+    
+    public void report(double timeTx) {
+        StringBuffer buf = new StringBuffer("ThroughputInterceptor Report[\n\tTx Msg:");
+        buf.append(msgTxCnt).append(" messages\n\tSent:");
+        buf.append(df.format(mbTx));
+        buf.append(" MB (total)\n\tSent:");
+        buf.append(df.format(mbAppTx));
+        buf.append(" MB (application)\n\tTime:");
+        buf.append(df.format(timeTx));
+        buf.append(" seconds\n\tTx Speed:");
+        buf.append(df.format(mbTx/timeTx));
+        buf.append(" MB/sec (total)\n\tTxSpeed:");
+        buf.append(df.format(mbAppTx/timeTx));
+        buf.append(" MB/sec (application)\n\tError Msg:");
+        buf.append(msgTxErr).append("\n\tRx Msg:");
+        buf.append(msgRxCnt);
+        buf.append(" messages\n\tRx Speed:");
+        buf.append(df.format(mbRx/((double)((System.currentTimeMillis()-rxStart)/1000))));
+        buf.append(" MB/sec (since 1st msg)\n\tReceived:");
+        buf.append(df.format(mbRx)).append(" MB]\n");
+        if ( log.isInfoEnabled() ) log.info(buf);
+    }
+    
+    public void setInterval(int interval) {
+        this.interval = interval;
+    }
+
+    public int getInterval() {
+        return interval;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java
new file mode 100644 (file)
index 0000000..bb53149
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.util.HashMap;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+import org.apache.catalina.tribes.util.Arrays;
+import org.apache.catalina.tribes.UniqueId;
+import java.util.Map;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class TwoPhaseCommitInterceptor extends ChannelInterceptorBase {
+
+    public static final byte[] START_DATA = new byte[] {113, 1, -58, 2, -34, -60, 75, -78, -101, -12, 32, -29, 32, 111, -40, 4};
+    public static final byte[] END_DATA = new byte[] {54, -13, 90, 110, 47, -31, 75, -24, -81, -29, 36, 52, -58, 77, -110, 56};
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(TwoPhaseCommitInterceptor.class);
+
+    protected HashMap messages = new HashMap();
+    protected long expire = 1000 * 60; //one minute expiration
+    protected boolean deepclone = true;
+
+    public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws
+        ChannelException {
+        //todo, optimize, if destination.length==1, then we can do
+        //msg.setOptions(msg.getOptions() & (~getOptionFlag())
+        //and just send one message
+        if (okToProcess(msg.getOptions()) ) {
+            super.sendMessage(destination, msg, null);
+            ChannelMessage confirmation = null;
+            if ( deepclone ) confirmation = (ChannelMessage)msg.deepclone();
+            else confirmation = (ChannelMessage)msg.clone();
+            confirmation.getMessage().reset();
+            UUIDGenerator.randomUUID(false,confirmation.getUniqueId(),0);
+            confirmation.getMessage().append(START_DATA,0,START_DATA.length);
+            confirmation.getMessage().append(msg.getUniqueId(),0,msg.getUniqueId().length);
+            confirmation.getMessage().append(END_DATA,0,END_DATA.length);
+            super.sendMessage(destination,confirmation,payload);
+        } else {
+            //turn off two phase commit
+            //this wont work if the interceptor has 0 as a flag
+            //since there is no flag to turn off
+            //msg.setOptions(msg.getOptions() & (~getOptionFlag()));
+            super.sendMessage(destination, msg, payload);
+        }
+    }
+
+    public void messageReceived(ChannelMessage msg) {
+        if (okToProcess(msg.getOptions())) {
+            if ( msg.getMessage().getLength() == (START_DATA.length+msg.getUniqueId().length+END_DATA.length) &&
+                 Arrays.contains(msg.getMessage().getBytesDirect(),0,START_DATA,0,START_DATA.length) &&
+                 Arrays.contains(msg.getMessage().getBytesDirect(),START_DATA.length+msg.getUniqueId().length,END_DATA,0,END_DATA.length) ) {
+                UniqueId id = new UniqueId(msg.getMessage().getBytesDirect(),START_DATA.length,msg.getUniqueId().length);
+                MapEntry original = (MapEntry)messages.get(id);
+                if ( original != null ) {
+                    super.messageReceived(original.msg);
+                    messages.remove(id);
+                } else log.warn("Received a confirmation, but original message is missing. Id:"+Arrays.toString(id.getBytes()));
+            } else {
+                UniqueId id = new UniqueId(msg.getUniqueId());
+                MapEntry entry = new MapEntry((ChannelMessage)msg.deepclone(),id,System.currentTimeMillis());
+                messages.put(id,entry);
+            }
+        } else {
+            super.messageReceived(msg);
+        }
+    }
+
+    public boolean getDeepclone() {
+        return deepclone;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setDeepclone(boolean deepclone) {
+        this.deepclone = deepclone;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+    
+    public void heartbeat() {
+        try {
+            long now = System.currentTimeMillis();
+            Map.Entry[] entries = (Map.Entry[])messages.entrySet().toArray(new Map.Entry[messages.size()]);
+            for (int i=0; i<entries.length; i++ ) {
+                MapEntry entry = (MapEntry)entries[i].getValue();
+                if ( entry.expired(now,expire) ) {
+                    log.info("Message ["+entry.id+"] has expired. Removing.");
+                    messages.remove(entry.id);
+                }//end if
+            }
+        } catch ( Exception x ) {
+            log.warn("Unable to perform heartbeat on the TwoPhaseCommit interceptor.",x);
+        } finally {
+            super.heartbeat();
+        }
+    }
+    
+    public static class MapEntry {
+        public ChannelMessage msg;
+        public UniqueId id;
+        public long timestamp;
+        
+        public MapEntry(ChannelMessage msg, UniqueId id, long timestamp) {
+            this.msg = msg;
+            this.id = id;
+            this.timestamp = timestamp;
+        }
+        public boolean expired(long now, long expiration) {
+            return (now - timestamp ) > expiration;
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/io/BufferPool.java b/java/org/apache/catalina/tribes/io/BufferPool.java
new file mode 100644 (file)
index 0000000..48b5e8b
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.io;
+
+import org.apache.commons.logging.LogFactory;
+
+import org.apache.commons.logging.Log;
+
+/**
+ *
+ * @author Filip Hanik
+ * 
+ * @version 1.0
+ */
+public class BufferPool {
+    protected static Log log = LogFactory.getLog(BufferPool.class);
+
+    public static int DEFAULT_POOL_SIZE = 100*1024*1024; //100MB
+
+
+
+    protected static BufferPool instance = null;
+    protected BufferPoolAPI pool = null;
+
+    private BufferPool(BufferPoolAPI pool) {
+        this.pool = pool;
+    }
+
+    public XByteBuffer getBuffer(int minSize, boolean discard) {
+        if ( pool != null ) return pool.getBuffer(minSize, discard);
+        else return new XByteBuffer(minSize,discard);
+    }
+
+    public void returnBuffer(XByteBuffer buffer) {
+        if ( pool != null ) pool.returnBuffer(buffer);
+    }
+
+    public void clear() {
+        if ( pool != null ) pool.clear();
+    }
+
+
+    public static BufferPool getBufferPool() {
+        if (  (instance == null) ) {
+            synchronized (BufferPool.class) {
+                if ( instance == null ) {
+                   BufferPoolAPI pool = null;
+                   Class clazz = null;
+                   try {
+                       clazz = Class.forName("org.apache.catalina.tribes.io.BufferPool15Impl");
+                       pool = (BufferPoolAPI)clazz.newInstance();
+                   } catch ( Throwable x ) {
+                       try {
+                           clazz = Class.forName("org.apache.catalina.tribes.io.BufferPool14Impl");
+                           pool = (BufferPoolAPI)clazz.newInstance();
+                       } catch ( Throwable e ) {
+                           log.warn("Unable to initilize BufferPool, not pooling XByteBuffer objects:"+x.getMessage());
+                           if ( log.isDebugEnabled() ) log.debug("Unable to initilize BufferPool, not pooling XByteBuffer objects:",x);
+                       }
+                   }
+                   pool.setMaxSize(DEFAULT_POOL_SIZE);
+                   log.info("Created a buffer pool with max size:"+DEFAULT_POOL_SIZE+" bytes of type:"+(clazz!=null?clazz.getName():"null"));
+                   instance = new BufferPool(pool);
+                }//end if
+            }//sync
+        }//end if
+        return instance;
+    }
+
+
+    public static interface BufferPoolAPI {
+        public void setMaxSize(int bytes);
+
+        public XByteBuffer getBuffer(int minSize, boolean discard);
+
+        public void returnBuffer(XByteBuffer buffer);
+
+        public void clear();
+    }    
+}
diff --git a/java/org/apache/catalina/tribes/io/BufferPool14Impl.java b/java/org/apache/catalina/tribes/io/BufferPool14Impl.java
new file mode 100644 (file)
index 0000000..f2b8750
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.io;
+
+import java.util.Queue;
+import java.util.LinkedList;
+
+
+/**
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+class BufferPool14Impl implements BufferPool.BufferPoolAPI {
+    protected int maxSize;
+    protected int size = 0;
+    protected LinkedList queue = new LinkedList();
+
+    public void setMaxSize(int bytes) {
+        this.maxSize = bytes;
+    }
+    
+    public synchronized int addAndGet(int val) {
+        size = size + (val);
+        return size;
+    }
+    
+    
+
+    public synchronized XByteBuffer getBuffer(int minSize, boolean discard) {
+        XByteBuffer buffer = (XByteBuffer)(queue.size()>0?queue.remove(0):null);
+        if ( buffer != null ) addAndGet(-buffer.getCapacity());
+        if ( buffer == null ) buffer = new XByteBuffer(minSize,discard);
+        else if ( buffer.getCapacity() <= minSize ) buffer.expand(minSize);
+        buffer.setDiscard(discard);
+        buffer.reset();
+        return buffer;
+    }
+
+    public synchronized void returnBuffer(XByteBuffer buffer) {
+        if ( (size + buffer.getCapacity()) <= maxSize ) {
+            addAndGet(buffer.getCapacity());
+            queue.add(buffer);
+        }
+    }
+
+    public synchronized void clear() {
+        queue.clear();
+        size = 0;
+    }
+
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/io/BufferPool15Impl.java b/java/org/apache/catalina/tribes/io/BufferPool15Impl.java
new file mode 100644 (file)
index 0000000..7cf5e5f
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.io;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+class BufferPool15Impl implements BufferPool.BufferPoolAPI {
+    protected int maxSize;
+    protected AtomicInteger size = new AtomicInteger(0);
+    protected ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
+
+    public void setMaxSize(int bytes) {
+        this.maxSize = bytes;
+    }
+
+
+    public XByteBuffer getBuffer(int minSize, boolean discard) {
+        XByteBuffer buffer = (XByteBuffer)queue.poll();
+        if ( buffer != null ) size.addAndGet(-buffer.getCapacity());
+        if ( buffer == null ) buffer = new XByteBuffer(minSize,discard);
+        else if ( buffer.getCapacity() <= minSize ) buffer.expand(minSize);
+        buffer.setDiscard(discard);
+        buffer.reset();
+        return buffer;
+    }
+
+    public void returnBuffer(XByteBuffer buffer) {
+        if ( (size.get() + buffer.getCapacity()) <= maxSize ) {
+            size.addAndGet(buffer.getCapacity());
+            queue.offer(buffer);
+        }
+    }
+
+    public void clear() {
+        queue.clear();
+        size.set(0);
+    }
+
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/io/ChannelData.java b/java/org/apache/catalina/tribes/io/ChannelData.java
new file mode 100644 (file)
index 0000000..42bdb83
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * Copyright 1999,2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.io;
+
+import java.util.Arrays;
+
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+import org.apache.catalina.tribes.Channel;
+import java.sql.Timestamp;
+
+/**
+ * The <code>ChannelData</code> object is used to transfer a message through the 
+ * channel interceptor stack and eventually out on a transport to be sent 
+ * to another node. While the message is being processed by the different 
+ * interceptors, the message data can be manipulated as each interceptor seems appropriate.
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 377484 $ $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ * 
+ */
+public class ChannelData implements ChannelMessage {
+    public static ChannelData[] EMPTY_DATA_ARRAY = new ChannelData[0];
+    
+    public static boolean USE_SECURE_RANDOM_FOR_UUID = false;
+    
+    /**
+     * The options this message was sent with
+     */
+    private int options = 0 ;
+    /**
+     * The message data, stored in a dynamic buffer
+     */
+    private XByteBuffer message ;
+    /**
+     * The timestamp that goes with this message
+     */
+    private long timestamp ;
+    /**
+     * A unique message id
+     */
+    private byte[] uniqueId ;
+    /**
+     * The source or reply-to address for this message
+     */
+    private Member address;
+
+    /**
+     * Creates an empty channel data with a new unique Id
+     * @see #ChannelData(boolean)
+     */
+    public ChannelData() {
+        this(true);
+    }
+    
+    /**
+     * Create an empty channel data object
+     * @param generateUUID boolean - if true, a unique Id will be generated
+     */
+    public ChannelData(boolean generateUUID) {
+        if ( generateUUID ) generateUUID();
+    }
+    
+    
+    
+    /**
+     * Creates a new channel data object with data
+     * @param uniqueId - unique message id
+     * @param message - message data
+     * @param timestamp - message timestamp
+     */
+    public ChannelData(byte[] uniqueId, XByteBuffer message, long timestamp) {
+        this.uniqueId = uniqueId;
+        this.message = message;
+        this.timestamp = timestamp;
+    }
+    
+    /**
+     * @return Returns the message byte buffer
+     */
+    public XByteBuffer getMessage() {
+        return message;
+    }
+    /**
+     * @param message The message to send.
+     */
+    public void setMessage(XByteBuffer message) {
+        this.message = message;
+    }
+    /**
+     * @return Returns the timestamp.
+     */
+    public long getTimestamp() {
+        return timestamp;
+    }
+    /**
+     * @param timestamp The timestamp to send
+     */
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+    /**
+     * @return Returns the uniqueId.
+     */
+    public byte[] getUniqueId() {
+        return uniqueId;
+    }
+    /**
+     * @param uniqueId The uniqueId to send.
+     */
+    public void setUniqueId(byte[] uniqueId) {
+        this.uniqueId = uniqueId;
+    }
+    /**
+     * @return returns the message options 
+     * see org.apache.catalina.tribes.Channel#sendMessage(org.apache.catalina.tribes.Member[], java.io.Serializable, int)
+     *                                                 
+     */
+    public int getOptions() {
+        return options;
+    }
+    /**
+     * @param sets the message options
+     */
+    public void setOptions(int options) {
+        this.options = options;
+    }
+    
+    /**
+     * Returns the source or reply-to address
+     * @return Member
+     */
+    public Member getAddress() {
+        return address;
+    }
+
+    /**
+     * Sets the source or reply-to address
+     * @param address Member
+     */
+    public void setAddress(Member address) {
+        this.address = address;
+    }
+    
+    /**
+     * Generates a UUID and invokes setUniqueId
+     */
+    public void generateUUID() {
+        byte[] data = new byte[16];
+        UUIDGenerator.randomUUID(USE_SECURE_RANDOM_FOR_UUID,data,0);
+        setUniqueId(data);
+    }
+
+    public int getDataPackageLength() {
+        int length = 
+            4 + //options
+            8 + //timestamp  off=4
+            4 + //unique id length off=12
+            uniqueId.length+ //id data off=12+uniqueId.length
+            4 + //addr length off=12+uniqueId.length+4
+            ((MemberImpl)address).getDataLength()+ //member data off=12+uniqueId.length+4+add.length
+            4 + //message length off=12+uniqueId.length+4+add.length+4
+            message.getLength();
+        return length;
+
+    }
+    
+    /**
+     * Serializes the ChannelData object into a byte[] array
+     * @return byte[]
+     */
+    public byte[] getDataPackage()  {
+        int length = getDataPackageLength();
+        byte[] data = new byte[length];
+        int offset = 0;
+        return getDataPackage(data,offset);
+    }
+
+    public byte[] getDataPackage(byte[] data, int offset)  {
+        byte[] addr = ((MemberImpl)address).getData(false);
+        XByteBuffer.toBytes(options,data,offset);
+        offset += 4; //options
+        XByteBuffer.toBytes(timestamp,data,offset);
+        offset += 8; //timestamp
+        XByteBuffer.toBytes(uniqueId.length,data,offset);
+        offset += 4; //uniqueId.length
+        System.arraycopy(uniqueId,0,data,offset,uniqueId.length);
+        offset += uniqueId.length; //uniqueId data
+        XByteBuffer.toBytes(addr.length,data,offset);
+        offset += 4; //addr.length
+        System.arraycopy(addr,0,data,offset,addr.length);
+        offset += addr.length; //addr data
+        XByteBuffer.toBytes(message.getLength(),data,offset);
+        offset += 4; //message.length
+        System.arraycopy(message.getBytesDirect(),0,data,offset,message.getLength());
+        offset += message.getLength(); //message data
+        return data;
+    }
+    
+    /**
+     * Deserializes a ChannelData object from a byte array
+     * @param b byte[]
+     * @return ChannelData
+     */
+    public static ChannelData getDataFromPackage(XByteBuffer xbuf)  {
+        ChannelData data = new ChannelData(false);
+        int offset = 0;
+        data.setOptions(XByteBuffer.toInt(xbuf.getBytesDirect(),offset));
+        offset += 4; //options
+        data.setTimestamp(XByteBuffer.toLong(xbuf.getBytesDirect(),offset));
+        offset += 8; //timestamp
+        data.uniqueId = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)];
+        offset += 4; //uniqueId length
+        System.arraycopy(xbuf.getBytesDirect(),offset,data.uniqueId,0,data.uniqueId.length);
+        offset += data.uniqueId.length; //uniqueId data
+        byte[] addr = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)];
+        offset += 4; //addr length
+        System.arraycopy(xbuf.getBytesDirect(),offset,addr,0,addr.length);
+        data.setAddress(MemberImpl.getMember(addr));
+        offset += addr.length; //addr data
+        int xsize = XByteBuffer.toInt(xbuf.getBytesDirect(),offset);
+        offset += 4; //xsize length
+        System.arraycopy(xbuf.getBytesDirect(),offset,xbuf.getBytesDirect(),0,xsize);
+        xbuf.setLength(xsize);
+        data.message = xbuf;
+        return data;
+
+    }
+
+    public static ChannelData getDataFromPackage(byte[] b)  {
+        ChannelData data = new ChannelData(false);
+        int offset = 0;
+        data.setOptions(XByteBuffer.toInt(b,offset));
+        offset += 4; //options
+        data.setTimestamp(XByteBuffer.toLong(b,offset));
+        offset += 8; //timestamp
+        data.uniqueId = new byte[XByteBuffer.toInt(b,offset)];
+        offset += 4; //uniqueId length
+        System.arraycopy(b,offset,data.uniqueId,0,data.uniqueId.length);
+        offset += data.uniqueId.length; //uniqueId data
+        byte[] addr = new byte[XByteBuffer.toInt(b,offset)];
+        offset += 4; //addr length
+        System.arraycopy(b,offset,addr,0,addr.length);
+        data.setAddress(MemberImpl.getMember(addr));
+        offset += addr.length; //addr data
+        int xsize = XByteBuffer.toInt(b,offset);
+        //data.message = new XByteBuffer(new byte[xsize],false);
+        data.message = BufferPool.getBufferPool().getBuffer(xsize,false);
+        offset += 4; //message length
+        System.arraycopy(b,offset,data.message.getBytesDirect(),0,xsize);
+        data.message.append(b,offset,xsize);
+        offset += xsize; //message data
+        return data;
+    }
+    
+    public int hashCode() {
+        return XByteBuffer.toInt(getUniqueId(),0);
+    }
+    
+    /**
+     * Compares to ChannelData objects, only compares on getUniqueId().equals(o.getUniqueId())
+     * @param o Object
+     * @return boolean
+     */
+    public boolean equals(Object o) {
+        if ( o instanceof ChannelData ) {
+            return Arrays.equals(getUniqueId(),((ChannelData)o).getUniqueId());
+        } else return false;
+    }
+    
+    /**
+     * Create a shallow clone, only the data gets recreated
+     * @return ClusterData
+     */
+    public Object clone() {
+//        byte[] d = this.getDataPackage();
+//        return ClusterData.getDataFromPackage(d);
+        ChannelData clone = new ChannelData(false);
+        clone.options = this.options;
+        clone.message = new XByteBuffer(this.message.getBytesDirect(),false);
+        clone.timestamp = this.timestamp;
+        clone.uniqueId = this.uniqueId;
+        clone.address = this.address;
+        return clone;
+    }
+    
+    /**
+     * Complete clone
+     * @return ClusterData
+     */
+    public Object deepclone() {
+        byte[] d = this.getDataPackage();
+        return ChannelData.getDataFromPackage(d);
+    }
+    
+    /**
+     * Utility method, returns true if the options flag indicates that an ack
+     * is to be sent after the message has been received and processed
+     * @param options int - the options for the message
+     * @return boolean 
+     * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK
+     * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK
+     */
+    public static boolean sendAckSync(int options) {
+        return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) &&
+            ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) == Channel.SEND_OPTIONS_SYNCHRONIZED_ACK);
+    }
+
+
+    /**
+     * Utility method, returns true if the options flag indicates that an ack
+     * is to be sent after the message has been received but not yet processed
+     * @param options int - the options for the message
+     * @return boolean 
+     * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK
+     * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK
+     */
+    public static boolean sendAckAsync(int options) {
+        return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) &&
+            ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) != Channel.SEND_OPTIONS_SYNCHRONIZED_ACK);
+    }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("ClusterData[src=");
+        buf.append(getAddress()).append("; id=");
+        buf.append(bToS(getUniqueId())).append("; sent=");
+        buf.append(new Timestamp(this.getTimestamp()).toString()).append("]");
+        return buf.toString();
+    }
+    
+    public static String bToS(byte[] data) {
+        StringBuffer buf = new StringBuffer(4*16);
+        buf.append("{");
+        for (int i=0; data!=null && i<data.length; i++ ) buf.append(String.valueOf(data[i])).append(" ");
+        buf.append("}");
+        return buf.toString();
+    }
+
+    
+}
diff --git a/java/org/apache/catalina/tribes/io/DirectByteArrayOutputStream.java b/java/org/apache/catalina/tribes/io/DirectByteArrayOutputStream.java
new file mode 100644 (file)
index 0000000..946e084
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Byte array output stream that exposes the byte array directly
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class DirectByteArrayOutputStream extends OutputStream {
+    
+    private XByteBuffer buffer;
+    
+    public DirectByteArrayOutputStream(int size) {
+        buffer = new XByteBuffer(size,false);
+    }
+
+    /**
+     * Writes the specified byte to this output stream.
+     *
+     * @param b the <code>byte</code>.
+     * @throws IOException if an I/O error occurs. In particular, an
+     *   <code>IOException</code> may be thrown if the output stream has
+     *   been closed.
+     * @todo Implement this java.io.OutputStream method
+     */
+    public void write(int b) throws IOException {
+        buffer.append((byte)b);
+    }
+    
+    public int size() {
+        return buffer.getLength();
+    }
+    
+    public byte[] getArrayDirect() {
+        return buffer.getBytesDirect();
+    }
+    
+    public byte[] getArray() {
+        return buffer.getBytes();
+    }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/io/ListenCallback.java b/java/org/apache/catalina/tribes/io/ListenCallback.java
new file mode 100644 (file)
index 0000000..c0cc9a0
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.io;
+
+import org.apache.catalina.tribes.ChannelMessage;
+
+
+
+/**
+ * Internal interface, similar to the MessageListener but used 
+ * at the IO base
+ * The listen callback interface is used by the replication system
+ * when data has been received. The interface does not care about
+ * objects and marshalling and just passes the bytes straight through.
+ * @author Filip Hanik
+ * @version $Revision: 303987 $, $Date: 2005-07-08 15:50:30 -0500 (Fri, 08 Jul 2005) $
+ */
+public interface ListenCallback
+{
+    /**
+     * This method is invoked on the callback object to notify it that new data has
+     * been received from one of the cluster nodes.
+     * @param data - the message bytes received from the cluster/replication system
+     */
+     public void messageDataReceived(ChannelMessage data);
+     
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/io/ObjectReader.java b/java/org/apache/catalina/tribes/io/ObjectReader.java
new file mode 100644 (file)
index 0000000..c69e8d2
--- /dev/null
@@ -0,0 +1,164 @@
+/*\r
+ * Copyright 1999,2004-2005 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.apache.catalina.tribes.io;\r
+\r
+import java.io.IOException;\r
+import java.net.Socket;\r
+import java.nio.ByteBuffer;\r
+import java.nio.channels.SocketChannel;\r
+\r
+import org.apache.catalina.tribes.ChannelMessage;\r
+\r
+\r
+\r
+/**\r
+ * The object reader object is an object used in conjunction with\r
+ * java.nio TCP messages. This object stores the message bytes in a\r
+ * <code>XByteBuffer</code> until a full package has been received.\r
+ * This object uses an XByteBuffer which is an extendable object buffer that also allows\r
+ * for message encoding and decoding.\r
+ *\r
+ * @author Filip Hanik\r
+ * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $\r
+ */\r
+public class ObjectReader {\r
+\r
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ObjectReader.class);\r
+\r
+    private XByteBuffer buffer;\r
+    \r
+    protected long lastAccess = System.currentTimeMillis();\r
+    \r
+    protected boolean accessed = false;\r
+    private boolean cancelled;\r
+\r
+    /**\r
+     * Creates an <code>ObjectReader</code> for a TCP NIO socket channel\r
+     * @param channel - the channel to be read.\r
+     */\r
+    public ObjectReader(SocketChannel channel) {\r
+        this(channel.socket());\r
+    }\r
+    \r
+    /**\r
+     * Creates an <code>ObjectReader</code> for a TCP socket\r
+     * @param socket Socket\r
+     */\r
+    public ObjectReader(Socket socket) {\r
+        try{\r
+            this.buffer = new XByteBuffer(socket.getReceiveBufferSize(), true);\r
+        }catch ( IOException x ) {\r
+            //unable to get buffer size\r
+            log.warn("Unable to retrieve the socket receiver buffer size, setting to default 43800 bytes.");\r
+            this.buffer = new XByteBuffer(43800,true);\r
+        }\r
+    }\r
+    \r
+    public synchronized void access() {\r
+        this.accessed = true;\r
+        this.lastAccess = System.currentTimeMillis();\r
+    }\r
+    \r
+    public synchronized void finish() {\r
+        this.accessed = false;\r
+        this.lastAccess = System.currentTimeMillis();\r
+    }\r
+    \r
+    public boolean isAccessed() {\r
+        return this.accessed;\r
+    }\r
+\r
+    /**\r
+     * Append new bytes to buffer. \r
+     * @see XByteBuffer#countPackages()\r
+     * @param data new transfer buffer\r
+     * @param off offset\r
+     * @param len length in buffer\r
+     * @return number of messages that sended to callback\r
+     * @throws java.io.IOException\r
+     */\r
+    public int append(ByteBuffer data, int len, boolean count) throws java.io.IOException {\r
+       buffer.append(data,len);\r
+       int pkgCnt = -1;\r
+       if ( count ) pkgCnt = buffer.countPackages();\r
+       return pkgCnt;\r
+   }\r
+\r
+     public int append(byte[] data,int off,int len, boolean count) throws java.io.IOException {\r
+        buffer.append(data,off,len);\r
+        int pkgCnt = -1;\r
+        if ( count ) pkgCnt = buffer.countPackages();\r
+        return pkgCnt;\r
+    }\r
+\r
+    /**\r
+     * Send buffer to cluster listener (callback).\r
+     * Is message complete receiver send message to callback?\r
+     *\r
+     * @see org.apache.catalina.tribes.transport.ClusterReceiverBase#messageDataReceived(ChannelMessage)\r
+     * @see XByteBuffer#doesPackageExist()\r
+     * @see XByteBuffer#extractPackage(boolean)\r
+     *\r
+     * @return number of received packages/messages\r
+     * @throws java.io.IOException\r
+     */\r
+    public ChannelMessage[] execute() throws java.io.IOException {\r
+        int pkgCnt = buffer.countPackages();\r
+        ChannelMessage[] result = new ChannelMessage[pkgCnt];\r
+        for (int i=0; i<pkgCnt; i++)  {\r
+            ChannelMessage data = buffer.extractPackage(true);\r
+            result[i] = data;\r
+        }\r
+        return result;\r
+    }\r
+    \r
+    public int bufferSize() {\r
+        return buffer.getLength();\r
+    }\r
+    \r
+\r
+    public boolean hasPackage() {\r
+        return buffer.countPackages(true)>0;\r
+    }\r
+    /**\r
+     * Returns the number of packages that the reader has read\r
+     * @return int\r
+     */\r
+    public int count() {\r
+        return buffer.countPackages();\r
+    }\r
+    \r
+    public void close() {\r
+        this.buffer = null;\r
+    }\r
+\r
+    public long getLastAccess() {\r
+        return lastAccess;\r
+    }\r
+\r
+    public boolean isCancelled() {\r
+        return cancelled;\r
+    }\r
+\r
+    public void setLastAccess(long lastAccess) {\r
+        this.lastAccess = lastAccess;\r
+    }\r
+\r
+    public void setCancelled(boolean cancelled) {\r
+        this.cancelled = cancelled;\r
+    }\r
+\r
+}\r
diff --git a/java/org/apache/catalina/tribes/io/ReplicationStream.java b/java/org/apache/catalina/tribes/io/ReplicationStream.java
new file mode 100644 (file)
index 0000000..a08a27a
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.tribes.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+
+/**
+ * Custom subclass of <code>ObjectInputStream</code> that loads from the
+ * class loader for this web application.  This allows classes defined only
+ * with the web application to be found correctly.
+ *
+ * @author Craig R. McClanahan
+ * @author Bip Thelin
+ * @author Filip Hanik
+ * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ */
+
+public final class ReplicationStream extends ObjectInputStream {
+
+    
+    /**
+     * The class loader we will use to resolve classes.
+     */
+    private ClassLoader[] classLoaders = null;
+    
+
+    /**
+     * Construct a new instance of CustomObjectInputStream
+     *
+     * @param stream The input stream we will read from
+     * @param classLoader The class loader used to instantiate objects
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    public ReplicationStream(InputStream stream,
+                             ClassLoader[] classLoaders)
+        throws IOException {
+
+        super(stream);
+        this.classLoaders = classLoaders;
+    }
+
+    /**
+     * Load the local class equivalent of the specified stream class
+     * description, by using the class loader assigned to this Context.
+     *
+     * @param classDesc Class description from the input stream
+     *
+     * @exception ClassNotFoundException if this class cannot be found
+     * @exception IOException if an input/output error occurs
+     */
+    public Class resolveClass(ObjectStreamClass classDesc)
+        throws ClassNotFoundException, IOException {
+        String name = classDesc.getName();
+        boolean tryRepFirst = name.startsWith("org.apache.catalina.tribes");
+        try {
+            try
+            {
+                if ( tryRepFirst ) return findReplicationClass(name);
+                else return findExternalClass(name);
+            }
+            catch ( Exception x )
+            {
+                if ( tryRepFirst ) return findExternalClass(name);
+                else return findReplicationClass(name);
+            }
+        } catch (ClassNotFoundException e) {
+            return super.resolveClass(classDesc);
+        }
+    }
+    
+    public Class findReplicationClass(String name)
+        throws ClassNotFoundException, IOException {
+        Class clazz = Class.forName(name, false, getClass().getClassLoader());
+        return clazz;
+    }
+
+    public Class findExternalClass(String name) throws ClassNotFoundException  {
+        ClassNotFoundException cnfe = null;
+        for (int i=0; i<classLoaders.length; i++ ) {
+            try {
+                Class clazz = Class.forName(name, false, classLoaders[i]);
+                return clazz;
+            } catch ( ClassNotFoundException x ) {
+                cnfe = x;
+            } 
+        }
+        if ( cnfe != null ) throw cnfe;
+        else throw new ClassNotFoundException(name);
+    }
+    
+    public void close() throws IOException  {
+        this.classLoaders = null;
+        super.close();
+    }
+
+
+}
diff --git a/java/org/apache/catalina/tribes/io/XByteBuffer.java b/java/org/apache/catalina/tribes/io/XByteBuffer.java
new file mode 100644 (file)
index 0000000..609965c
--- /dev/null
@@ -0,0 +1,611 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+
+/**
+ * The XByteBuffer provides a dual functionality.
+ * One, it stores message bytes and automatically extends the byte buffer if needed.<BR>
+ * Two, it can encode and decode packages so that they can be defined and identified
+ * as they come in on a socket.
+ * <br>
+ * <b>THIS CLASS IS NOT THREAD SAFE</B><BR>
+ * <br/>
+ * Transfer package:
+ * <ul>
+ * <li><b>START_DATA/b> - 7 bytes - <i>FLT2002</i></li>
+ * <li><b>SIZE</b>      - 4 bytes - size of the data package</li>
+ * <li><b>DATA</b>      - should be as many bytes as the prev SIZE</li>
+ * <li><b>END_DATA</b>  - 7 bytes - <i>TLF2003</i></lI>
+ * </ul>
+ * @author Filip Hanik
+ * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ */
+public class XByteBuffer
+{
+    
+    public static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog( XByteBuffer.class );
+    
+    /**
+     * This is a package header, 7 bytes (FLT2002)
+     */
+    public static final byte[] START_DATA = {70,76,84,50,48,48,50};
+    
+    /**
+     * This is the package footer, 7 bytes (TLF2003)
+     */
+    public static final byte[] END_DATA = {84,76,70,50,48,48,51};
+    /**
+     * Default size on the initial byte buffer
+     */
+    private static final int DEF_SIZE = 2048;
+    /**
+     * Default size to extend the buffer with
+     */
+    private static final int DEF_EXT  = 1024;
+    
+    /**
+     * Variable to hold the data
+     */
+    protected byte[] buf = null;
+    
+    /**
+     * Current length of data in the buffer
+     */
+    protected int bufSize = 0;
+    
+    /**
+     * Flag for discarding invalid packages
+     * If this flag is set to true, and append(byte[],...) is called,
+     * the data added will be inspected, and if it doesn't start with 
+     * <code>START_DATA</code> it will be thrown away.
+     * 
+     */
+    protected boolean discard = true;
+    
+    /**
+     * Constructs a new XByteBuffer
+     * @param size - the initial size of the byte buffer
+     * @todo use a pool of byte[] for performance
+     */
+    public XByteBuffer(int size, boolean discard) {
+        buf = new byte[size];
+        this.discard = discard;
+    }
+    
+    public XByteBuffer(byte[] data,boolean discard) {
+        this(data,data.length+128,discard);
+    }
+    
+    public XByteBuffer(byte[] data, int size,boolean discard) {
+        int length = Math.max(data.length,size);
+        buf = new byte[length];
+        System.arraycopy(data,0,buf,0,data.length);
+        bufSize = data.length;
+        this.discard = discard;
+    }
+    
+    public int getLength() {
+        return bufSize;
+    }
+
+    public void setLength(int size) {
+        if ( size > buf.length ) throw new ArrayIndexOutOfBoundsException("Size is larger than existing buffer.");
+        bufSize = size;
+    }
+    
+    public void trim(int length) {
+        if ( (bufSize - length) < 0 ) 
+            throw new ArrayIndexOutOfBoundsException("Can't trim more bytes than are available. length:"+bufSize+" trim:"+length);
+        bufSize -= length;
+    }
+    
+    public void reset() {
+        bufSize = 0;
+    }
+            
+    public byte[] getBytesDirect() {
+        return this.buf;
+    }
+
+    /**
+     * Returns the bytes in the buffer, in its exact length
+     */
+    public byte[] getBytes() {
+        byte[] b = new byte[bufSize];
+        System.arraycopy(buf,0,b,0,bufSize);
+        return b;
+    }
+
+    /**
+     * Resets the buffer
+     */
+    public void clear() {
+        bufSize = 0;
+    }
+
+    /**
+     * Appends the data to the buffer. If the data is incorrectly formatted, ie, the data should always start with the
+     * header, false will be returned and the data will be discarded.
+     * @param b - bytes to be appended
+     * @param off - the offset to extract data from
+     * @param len - the number of bytes to append.
+     * @return true if the data was appended correctly. Returns false if the package is incorrect, ie missing header or something, or the length of data is 0
+     */
+    public boolean append(ByteBuffer b, int len) {
+        int newcount = bufSize + len;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        b.get(buf,bufSize,len);
+        
+        bufSize = newcount;
+        
+        if ( discard ) {
+            if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) {
+                bufSize = 0;
+                log.error("Discarded the package, invalid header");
+                return false;
+            }
+        }
+        return true;
+
+    }
+    
+    public boolean append(byte i) {
+        int newcount = bufSize + 1;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        buf[bufSize] = i;
+        bufSize = newcount;
+        return true;
+    }
+
+
+    public boolean append(boolean i) {
+        int newcount = bufSize + 1;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        XByteBuffer.toBytes(i,buf,bufSize);
+        bufSize = newcount;
+        return true;
+    }
+
+    public boolean append(long i) {
+        int newcount = bufSize + 8;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        XByteBuffer.toBytes(i,buf,bufSize);
+        bufSize = newcount;
+        return true;
+    }
+    
+    public boolean append(int i) {
+        int newcount = bufSize + 4;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        XByteBuffer.toBytes(i,buf,bufSize);
+        bufSize = newcount;
+        return true;
+    }
+
+    public boolean append(byte[] b, int off, int len) {
+        if ((off < 0) || (off > b.length) || (len < 0) ||
+            ((off + len) > b.length) || ((off + len) < 0))  {
+            throw new IndexOutOfBoundsException();
+        } else if (len == 0) {
+            return false;
+        }
+
+        int newcount = bufSize + len;
+        if (newcount > buf.length) {
+            expand(newcount);
+        }
+        System.arraycopy(b, off, buf, bufSize, len);
+        bufSize = newcount;
+
+        if ( discard ) {
+            if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) {
+                bufSize = 0;
+                log.error("Discarded the package, invalid header");
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void expand(int newcount) {
+        //don't change the allocation strategy
+        byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)];
+        System.arraycopy(buf, 0, newbuf, 0, bufSize);
+        buf = newbuf;
+    }
+    
+    public int getCapacity() {
+        return buf.length;
+    }
+
+
+    /**
+     * Internal mechanism to make a check if a complete package exists
+     * within the buffer
+     * @return - true if a complete package (header,compress,size,data,footer) exists within the buffer
+     */
+    public int countPackages() {
+        return countPackages(false);
+    }
+
+    public int countPackages(boolean first)
+    {
+        int cnt = 0;
+        int pos = START_DATA.length;
+        int start = 0;
+
+        while ( start < bufSize ) {
+            //first check start header
+            int index = XByteBuffer.firstIndexOf(buf,start,START_DATA);
+            //if the header (START_DATA) isn't the first thing or
+            //the buffer isn't even 14 bytes
+            if ( index != start || ((bufSize-start)<14) ) break;
+            //next 4 bytes are compress flag not needed for count packages
+            //then get the size 4 bytes
+            int size = toInt(buf, pos);
+            //now the total buffer has to be long enough to hold
+            //START_DATA.length+4+size+END_DATA.length
+            pos = start + START_DATA.length + 4 + size;
+            if ( (pos + END_DATA.length) > bufSize) break;
+            //and finally check the footer of the package END_DATA
+            int newpos = firstIndexOf(buf, pos, END_DATA);
+            //mismatch, there is no package
+            if (newpos != pos) break;
+            //increase the packet count
+            cnt++;
+            //reset the values
+            start = pos + END_DATA.length;
+            pos = start + START_DATA.length;
+            //we only want to verify that we have at least one package
+            if ( first ) break;
+        }
+        return cnt;
+    }
+
+    /**
+     * Method to check if a package exists in this byte buffer.
+     * @return - true if a complete package (header,options,size,data,footer) exists within the buffer
+     */
+    public boolean doesPackageExist()  {
+        return (countPackages(true)>0);
+    }
+
+    /**
+     * Extracts the message bytes from a package.
+     * If no package exists, a IllegalStateException will be thrown.
+     * @param clearFromBuffer - if true, the package will be removed from the byte buffer
+     * @return - returns the actual message bytes (header, compress,size and footer not included).
+     */
+    public XByteBuffer extractDataPackage(boolean clearFromBuffer) {
+        int psize = countPackages(true);
+        if (psize == 0) {
+            throw new java.lang.IllegalStateException("No package exists in XByteBuffer");
+        }
+        int size = toInt(buf, START_DATA.length);
+        XByteBuffer xbuf = BufferPool.getBufferPool().getBuffer(size,false);
+        xbuf.setLength(size);
+        System.arraycopy(buf, START_DATA.length + 4, xbuf.getBytesDirect(), 0, size);
+        if (clearFromBuffer) {
+            int totalsize = START_DATA.length + 4 + size + END_DATA.length;
+            bufSize = bufSize - totalsize;
+            System.arraycopy(buf, totalsize, buf, 0, bufSize);
+        }
+        return xbuf;
+
+    }
+    
+    public ChannelData extractPackage(boolean clearFromBuffer) throws java.io.IOException {
+        XByteBuffer xbuf = extractDataPackage(clearFromBuffer);
+        ChannelData cdata = ChannelData.getDataFromPackage(xbuf);
+        return cdata;
+    }
+    
+    /**
+     * Creates a complete data package
+     * @param indata - the message data to be contained within the package
+     * @param compressed - compression flag for the indata buffer
+     * @return - a full package (header,size,data,footer)
+     * 
+     */
+    public static byte[] createDataPackage(ChannelData cdata) {
+//        return createDataPackage(cdata.getDataPackage());
+        //avoid one extra byte array creation
+        int dlength = cdata.getDataPackageLength();
+        int length = getDataPackageLength(dlength);
+        byte[] data = new byte[length];
+        int offset = 0;
+        System.arraycopy(START_DATA, 0, data, offset, START_DATA.length);
+        offset += START_DATA.length;
+        toBytes(dlength,data, START_DATA.length);
+        offset += 4;
+        cdata.getDataPackage(data,offset);
+        offset += dlength;
+        System.arraycopy(END_DATA, 0, data, offset, END_DATA.length);
+        offset += END_DATA.length;
+        return data;
+    }
+    
+    public static byte[] createDataPackage(byte[] data, int doff, int dlength, byte[] buffer, int bufoff) {
+        if ( (buffer.length-bufoff) > getDataPackageLength(dlength) ) {
+            throw new ArrayIndexOutOfBoundsException("Unable to create data package, buffer is too small.");
+        }
+        System.arraycopy(START_DATA, 0, buffer, bufoff, START_DATA.length);
+        toBytes(data.length,buffer, bufoff+START_DATA.length);
+        System.arraycopy(data, doff, buffer, bufoff+START_DATA.length + 4, dlength);
+        System.arraycopy(END_DATA, 0, buffer, bufoff+START_DATA.length + 4 + data.length, END_DATA.length);
+        return buffer;
+    }
+
+    
+    public static int getDataPackageLength(int datalength) {
+        int length = 
+            START_DATA.length + //header length
+            4 + //data length indicator
+            datalength + //actual data length
+            END_DATA.length; //footer length
+        return length;
+
+    }
+    
+    public static byte[] createDataPackage(byte[] data) {
+        int length = getDataPackageLength(data.length);
+        byte[] result = new byte[length];
+        return createDataPackage(data,0,data.length,result,0);
+    }
+    
+    
+
+//    public static void fillDataPackage(byte[] data, int doff, int dlength, XByteBuffer buf) {
+//        int pkglen = getDataPackageLength(dlength);
+//        if ( buf.getCapacity() <  pkglen ) buf.expand(pkglen);
+//        createDataPackage(data,doff,dlength,buf.getBytesDirect(),buf.getLength());
+//    }
+
+    /**
+     * Convert four bytes to an int
+     * @param b - the byte array containing the four bytes
+     * @param off - the offset
+     * @return the integer value constructed from the four bytes
+     * @exception java.lang.ArrayIndexOutOfBoundsException
+     */
+    public static int toInt(byte[] b,int off){
+        return ( ( (int) b[off+3]) & 0xFF) +
+            ( ( ( (int) b[off+2]) & 0xFF) << 8) +
+            ( ( ( (int) b[off+1]) & 0xFF) << 16) +
+            ( ( ( (int) b[off+0]) & 0xFF) << 24);
+    }
+
+    /**
+     * Convert eight bytes to a long
+     * @param b - the byte array containing the four bytes
+     * @param off - the offset
+     * @return the long value constructed from the eight bytes
+     * @exception java.lang.ArrayIndexOutOfBoundsException
+     */
+    public static long toLong(byte[] b,int off){
+        return ( ( (long) b[off+7]) & 0xFF) +
+            ( ( ( (long) b[off+6]) & 0xFF) << 8) +
+            ( ( ( (long) b[off+5]) & 0xFF) << 16) +
+            ( ( ( (long) b[off+4]) & 0xFF) << 24) +
+            ( ( ( (long) b[off+3]) & 0xFF) << 32) +
+            ( ( ( (long) b[off+2]) & 0xFF) << 40) +
+            ( ( ( (long) b[off+1]) & 0xFF) << 48) +
+            ( ( ( (long) b[off+0]) & 0xFF) << 56);
+    }
+
+    
+    /**
+     * Converts an integer to four bytes
+     * @param n - the integer
+     * @return - four bytes in an array
+     * @deprecated use toBytes(boolean,byte[],int)
+     */
+    public static byte[] toBytes(boolean bool) {
+        byte[] b = new byte[1] ;
+        return toBytes(bool,b,0);
+
+    }
+    
+    public static byte[] toBytes(boolean bool, byte[] data, int offset) {
+        data[offset] = (byte)(bool?1:0);
+        return data;
+    }
+    
+    /**
+     * 
+     * @param <any> long
+     * @return use
+     */
+    public static boolean toBoolean(byte[] b, int offset) {
+        return b[offset] != 0;
+    }
+
+    
+    /**
+     * Converts an integer to four bytes
+     * @param n - the integer
+     * @return - four bytes in an array
+     * @deprecated use toBytes(int,byte[],int)
+     */
+    public static byte[] toBytes(int n) {
+        return toBytes(n,new byte[4],0);
+    }
+
+    public static byte[] toBytes(int n,byte[] b, int offset) {
+        b[offset+3] = (byte) (n);
+        n >>>= 8;
+        b[offset+2] = (byte) (n);
+        n >>>= 8;
+        b[offset+1] = (byte) (n);
+        n >>>= 8;
+        b[offset+0] = (byte) (n);
+        return b;
+    }
+
+    /**
+     * Converts an long to eight bytes
+     * @param n - the long
+     * @return - eight bytes in an array
+     * @deprecated use toBytes(long,byte[],int)
+     */
+    public static byte[] toBytes(long n) {
+        return toBytes(n,new byte[8],0);
+    }
+    public static byte[] toBytes(long n, byte[] b, int offset) {
+        b[offset+7] = (byte) (n);
+        n >>>= 8;
+        b[offset+6] = (byte) (n);
+        n >>>= 8;
+        b[offset+5] = (byte) (n);
+        n >>>= 8;
+        b[offset+4] = (byte) (n);
+        n >>>= 8;
+        b[offset+3] = (byte) (n);
+        n >>>= 8;
+        b[offset+2] = (byte) (n);
+        n >>>= 8;
+        b[offset+1] = (byte) (n);
+        n >>>= 8;
+        b[offset+0] = (byte) (n);
+        return b;
+    }
+
+    /**
+     * Similar to a String.IndexOf, but uses pure bytes
+     * @param src - the source bytes to be searched
+     * @param srcOff - offset on the source buffer
+     * @param find - the string to be found within src
+     * @return - the index of the first matching byte. -1 if the find array is not found
+     */
+    public static int firstIndexOf(byte[] src, int srcOff, byte[] find){
+        int result = -1;
+        if (find.length > src.length) return result;
+        if (find.length == 0 || src.length == 0) return result;
+        if (srcOff >= src.length ) throw new java.lang.ArrayIndexOutOfBoundsException();
+        boolean found = false;
+        int srclen = src.length;
+        int findlen = find.length;
+        byte first = find[0];
+        int pos = srcOff;
+        while (!found) {
+            //find the first byte
+            while (pos < srclen){
+                if (first == src[pos])
+                    break;
+                pos++;
+            }
+            if (pos >= srclen)
+                return -1;
+
+            //we found the first character
+            //match the rest of the bytes - they have to match
+            if ( (srclen - pos) < findlen)
+                return -1;
+            //assume it does exist
+            found = true;
+            for (int i = 1; ( (i < findlen) && found); i++)
+                found = found && (find[i] == src[pos + i]);
+            if (found)
+                result = pos;
+            else if ( (srclen - pos) < findlen)
+                return -1; //no more matches possible
+            else
+                pos++;
+        }
+        return result;
+    }
+
+    
+    public static Serializable deserialize(byte[] data) 
+        throws IOException, ClassNotFoundException, ClassCastException {
+        return deserialize(data,0,data.length);
+    }
+    
+    public static Serializable deserialize(byte[] data, int offset, int length)  
+        throws IOException, ClassNotFoundException, ClassCastException {
+        return deserialize(data,offset,length,null);     
+    }
+    public static int invokecount = 0;
+    public static Serializable deserialize(byte[] data, int offset, int length, ClassLoader[] cls) 
+        throws IOException, ClassNotFoundException, ClassCastException {
+        synchronized (XByteBuffer.class) { invokecount++;}
+        Object message = null;
+        if ( cls == null ) cls = new ClassLoader[0];
+        if (data != null) {
+            InputStream  instream = new ByteArrayInputStream(data,offset,length);
+            ObjectInputStream stream = null;
+            stream = (cls.length>0)? new ReplicationStream(instream,cls):new ObjectInputStream(instream);
+            message = stream.readObject();
+            instream.close();
+            stream.close();
+        }
+        if ( message == null ) {
+            return null;
+        } else if (message instanceof Serializable)
+            return (Serializable) message;
+        else {
+            throw new ClassCastException("Message has the wrong class. It should implement Serializable, instead it is:"+message.getClass().getName());
+        }
+    }
+
+    /**
+     * Serializes a message into cluster data
+     * @param msg ClusterMessage
+     * @param compress boolean
+     * @return 
+     * @throws IOException
+     */
+    public static byte[] serialize(Serializable msg) throws IOException {
+        ByteArrayOutputStream outs = new ByteArrayOutputStream();
+        ObjectOutputStream out = new ObjectOutputStream(outs);
+        out.writeObject(msg);
+        out.flush();
+        byte[] data = outs.toByteArray();
+        return data;
+    }
+
+    public void setDiscard(boolean discard) {
+        this.discard = discard;
+    }
+
+    public boolean getDiscard() {
+        return discard;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..3f5bf56
--- /dev/null
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<mbeans-descriptors>
+
+  <mbean         name="SimpleTcpCluster"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Tcp Cluster implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.cluster.tcp.SimpleTcpCluster">
+
+    <attribute   name="protocolStack"
+          description="JavaGroups protocol stack selection"
+                 type="java.lang.String"/>
+
+  </mbean>
+
+
+  <mbean         name="SimpleTcpReplicationManager"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Clustered implementation of the Manager interface"
+               domain="Catalina"
+                group="Manager"
+                 type="org.apache.catalina.cluster.tcp.SimpleTcpReplicationManager">
+
+    <attribute   name="algorithm"
+          description="The message digest algorithm to be used when generating
+                       session identifiers"
+                 type="java.lang.String"/>
+
+    <attribute   name="checkInterval"
+          description="The interval (in seconds) between checks for expired
+                       sessions"
+                 type="int"/>
+
+    <attribute   name="className"
+          description="Fully qualified class name of the managed object"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="distributable"
+          description="The distributable flag for Sessions created by this
+                       Manager"
+                 type="boolean"/>
+
+    <attribute   name="entropy"
+          description="A String initialization parameter used to increase the
+                       entropy of the initialization of our random number
+                       generator"
+                 type="java.lang.String"/>
+
+    <attribute   name="managedResource"
+          description="The managed resource this MBean is associated with"
+                 type="java.lang.Object"/>
+
+    <attribute   name="maxActiveSessions"
+          description="The maximum number of active Sessions allowed, or -1
+                       for no limit"
+                 type="int"/>
+
+    <attribute   name="maxInactiveInterval"
+          description="The default maximum inactive interval for Sessions
+                       created by this Manager"
+                 type="int"/>
+
+    <attribute   name="name"
+          description="The descriptive name of this Manager implementation
+                       (for logging)"
+                 type="java.lang.String"
+            writeable="false"/>
+
+  </mbean>
+
+
+
+<mbean         name="ReplicationValve"
+            className="org.apache.catalina.mbeans.ClassNameMBean"
+          description="Valve for simple tcp replication"
+               domain="Catalina"
+                group="Valve"
+                 type="org.apache.catalina.cluster.tcp.ReplicationValve">
+
+    <attribute   name="className"
+          description="Fully qualified class name of the managed object"
+                 type="java.lang.String"
+            writeable="false"/>
+
+    <attribute   name="debug"
+          description="The debugging detail level for this component"
+                 type="int"/>
+
+  </mbean>
+
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/tribes/membership/Constants.java b/java/org/apache/catalina/tribes/membership/Constants.java
new file mode 100644 (file)
index 0000000..29aaf5a
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.tribes.membership;
+
+import org.apache.catalina.tribes.util.Arrays;
+
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.tribes.membership</code>
+ * package.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 303950 $ $Date: 2005-06-09 15:38:30 -0500 (Thu, 09 Jun 2005) $
+ * @author Filip Hanik
+ */
+
+public class Constants {
+
+    public static final String Package = "org.apache.catalina.tribes.membership";
+    public static void main(String[] args) throws Exception {
+        System.out.println(Arrays.toString("TRIBES-B".getBytes()));
+        System.out.println(Arrays.toString("TRIBES-E".getBytes()));
+    }
+}
diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings.properties b/java/org/apache/catalina/tribes/membership/LocalStrings.properties
new file mode 100644 (file)
index 0000000..0dafa46
--- /dev/null
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/tribes/membership/McastService.java b/java/org/apache/catalina/tribes/membership/McastService.java
new file mode 100644 (file)
index 0000000..4a7aeb9
--- /dev/null
@@ -0,0 +1,448 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.membership;
+
+import java.util.Properties;
+
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.MembershipService;
+import org.apache.catalina.tribes.util.StringManager;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+import java.io.IOException;
+
+/**
+ * A <b>membership</b> implementation using simple multicast.
+ * This is the representation of a multicast membership service.
+ * This class is responsible for maintaining a list of active cluster nodes in the cluster.
+ * If a node fails to send out a heartbeat, the node will be dismissed.
+ *
+ * @author Filip Hanik
+ * @version $Revision: 378093 $, $Date: 2006-02-15 15:13:45 -0600 (Wed, 15 Feb 2006) $
+ */
+
+
+public class McastService implements MembershipService,MembershipListener {
+
+    private static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog( McastService.class );
+
+    /**
+     * The string manager for this package.
+     */
+    protected StringManager sm = StringManager.getManager(Constants.Package);
+
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "McastService/2.1";
+
+    /**
+     * The implementation specific properties
+     */
+    protected Properties properties = new Properties();
+    /**
+     * A handle to the actual low level implementation
+     */
+    protected McastServiceImpl impl;
+    /**
+     * A membership listener delegate (should be the cluster :)
+     */
+    protected MembershipListener listener;
+    /**
+     * The local member
+     */
+    protected MemberImpl localMember ;
+    private int mcastSoTimeout;
+    private int mcastTTL;
+    
+    protected byte[] payload;
+    
+    protected byte[] domain;
+
+    /**
+     * Create a membership service.
+     */
+    public McastService() {
+        //default values
+        properties.setProperty("mcastPort","45564");
+        properties.setProperty("mcastAddress","228.0.0.4");
+        properties.setProperty("memberDropTime","3000");
+        properties.setProperty("mcastFrequency","500");
+
+    }
+
+    /**
+     * Return descriptive information about this implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+        return (info);
+    }
+    
+    /**
+     *
+     * @param properties
+     * <BR/>All are required<BR />
+     * 1. mcastPort - the port to listen to<BR>
+     * 2. mcastAddress - the mcast group address<BR>
+     * 4. bindAddress - the bind address if any - only one that can be null<BR>
+     * 5. memberDropTime - the time a member is gone before it is considered gone.<BR>
+     * 6. mcastFrequency - the frequency of sending messages<BR>
+     * 7. tcpListenPort - the port this member listens to<BR>
+     * 8. tcpListenHost - the bind address of this member<BR>
+     * @exception java.lang.IllegalArgumentException if a property is missing.
+     */
+    public void setProperties(Properties properties) {
+        hasProperty(properties,"mcastPort");
+        hasProperty(properties,"mcastAddress");
+        hasProperty(properties,"memberDropTime");
+        hasProperty(properties,"mcastFrequency");
+        hasProperty(properties,"tcpListenPort");
+        hasProperty(properties,"tcpListenHost");
+        this.properties = properties;
+    }
+
+    /**
+     * Return the properties, see setProperties
+     */
+    public Properties getProperties() {
+        return properties;
+    }
+
+    /**
+     * Return the local member name
+     */
+    public String getLocalMemberName() {
+        return localMember.toString() ;
+    }
+    /**
+     * Return the local member
+     */
+    public Member getLocalMember(boolean alive) {
+        if ( alive && localMember != null && impl != null) localMember.setMemberAliveTime(System.currentTimeMillis()-impl.getServiceStartTime());
+        return localMember;
+    }
+    
+    /**
+     * Sets the local member properties for broadcasting
+     */
+    public void setLocalMemberProperties(String listenHost, int listenPort) {
+        properties.setProperty("tcpListenHost",listenHost);
+        properties.setProperty("tcpListenPort",String.valueOf(listenPort));
+        try {
+            if (localMember != null) {
+                localMember.setHostname(listenHost);
+                localMember.setPort(listenPort);
+            } else {
+                localMember = new MemberImpl(listenHost, listenPort, 0);
+                localMember.setUniqueId(UUIDGenerator.randomUUID(true));
+                localMember.setPayload(getPayload());
+                localMember.setDomain(getDomain());
+            }
+            localMember.getData(true, true);
+        }catch ( IOException x ) {
+            throw new IllegalArgumentException(x);
+        }
+    }
+    
+    public void setMcastAddr(String addr) {
+        properties.setProperty("mcastAddress", addr);
+    }
+
+    public String getMcastAddr() {
+        return properties.getProperty("mcastAddress");
+    }
+
+    public void setMcastBindAddress(String bindaddr) {
+        properties.setProperty("mcastBindAddress", bindaddr);
+    }
+
+    public String getMcastBindAddress() {
+        return properties.getProperty("mcastBindAddress");
+    }
+
+    public void setMcastPort(int port) {
+        properties.setProperty("mcastPort", String.valueOf(port));
+    }
+
+    public int getMcastPort() {
+        String p = properties.getProperty("mcastPort");
+        return new Integer(p).intValue();
+    }
+    
+    public void setMcastFrequency(long time) {
+        properties.setProperty("mcastFrequency", String.valueOf(time));
+    }
+
+    public long getMcastFrequency() {
+        String p = properties.getProperty("mcastFrequency");
+        return new Long(p).longValue();
+    }
+
+    public void setMcastDropTime(long time) {
+        properties.setProperty("memberDropTime", String.valueOf(time));
+    }
+
+    public long getMcastDropTime() {
+        String p = properties.getProperty("memberDropTime");
+        return new Long(p).longValue();
+    }
+
+    /**
+     * Check if a required property is available.
+     * @param properties The set of properties
+     * @param name The property to check for
+     */
+    protected void hasProperty(Properties properties, String name){
+        if ( properties.getProperty(name)==null) throw new IllegalArgumentException("McastService:Required property \""+name+"\" is missing.");
+    }
+
+    /**
+     * Start broadcasting and listening to membership pings
+     * @throws java.lang.Exception if a IO error occurs
+     */
+    public void start() throws java.lang.Exception {
+        start(MembershipService.MBR_RX);
+        start(MembershipService.MBR_TX);
+    }
+    
+    public void start(int level) throws java.lang.Exception {
+        hasProperty(properties,"mcastPort");
+        hasProperty(properties,"mcastAddress");
+        hasProperty(properties,"memberDropTime");
+        hasProperty(properties,"mcastFrequency");
+        hasProperty(properties,"tcpListenPort");
+        hasProperty(properties,"tcpListenHost");
+
+        if ( impl != null ) {
+            impl.start(level);
+            return;
+        }
+        String host = getProperties().getProperty("tcpListenHost");
+        int port = Integer.parseInt(getProperties().getProperty("tcpListenPort"));
+        
+        if ( localMember == null ) {
+            localMember = new MemberImpl(host, port, 100);
+            localMember.setUniqueId(UUIDGenerator.randomUUID(true));
+        } else {
+            localMember.setHostname(host);
+            localMember.setPort(port);
+            localMember.setMemberAliveTime(100);
+        }
+        if ( this.payload != null ) localMember.setPayload(payload);
+        if ( this.domain != null ) localMember.setDomain(domain);
+        localMember.setServiceStartTime(System.currentTimeMillis());
+        java.net.InetAddress bind = null;
+        if ( properties.getProperty("mcastBindAddress")!= null ) {
+            bind = java.net.InetAddress.getByName(properties.getProperty("mcastBindAddress"));
+        }
+        int ttl = -1;
+        int soTimeout = -1;
+        if ( properties.getProperty("mcastTTL") != null ) {
+            try {
+                ttl = Integer.parseInt(properties.getProperty("mcastTTL"));
+            } catch ( Exception x ) {
+                log.error("Unable to parse mcastTTL="+properties.getProperty("mcastTTL"),x);
+            }
+        }
+        if ( properties.getProperty("mcastSoTimeout") != null ) {
+            try {
+                soTimeout = Integer.parseInt(properties.getProperty("mcastSoTimeout"));
+            } catch ( Exception x ) {
+                log.error("Unable to parse mcastSoTimeout="+properties.getProperty("mcastSoTimeout"),x);
+            }
+        }
+
+        impl = new McastServiceImpl((MemberImpl)localMember,Long.parseLong(properties.getProperty("mcastFrequency")),
+                                    Long.parseLong(properties.getProperty("memberDropTime")),
+                                    Integer.parseInt(properties.getProperty("mcastPort")),
+                                    bind,
+                                    java.net.InetAddress.getByName(properties.getProperty("mcastAddress")),
+                                    ttl,
+                                    soTimeout,
+                                    this);
+        
+        impl.start(level);
+               
+
+    }
+
+    /**
+     * Stop broadcasting and listening to membership pings
+     */
+    public void stop(int svc) {
+        try  {
+            if ( impl != null && impl.stop(svc) ) impl = null;
+        } catch ( Exception x)  {
+            log.error("Unable to stop the mcast service, level:"+svc+".",x);
+        }
+    }
+
+
+    /**
+     * Return all the members by name
+     */
+    public String[] getMembersByName() {
+        Member[] currentMembers = getMembers();
+        String [] membernames ;
+        if(currentMembers != null) {
+            membernames = new String[currentMembers.length];
+            for (int i = 0; i < currentMembers.length; i++) {
+                membernames[i] = currentMembers[i].toString() ;
+            }
+        } else
+            membernames = new String[0] ;
+        return membernames ;
+    }
+    /**
+     * Return the member by name
+     */
+    public Member findMemberByName(String name) {
+        Member[] currentMembers = getMembers();
+        for (int i = 0; i < currentMembers.length; i++) {
+            if (name.equals(currentMembers[i].toString()))
+                return currentMembers[i];
+        }
+        return null;
+    }
+
+    /**
+     * has members?
+     */
+    public boolean hasMembers() {
+       if ( impl == null || impl.membership == null ) return false;
+       return impl.membership.hasMembers();
+    }
+    
+    public Member getMember(Member mbr) {
+        if ( impl == null || impl.membership == null ) return null;
+        return impl.membership.getMember(mbr);
+    }
+
+    /**
+     * Return all the members
+     */
+    public Member[] getMembers() {
+        if ( impl == null || impl.membership == null ) return null;
+        return impl.membership.getMembers();
+    }
+    /**
+     * Add a membership listener, this version only supports one listener per service,
+     * so calling this method twice will result in only the second listener being active.
+     * @param listener The listener
+     */
+    public void setMembershipListener(MembershipListener listener) {
+        this.listener = listener;
+    }
+    /**
+     * Remove the membership listener
+     */
+    public void removeMembershipListener(){
+        listener = null;
+    }
+
+    public void memberAdded(Member member) {
+        if ( listener!=null ) listener.memberAdded(member);
+    }
+
+    /**
+     * Callback from the impl when a new member has been received
+     * @param member The member
+     */
+    public void memberDisappeared(Member member)
+    {
+        if ( listener!=null ) listener.memberDisappeared(member);
+    }
+
+    public int getMcastSoTimeout() {
+        return mcastSoTimeout;
+    }
+    public void setMcastSoTimeout(int mcastSoTimeout) {
+        this.mcastSoTimeout = mcastSoTimeout;
+        properties.setProperty("mcastSoTimeout", String.valueOf(mcastSoTimeout));
+    }
+    public int getMcastTTL() {
+        return mcastTTL;
+    }
+
+    public byte[] getPayload() {
+        return payload;
+    }
+    
+    public byte[] getDomain() {
+        return domain;
+    }
+
+    public void setMcastTTL(int mcastTTL) {
+        this.mcastTTL = mcastTTL;
+        properties.setProperty("mcastTTL", String.valueOf(mcastTTL));
+    }
+
+    public void setPayload(byte[] payload) {
+        this.payload = payload;
+        if ( localMember != null ) {
+            localMember.setPayload(payload);
+            localMember.getData(true,true);
+            try {
+                if (impl != null) impl.send(false);
+            }catch ( Exception x ) {
+                log.error("Unable to send payload update.",x);
+            }
+        }
+    }
+    
+    public void setDomain(byte[] domain) {
+        this.domain = domain;
+        if ( localMember != null ) {
+            localMember.setDomain(domain);
+            localMember.getData(true,true);
+            try {
+                if (impl != null) impl.send(false);
+            }catch ( Exception x ) {
+                log.error("Unable to send domain update.",x);
+            }
+        }
+    }
+
+    /**
+     * Simple test program
+     * @param args Command-line arguments
+     * @throws Exception If an error occurs
+     */
+    public static void main(String args[]) throws Exception {
+               if(log.isInfoEnabled())
+            log.info("Usage McastService hostname tcpport");
+        McastService service = new McastService();
+        java.util.Properties p = new java.util.Properties();
+        p.setProperty("mcastPort","5555");
+        p.setProperty("mcastAddress","224.10.10.10");
+        p.setProperty("mcastClusterDomain","catalina");
+        p.setProperty("bindAddress","localhost");
+        p.setProperty("memberDropTime","3000");
+        p.setProperty("mcastFrequency","500");
+        p.setProperty("tcpListenPort","4000");
+        p.setProperty("tcpListenHost","127.0.0.1");
+        service.setProperties(p);
+        service.start();
+        Thread.sleep(60*1000*60);
+    }
+}
diff --git a/java/org/apache/catalina/tribes/membership/McastServiceImpl.java b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java
new file mode 100644 (file)
index 0000000..999b1f0
--- /dev/null
@@ -0,0 +1,397 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.membership;
+
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+
+import org.apache.catalina.tribes.MembershipListener;
+import java.util.Arrays;
+import java.net.SocketTimeoutException;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.Channel;
+import java.net.InetSocketAddress;
+
+/**
+ * A <b>membership</b> implementation using simple multicast.
+ * This is the representation of a multicast membership service.
+ * This class is responsible for maintaining a list of active cluster nodes in the cluster.
+ * If a node fails to send out a heartbeat, the node will be dismissed.
+ * This is the low level implementation that handles the multicasting sockets.
+ * Need to fix this, could use java.nio and only need one thread to send and receive, or
+ * just use a timeout on the receive
+ * @author Filip Hanik
+ * @version $Revision: 356540 $, $Date: 2005-12-13 10:53:40 -0600 (Tue, 13 Dec 2005) $
+ */
+public class McastServiceImpl
+{
+    private static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog( McastService.class );
+    
+    protected static int MAX_PACKET_SIZE = 65535;
+    /**
+     * Internal flag used for the listen thread that listens to the multicasting socket.
+     */
+    protected boolean doRunSender = false;
+    protected boolean doRunReceiver = false;
+    protected int startLevel = 0;
+    /**
+     * Socket that we intend to listen to
+     */
+    protected MulticastSocket socket;
+    /**
+     * The local member that we intend to broad cast over and over again
+     */
+    protected MemberImpl member;
+    /**
+     * The multicast address
+     */
+    protected InetAddress address;
+    /**
+     * The multicast port
+     */
+    protected int port;
+    /**
+     * The time it takes for a member to expire.
+     */
+    protected long timeToExpiration;
+    /**
+     * How often to we send out a broadcast saying we are alive, must be smaller than timeToExpiration
+     */
+    protected long sendFrequency;
+    /**
+     * Reuse the sendPacket, no need to create a new one everytime
+     */
+    protected DatagramPacket sendPacket;
+    /**
+     * Reuse the receivePacket, no need to create a new one everytime
+     */
+    protected DatagramPacket receivePacket;
+    /**
+     * The membership, used so that we calculate memberships when they arrive or don't arrive
+     */
+    protected Membership membership;
+    /**
+     * The actual listener, for callback when shits goes down
+     */
+    protected MembershipListener service;
+    /**
+     * Thread to listen for pings
+     */
+    protected ReceiverThread receiver;
+    /**
+     * Thread to send pings
+     */
+    protected SenderThread sender;
+
+    /**
+     * When was the service started
+     */
+    protected long serviceStartTime = System.currentTimeMillis();
+    
+    /**
+     * Time to live for the multicast packets that are being sent out
+     */
+    protected int mcastTTL = -1;
+    /**
+     * Read timeout on the mcast socket
+     */
+    protected int mcastSoTimeout = -1;
+    /**
+     * bind address
+     */
+    protected InetAddress mcastBindAddress = null;
+    
+    /**
+     * Create a new mcast service impl
+     * @param member - the local member
+     * @param sendFrequency - the time (ms) in between pings sent out
+     * @param expireTime - the time (ms) for a member to expire
+     * @param port - the mcast port
+     * @param bind - the bind address (not sure this is used yet)
+     * @param mcastAddress - the mcast address
+     * @param service - the callback service
+     * @throws IOException
+     */
+    public McastServiceImpl(
+        MemberImpl member,
+        long sendFrequency,
+        long expireTime,
+        int port,
+        InetAddress bind,
+        InetAddress mcastAddress,
+        int ttl,
+        int soTimeout,
+        MembershipListener service)
+    throws IOException {
+        this.member = member;
+        this.address = mcastAddress;
+        this.port = port;
+        this.mcastSoTimeout = soTimeout;
+        this.mcastTTL = ttl;
+        this.mcastBindAddress = bind;
+        this.timeToExpiration = expireTime;
+        this.service = service;
+        this.sendFrequency = sendFrequency;
+        setupSocket();
+        sendPacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
+        sendPacket.setAddress(address);
+        sendPacket.setPort(port);
+        receivePacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
+        receivePacket.setAddress(address);
+        receivePacket.setPort(port);
+        membership = new Membership(member);
+    }
+    
+    protected void setupSocket() throws IOException {
+        if (mcastBindAddress != null) socket = new MulticastSocket(new InetSocketAddress(mcastBindAddress, port));
+        else socket = new MulticastSocket(port);
+        if (mcastBindAddress != null) {
+                       if(log.isInfoEnabled())
+                log.info("Setting multihome multicast interface to:" +mcastBindAddress);
+            socket.setInterface(mcastBindAddress);
+        } //end if
+        //force a so timeout so that we don't block forever
+        if ( mcastSoTimeout <= 0 ) mcastSoTimeout = (int)sendFrequency;
+        if(log.isInfoEnabled())
+            log.info("Setting cluster mcast soTimeout to "+mcastSoTimeout);
+        socket.setSoTimeout(mcastSoTimeout);
+
+        if ( mcastTTL >= 0 ) {
+                       if(log.isInfoEnabled())
+                log.info("Setting cluster mcast TTL to " + mcastTTL);
+            socket.setTimeToLive(mcastTTL);
+        }
+    }
+    
+    
+
+    /**
+     * Start the service
+     * @param level 1 starts the receiver, level 2 starts the sender
+     * @throws IOException if the service fails to start
+     * @throws IllegalStateException if the service is already started
+     */
+    public synchronized void start(int level) throws IOException {
+        boolean valid = false;
+        if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) {
+            if ( receiver != null ) throw new IllegalStateException("McastService.receive already running.");
+            if ( sender == null ) socket.joinGroup(address);
+            doRunReceiver = true;
+            receiver = new ReceiverThread();
+            receiver.setDaemon(true);
+            receiver.start();
+            valid = true;
+        } 
+        if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) {
+            if ( sender != null ) throw new IllegalStateException("McastService.send already running.");
+            if ( receiver == null ) socket.joinGroup(address);
+            //make sure at least one packet gets out there
+            send(false);
+            doRunSender = true;
+            serviceStartTime = System.currentTimeMillis();
+            sender = new SenderThread(sendFrequency);
+            sender.setDaemon(true);
+            sender.start();
+            //we have started the receiver, but not yet waited for membership to establish
+            valid = true;
+        } 
+        if (!valid) {
+            throw new IllegalArgumentException("Invalid start level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ");
+        }
+        //pause, once or twice
+        waitForMembers(level);
+        startLevel = (startLevel | level);
+    }
+
+    private void waitForMembers(int level) {
+        long memberwait = sendFrequency*2;
+        if(log.isInfoEnabled())
+            log.info("Sleeping for "+memberwait+" milliseconds to establish cluster membership, start level:"+level);
+        try {Thread.sleep(memberwait);}catch (InterruptedException ignore){}
+        if(log.isInfoEnabled())
+            log.info("Done sleeping, membership established, start level:"+level);
+    }
+
+    /**
+     * Stops the service
+     * @throws IOException if the service fails to disconnect from the sockets
+     */
+    public synchronized boolean stop(int level) throws IOException {
+        boolean valid = false;
+        
+        if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) {
+            valid = true;
+            doRunReceiver = false;
+            if ( receiver !=null ) receiver.interrupt();
+            receiver = null;
+        } 
+        if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) {
+            valid = true;
+            doRunSender = false;
+            if ( sender != null )sender.interrupt();
+            sender = null;
+        } 
+        
+        if (!valid) {
+            throw new IllegalArgumentException("Invalid stop level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ");
+        }
+        startLevel = (startLevel & (~level));
+        //we're shutting down, send a shutdown message and close the socket
+        if ( startLevel == 0 ) {
+            //send a stop message
+            member.setCommand(Member.SHUTDOWN_PAYLOAD);
+            member.getData(true, true);
+            send(false);
+            //leave mcast group
+            try {socket.leaveGroup(address);}catch ( Exception ignore){}
+            serviceStartTime = Long.MAX_VALUE;
+        }
+        return (startLevel == 0);
+    }
+
+    /**
+     * Receive a datagram packet, locking wait
+     * @throws IOException
+     */
+    public void receive() throws IOException {
+        try {
+            socket.receive(receivePacket);
+            if(receivePacket.getLength() > MAX_PACKET_SIZE) {
+                log.error("Multicast packet received was too long, dropping package:"+receivePacket.getLength());
+            } else {
+                byte[] data = new byte[receivePacket.getLength()];
+                System.arraycopy(receivePacket.getData(), receivePacket.getOffset(), data, 0, data.length);
+                final MemberImpl m = MemberImpl.getMember(data);
+                if (log.isTraceEnabled()) log.trace("Mcast receive ping from member " + m);
+                Thread t = null;
+                if (Arrays.equals(m.getCommand(), Member.SHUTDOWN_PAYLOAD)) {
+                    if (log.isDebugEnabled()) log.debug("Member has shutdown:" + m);
+                    membership.removeMember(m);
+                    t = new Thread() {
+                        public void run() {
+                            service.memberDisappeared(m);
+                        }
+                    };
+                } else if (membership.memberAlive(m)) {
+                    if (log.isDebugEnabled()) log.debug("Mcast add member " + m);
+                    t = new Thread() {
+                        public void run() {
+                            service.memberAdded(m);
+                        }
+                    };
+                } //end if
+                if ( t != null ) t.start();
+            }
+        } catch (SocketTimeoutException x ) { 
+            //do nothing, this is normal, we don't want to block forever
+            //since the receive thread is the same thread
+            //that does membership expiration
+        }
+        checkExpired();
+    }
+    
+    protected Object expiredMutex = new Object();
+    protected void checkExpired() {
+        synchronized (expiredMutex) {
+            MemberImpl[] expired = membership.expire(timeToExpiration);
+            for (int i = 0; i < expired.length; i++) {
+                final MemberImpl member = expired[i];
+                if (log.isDebugEnabled())
+                    log.debug("Mcast exipre  member " + expired[i]);
+                try {
+                    Thread t = new Thread() {
+                        public void run() {
+                            service.memberDisappeared(member);
+                        }
+                    };
+                    t.start();
+                } catch (Exception x) {
+                    log.error("Unable to process member disappeared message.", x);
+                }
+            }
+        }
+    }
+
+    /**
+     * Send a ping
+     * @throws Exception
+     */ 
+    public void send(boolean checkexpired) throws IOException{
+        //ignore if we haven't started the sender
+        //if ( (startLevel&Channel.MBR_TX_SEQ) != Channel.MBR_TX_SEQ ) return;
+        member.inc();
+        if(log.isTraceEnabled())
+            log.trace("Mcast send ping from member " + member);
+        byte[] data = member.getData();
+        DatagramPacket p = new DatagramPacket(data,data.length);
+        p.setAddress(address);
+        p.setPort(port);
+        socket.send(p);
+        if ( checkexpired ) checkExpired();
+    }
+
+    public long getServiceStartTime() {
+       return this.serviceStartTime;
+    }
+
+
+    public class ReceiverThread extends Thread {
+        public ReceiverThread() {
+            super();
+            setName("Cluster-MembershipReceiver");
+        }
+        public void run() {
+            while ( doRunReceiver ) {
+                try {
+                    receive();
+                } catch ( ArrayIndexOutOfBoundsException ax ) {
+                    //we can ignore this, as it means we have an invalid package
+                    //but we will log it to debug
+                    if ( log.isDebugEnabled() )
+                        log.debug("Invalid member mcast package.",ax);
+                } catch ( Exception x ) {
+                    log.warn("Error receiving mcast package. Sleeping 500ms",x);
+                    try { Thread.sleep(500); } catch ( Exception ignore ){}
+                    
+                }
+            }
+        }
+    }//class ReceiverThread
+
+    public class SenderThread extends Thread {
+        long time;
+        public SenderThread(long time) {
+            this.time = time;
+            setName("Cluster-MembershipSender");
+
+        }
+        public void run() {
+            while ( doRunSender ) {
+                try {
+                    send(true);
+                } catch ( Exception x ) {
+                    log.warn("Unable to send mcast message.",x);
+                }
+                try { Thread.sleep(time); } catch ( Exception ignore ) {}
+            }
+        }
+    }//class SenderThread
+}
diff --git a/java/org/apache/catalina/tribes/membership/MemberImpl.java b/java/org/apache/catalina/tribes/membership/MemberImpl.java
new file mode 100644 (file)
index 0000000..cd9f00f
--- /dev/null
@@ -0,0 +1,570 @@
+/*\r
+ * Copyright 1999,2004-2005 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.apache.catalina.tribes.membership;\r
+\r
+import java.io.IOException;\r
+import java.io.ObjectInput;\r
+import java.io.ObjectOutput;\r
+import java.util.Arrays;\r
+\r
+import org.apache.catalina.tribes.Member;\r
+import org.apache.catalina.tribes.io.XByteBuffer;\r
+import org.apache.catalina.tribes.transport.SenderState;\r
+\r
+/**\r
+ * A <b>membership</b> implementation using simple multicast.\r
+ * This is the representation of a multicast member.\r
+ * Carries the host, and port of the this or other cluster nodes.\r
+ *\r
+ * @author Filip Hanik\r
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $\r
+ */\r
+public class MemberImpl implements Member, java.io.Externalizable {\r
+\r
+    /**\r
+     * Public properties specific to this implementation\r
+     */\r
+    public static final transient String TCP_LISTEN_PORT = "tcpListenPort";\r
+    public static final transient String TCP_LISTEN_HOST = "tcpListenHost";\r
+    public static final transient String MEMBER_NAME = "memberName";\r
+    \r
+    public static final transient byte[] TRIBES_MBR_BEGIN = new byte[] {84, 82, 73, 66, 69, 83, 45, 66};\r
+    public static final transient byte[] TRIBES_MBR_END   = new byte[] {84, 82, 73, 66, 69, 83, 45, 69};\r
+    \r
+    /**\r
+     * The listen host for this member\r
+     */\r
+    protected byte[] host;\r
+    protected transient String hostname;\r
+    /**\r
+     * The tcp listen port for this member\r
+     */\r
+    protected int port;\r
+\r
+    /**\r
+     * Counter for how many broadcast messages have been sent from this member\r
+     */\r
+    protected int msgCount = 0;\r
+    /**\r
+     * The number of milliseconds since this members was\r
+     * created, is kept track of using the start time\r
+     */\r
+    protected long memberAliveTime = 0;\r
+    \r
+    /**\r
+     * For the local member only\r
+     */\r
+    protected transient long serviceStartTime;\r
+    \r
+    /**\r
+     * To avoid serialization over and over again, once the local dataPkg\r
+     * has been set, we use that to transmit data\r
+     */\r
+    protected transient byte[] dataPkg = null;\r
+\r
+    /**\r
+     * Unique session Id for this member\r
+     */\r
+    protected byte[] uniqueId = new byte[16];\r
+    \r
+    /**\r
+     * Custom payload that an app framework can broadcast\r
+     * Also used to transport stop command.\r
+     */\r
+    protected byte[] payload = new byte[0];\r
+    \r
+    /**\r
+     * Command, so that the custom payload doesn't have to be used\r
+     * This is for internal tribes use, such as SHUTDOWN_COMMAND\r
+     */\r
+    protected byte[] command = new byte[0];\r
+\r
+    /**\r
+     * Domain if we want to filter based on domain.\r
+     */\r
+    protected byte[] domain = new byte[0];\r
+    \r
+    /**\r
+     * Empty constructor for serialization\r
+     */\r
+    public MemberImpl() {\r
+        \r
+    }\r
+\r
+    /**\r
+     * Construct a new member object\r
+     * @param name - the name of this member, cluster unique\r
+     * @param domain - the cluster domain name of this member\r
+     * @param host - the tcp listen host\r
+     * @param port - the tcp listen port\r
+     */\r
+    public MemberImpl(String host,\r
+                      int port,\r
+                      long aliveTime) throws IOException {\r
+        setHostname(host);\r
+        this.port = port;\r
+        this.memberAliveTime=aliveTime;\r
+    }\r
+    \r
+    public MemberImpl(String host,\r
+                      int port,\r
+                      long aliveTime,\r
+                      byte[] payload) throws IOException {\r
+        this(host,port,aliveTime);\r
+        setPayload(payload);\r
+    }\r
+    \r
+    public boolean isReady() {\r
+        return SenderState.getSenderState(this).isReady();\r
+    }\r
+    public boolean isSuspect() {\r
+        return SenderState.getSenderState(this).isSuspect();\r
+    }\r
+    public boolean isFailing() {\r
+        return SenderState.getSenderState(this).isFailing();\r
+    }\r
+\r
+    /**\r
+     * Increment the message count.\r
+     */\r
+    protected void inc() {\r
+        msgCount++;\r
+    }\r
+\r
+    /**\r
+     * Create a data package to send over the wire representing this member.\r
+     * This is faster than serialization.\r
+     * @return - the bytes for this member deserialized\r
+     * @throws Exception\r
+     */\r
+    public byte[] getData()  {\r
+        return getData(true);\r
+    }\r
+    /**\r
+     * Highly optimized version of serializing a member into a byte array\r
+     * Returns a cached byte[] reference, do not modify this data\r
+     * @param getalive boolean\r
+     * @return byte[]\r
+     */\r
+    public byte[] getData(boolean getalive)  {\r
+        return getData(getalive,false);\r
+    }\r
+    \r
+    \r
+    public int getDataLength() {\r
+        return TRIBES_MBR_BEGIN.length+ //start pkg\r
+               4+ //data length\r
+               8+ //alive time\r
+               4+ //port\r
+               1+ //host length\r
+               host.length+ //host\r
+               4+ //command length\r
+               command.length+ //command\r
+               4+ //domain length\r
+               domain.length+ //domain\r
+               16+ //unique id\r
+               4+ //payload length\r
+               payload.length+ //payload\r
+               TRIBES_MBR_END.length; //end pkg\r
+    }\r
+    \r
+    /**\r
+     * \r
+     * @param getalive boolean - calculate memberAlive time\r
+     * @param reset boolean - reset the cached data package, and create a new one\r
+     * @return byte[]\r
+     */\r
+    public byte[] getData(boolean getalive, boolean reset)  {\r
+        if ( reset ) dataPkg = null;\r
+        //look in cache first\r
+        if ( dataPkg!=null ) {\r
+            if ( getalive ) {\r
+                //you'd be surprised, but System.currentTimeMillis\r
+                //shows up on the profiler\r
+                long alive=System.currentTimeMillis()-getServiceStartTime();\r
+                XByteBuffer.toBytes( (long) alive, dataPkg, TRIBES_MBR_BEGIN.length+4);\r
+            }\r
+            return dataPkg;\r
+        }\r
+        \r
+        //package looks like\r
+        //start package TRIBES_MBR_BEGIN.length\r
+        //package length - 4 bytes\r
+        //alive - 8 bytes\r
+        //port - 4 bytes\r
+        //host length - 1 byte\r
+        //host - hl bytes\r
+        //clen - 4 bytes\r
+        //command - clen bytes\r
+        //dlen - 4 bytes\r
+        //domain - dlen bytes\r
+        //uniqueId - 16 bytes\r
+        //payload length - 4 bytes\r
+        //payload plen bytes\r
+        //end package TRIBES_MBR_END.length\r
+        byte[] addr = host;\r
+        long alive=System.currentTimeMillis()-getServiceStartTime();\r
+        byte hl = (byte)addr.length;\r
+        byte[] data = new byte[getDataLength()];\r
+        \r
+        int bodylength = (getDataLength() - TRIBES_MBR_BEGIN.length - TRIBES_MBR_END.length - 4);\r
+        \r
+        int pos = 0;\r
+        \r
+        //TRIBES_MBR_BEGIN\r
+        System.arraycopy(TRIBES_MBR_BEGIN,0,data,pos,TRIBES_MBR_BEGIN.length);\r
+        pos += TRIBES_MBR_BEGIN.length;\r
+        \r
+        //body length\r
+        XByteBuffer.toBytes(bodylength,data,pos);\r
+        pos += 4;\r
+        \r
+        //alive data\r
+        XByteBuffer.toBytes((long)alive,data,pos);\r
+        pos += 8;\r
+        //port\r
+        XByteBuffer.toBytes(port,data,pos);\r
+        pos += 4;\r
+        //host length\r
+        data[pos++] = hl;\r
+        //host\r
+        System.arraycopy(addr,0,data,pos,addr.length);\r
+        pos+=addr.length;\r
+        //clen - 4 bytes\r
+        XByteBuffer.toBytes(command.length,data,pos);\r
+        pos+=4;\r
+        //command - clen bytes\r
+        System.arraycopy(command,0,data,pos,command.length);\r
+        pos+=command.length;\r
+        //dlen - 4 bytes\r
+        XByteBuffer.toBytes(domain.length,data,pos);\r
+        pos+=4;\r
+        //domain - dlen bytes\r
+        System.arraycopy(domain,0,data,pos,domain.length);\r
+        pos+=domain.length;\r
+        //unique Id\r
+        System.arraycopy(uniqueId,0,data,pos,uniqueId.length);\r
+        pos+=uniqueId.length;\r
+        //payload\r
+        XByteBuffer.toBytes(payload.length,data,pos);\r
+        pos+=4;\r
+        System.arraycopy(payload,0,data,pos,payload.length);\r
+        pos+=payload.length;\r
+        \r
+        //TRIBES_MBR_END\r
+        System.arraycopy(TRIBES_MBR_END,0,data,pos,TRIBES_MBR_END.length);\r
+        pos += TRIBES_MBR_END.length;\r
+\r
+        //create local data\r
+        dataPkg = data;\r
+        return data;\r
+    }\r
+    /**\r
+     * Deserializes a member from data sent over the wire\r
+     * @param data - the bytes received\r
+     * @return a member object.\r
+     */\r
+    public static MemberImpl getMember(byte[] data, MemberImpl member) {\r
+        return getMember(data,0,data.length,member);\r
+    }\r
+\r
+    public static MemberImpl getMember(byte[] data, int offset, int length, MemberImpl member) {\r
+        //package looks like\r
+        //start package TRIBES_MBR_BEGIN.length\r
+        //package length - 4 bytes\r
+        //alive - 8 bytes\r
+        //port - 4 bytes\r
+        //host length - 1 byte\r
+        //host - hl bytes\r
+        //clen - 4 bytes\r
+        //command - clen bytes\r
+        //dlen - 4 bytes\r
+        //domain - dlen bytes\r
+        //uniqueId - 16 bytes\r
+        //payload length - 4 bytes\r
+        //payload plen bytes\r
+        //end package TRIBES_MBR_END.length\r
+\r
+        int pos = offset;\r
+        \r
+        if (XByteBuffer.firstIndexOf(data,offset,TRIBES_MBR_BEGIN)!=pos) {\r
+            throw new IllegalArgumentException("Invalid package, should start with:"+org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_BEGIN));\r
+        }\r
+\r
+        if ( length < (TRIBES_MBR_BEGIN.length+4) ) {\r
+            throw new ArrayIndexOutOfBoundsException("Member package to small to validate.");\r
+        }\r
+        \r
+        pos += TRIBES_MBR_BEGIN.length;\r
+        \r
+        int bodylength = XByteBuffer.toInt(data,pos);\r
+        pos += 4;\r
+        \r
+        if ( length < (bodylength+4+TRIBES_MBR_BEGIN.length+TRIBES_MBR_END.length) ) {\r
+            throw new ArrayIndexOutOfBoundsException("Not enough bytes in member package.");\r
+        }\r
+        \r
+        int endpos = pos+bodylength;\r
+        if (XByteBuffer.firstIndexOf(data,endpos,TRIBES_MBR_END)!=endpos) {\r
+            throw new IllegalArgumentException("Invalid package, should end with:"+org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_END));\r
+        }\r
+\r
+\r
+        byte[] alived = new byte[8];\r
+        System.arraycopy(data, pos, alived, 0, 8);\r
+        pos += 8;\r
+        byte[] portd = new byte[4];\r
+        System.arraycopy(data, pos, portd, 0, 4);\r
+        pos += 4;\r
+    \r
+        byte hl = data[pos++];\r
+        byte[] addr = new byte[hl];\r
+        System.arraycopy(data, pos, addr, 0, hl);\r
+        pos += hl;\r
+    \r
+        int cl = XByteBuffer.toInt(data, pos);\r
+        pos += 4;\r
+    \r
+        byte[] command = new byte[cl];\r
+        System.arraycopy(data, pos, command, 0, command.length);\r
+        pos += command.length;\r
+    \r
+        int dl = XByteBuffer.toInt(data, pos);\r
+        pos += 4;\r
+    \r
+        byte[] domain = new byte[dl];\r
+        System.arraycopy(data, pos, domain, 0, domain.length);\r
+        pos += domain.length;\r
+    \r
+        byte[] uniqueId = new byte[16];\r
+        System.arraycopy(data, pos, uniqueId, 0, 16);\r
+        pos += 16;\r
+    \r
+        int pl = XByteBuffer.toInt(data, pos);\r
+        pos += 4;\r
+    \r
+        byte[] payload = new byte[pl];\r
+        System.arraycopy(data, pos, payload, 0, payload.length);\r
+        pos += payload.length;\r
+    \r
+        member.setHost(addr);\r
+        member.setPort(XByteBuffer.toInt(portd, 0));\r
+        member.setMemberAliveTime(XByteBuffer.toLong(alived, 0));\r
+        member.setUniqueId(uniqueId);\r
+        member.payload = payload;\r
+        member.domain = domain;\r
+        member.command = command;\r
+    \r
+        member.dataPkg = new byte[length];\r
+        System.arraycopy(data, offset, member.dataPkg, 0, length);\r
+    \r
+        return member;\r
+    }\r
+\r
+    public static MemberImpl getMember(byte[] data) {\r
+       return getMember(data,new MemberImpl());\r
+    }\r
+\r
+    /**\r
+     * Return the name of this object\r
+     * @return a unique name to the cluster\r
+     */\r
+    public String getName() {\r
+        return "tcp://"+getHostname()+":"+getPort();\r
+    }\r
+    \r
+    /**\r
+     * Return the listen port of this member\r
+     * @return - tcp listen port\r
+     */\r
+    public int getPort()  {\r
+        return this.port;\r
+    }\r
+\r
+    /**\r
+     * Return the TCP listen host for this member\r
+     * @return IP address or host name\r
+     */\r
+    public byte[] getHost()  {\r
+        return host;\r
+    }\r
+    \r
+    public String getHostname() {\r
+        if ( this.hostname != null ) return hostname;\r
+        else {\r
+            try {\r
+                this.hostname = java.net.InetAddress.getByAddress(host).getHostName();\r
+                return this.hostname;\r
+            }catch ( IOException x ) {\r
+                throw new RuntimeException("Unable to parse hostname.",x);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Contains information on how long this member has been online.\r
+     * The result is the number of milli seconds this member has been\r
+     * broadcasting its membership to the cluster.\r
+     * @return nr of milliseconds since this member started.\r
+     */\r
+    public long getMemberAliveTime() {\r
+       return memberAliveTime;\r
+    }\r
+\r
+    public long getServiceStartTime() {\r
+        return serviceStartTime;\r
+    }\r
+\r
+    public byte[] getUniqueId() {\r
+        return uniqueId;\r
+    }\r
+\r
+    public byte[] getPayload() {\r
+        return payload;\r
+    }\r
+\r
+    public byte[] getCommand() {\r
+        return command;\r
+    }\r
+\r
+    public byte[] getDomain() {\r
+        return domain;\r
+    }\r
+\r
+    public void setMemberAliveTime(long time) {\r
+       memberAliveTime=time;\r
+    }\r
+\r
+\r
+\r
+    /**\r
+     * String representation of this object\r
+     */\r
+    public String toString()  {\r
+        StringBuffer buf = new StringBuffer("org.apache.catalina.tribes.membership.MemberImpl[");\r
+        buf.append(getName()).append(",");\r
+        buf.append(getHostname()).append(",");\r
+        buf.append(port).append(", alive=");\r
+        buf.append(memberAliveTime).append(",");\r
+        buf.append("id=").append(bToS(this.uniqueId)).append(", ");\r
+        buf.append("payload=").append(bToS(this.payload,8)).append(", ");\r
+        buf.append("command=").append(bToS(this.command,8)).append(", ");\r
+        buf.append("domain=").append(bToS(this.domain,8)).append(", ");\r
+        buf.append("]");\r
+        return buf.toString();\r
+    }\r
+    public static String bToS(byte[] data) {\r
+        return bToS(data,data.length);\r
+    }\r
+    public static String bToS(byte[] data, int max) {\r
+        StringBuffer buf = new StringBuffer(4*16);\r
+        buf.append("{");\r
+        for (int i=0; data!=null && i<data.length; i++ ) {\r
+            buf.append(String.valueOf(data[i])).append(" ");\r
+            if ( i==max ) {\r
+                buf.append("...("+data.length+")");\r
+                break;\r
+            }\r
+        }\r
+        buf.append("}");\r
+        return buf.toString();\r
+    }\r
+\r
+    /**\r
+     * @see java.lang.Object#hashCode()\r
+     * @return The hash code\r
+     */\r
+    public int hashCode() {\r
+        return getHost()[0]+getHost()[1]+getHost()[2]+getHost()[3];\r
+    }\r
+\r
+    /**\r
+     * Returns true if the param o is a McastMember with the same name\r
+     * @param o\r
+     */\r
+    public boolean equals(Object o) {\r
+        if ( o instanceof MemberImpl )    {\r
+            return Arrays.equals(this.getHost(),((MemberImpl)o).getHost()) &&\r
+                   this.getPort() == ((MemberImpl)o).getPort() &&\r
+                   Arrays.equals(this.getUniqueId(),((MemberImpl)o).getUniqueId());\r
+        }\r
+        else\r
+            return false;\r
+    }\r
+    \r
+    public void setHost(byte[] host) {\r
+        this.host = host;\r
+    }\r
+    \r
+    public void setHostname(String host) throws IOException {\r
+        hostname = host;\r
+        this.host = java.net.InetAddress.getByName(host).getAddress();\r
+    }\r
+    \r
+    public void setMsgCount(int msgCount) {\r
+        this.msgCount = msgCount;\r
+    }\r
+\r
+    public void setPort(int port) {\r
+        this.port = port;\r
+        this.dataPkg = null;\r
+    }\r
+\r
+    public void setServiceStartTime(long serviceStartTime) {\r
+        this.serviceStartTime = serviceStartTime;\r
+    }\r
+\r
+    public void setUniqueId(byte[] uniqueId) {\r
+        this.uniqueId = uniqueId!=null?uniqueId:new byte[16];\r
+        getData(true,true);\r
+    }\r
+\r
+    public void setPayload(byte[] payload) {\r
+        byte[] oldpayload = this.payload;\r
+        this.payload = payload!=null?payload:new byte[0];\r
+        if ( this.getData(true,true).length > McastServiceImpl.MAX_PACKET_SIZE ) {\r
+            this.payload = oldpayload;\r
+            throw new IllegalArgumentException("Payload is to large for tribes to handle.");\r
+        }\r
+        \r
+    }\r
+\r
+    public void setCommand(byte[] command) {\r
+        this.command = command!=null?command:new byte[0];\r
+        getData(true,true);\r
+    }\r
+\r
+    public void setDomain(byte[] domain) {\r
+        this.domain = domain!=null?domain:new byte[0];\r
+        getData(true,true);\r
+    }\r
+\r
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {\r
+        int length = in.readInt();\r
+        byte[] message = new byte[length];\r
+        in.read(message);\r
+        getMember(message,this);\r
+        \r
+    }\r
+\r
+    public void writeExternal(ObjectOutput out) throws IOException {\r
+        byte[] data = this.getData();\r
+        out.writeInt(data.length);\r
+        out.write(data);\r
+    }\r
+    \r
+}\r
diff --git a/java/org/apache/catalina/tribes/membership/Membership.java b/java/org/apache/catalina/tribes/membership/Membership.java
new file mode 100644 (file)
index 0000000..c4cabde
--- /dev/null
@@ -0,0 +1,324 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.membership;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.catalina.tribes.Member;
+import java.util.Comparator;
+
+/**
+ * A <b>membership</b> implementation using simple multicast.
+ * This is the representation of a multicast membership.
+ * This class is responsible for maintaining a list of active cluster nodes in the cluster.
+ * If a node fails to send out a heartbeat, the node will be dismissed.
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 356540 $, $Date: 2005-12-13 10:53:40 -0600 (Tue, 13 Dec 2005) $
+ */
+public class Membership
+{
+    protected static final MemberImpl[] EMPTY_MEMBERS = new MemberImpl[0];
+    
+    /**
+     * The name of this membership, has to be the same as the name for the local
+     * member
+     */
+    protected MemberImpl local;
+    
+    /**
+     * A map of all the members in the cluster.
+     */
+    protected HashMap map = new HashMap();
+    
+    /**
+     * A list of all the members in the cluster.
+     */
+    protected MemberImpl[] members = EMPTY_MEMBERS;
+    
+    /**
+      * sort members by alive time
+      */
+    protected Comparator memberComparator = new MemberComparator();
+
+    public Object clone() {
+        synchronized (members) {
+            Membership clone = new Membership(local, memberComparator);
+            clone.map = (HashMap) map.clone();
+            clone.members = new MemberImpl[members.length];
+            System.arraycopy(members,0,clone.members,0,members.length);
+            return clone;
+        }
+    }
+
+    /**
+     * Constructs a new membership
+     * @param name - has to be the name of the local member. Used to filter the local member from the cluster membership
+     */
+    public Membership(MemberImpl local, boolean includeLocal) {
+        this.local = local;
+        if ( includeLocal ) addMember(local);
+    }
+
+    public Membership(MemberImpl local) {
+        this(local,false);
+    }
+
+    public Membership(MemberImpl local, Comparator comp) {
+        this(local,comp,false);
+    }
+
+    public Membership(MemberImpl local, Comparator comp, boolean includeLocal) {
+        this(local,includeLocal);
+        this.memberComparator = comp;
+    }
+    /**
+     * Reset the membership and start over fresh.
+     * Ie, delete all the members and wait for them to ping again and join this membership
+     */
+    public synchronized void reset() {
+        map.clear();
+        members = EMPTY_MEMBERS ;
+    }
+
+    /**
+     * Notify the membership that this member has announced itself.
+     *
+     * @param member - the member that just pinged us
+     * @return - true if this member is new to the cluster, false otherwise.
+     * @return - false if this member is the local member or updated.
+     */
+    public synchronized boolean memberAlive(MemberImpl member) {
+        boolean result = false;
+        //ignore ourselves
+        if (  member.equals(local) ) return result;
+
+        //return true if the membership has changed
+        MbrEntry entry = (MbrEntry)map.get(member);
+        if ( entry == null ) {
+            entry = addMember(member);
+            result = true;
+       } else {
+            //update the member alive time
+            MemberImpl updateMember = entry.getMember() ;
+            if(updateMember.getMemberAliveTime() != member.getMemberAliveTime()) {
+                //update fields that can change
+                updateMember.setMemberAliveTime(member.getMemberAliveTime());
+                updateMember.setPayload(member.getPayload());
+                updateMember.setCommand(member.getCommand());
+                Arrays.sort(members, memberComparator);
+            }
+        }
+        entry.accessed();
+        return result;
+    }
+
+    /**
+     * Add a member to this component and sort array with memberComparator
+     * @param member The member to add
+     */
+    public synchronized MbrEntry addMember(MemberImpl member) {
+      synchronized (members) {
+          MbrEntry entry = new MbrEntry(member);
+          if (!map.containsKey(member) ) {
+              map.put(member, entry);
+              MemberImpl results[] = new MemberImpl[members.length + 1];
+              for (int i = 0; i < members.length; i++) results[i] = members[i];
+              results[members.length] = member;
+              members = results;
+              Arrays.sort(members, memberComparator);
+          }
+          return entry;
+      }
+    }
+    
+    /**
+     * Remove a member from this component.
+     * 
+     * @param member The member to remove
+     */
+    public void removeMember(MemberImpl member) {
+        map.remove(member);
+        synchronized (members) {
+            int n = -1;
+            for (int i = 0; i < members.length; i++) {
+                if (members[i] == member || members[i].equals(member)) {
+                    n = i;
+                    break;
+                }
+            }
+            if (n < 0) return;
+            MemberImpl results[] = new MemberImpl[members.length - 1];
+            int j = 0;
+            for (int i = 0; i < members.length; i++) {
+                if (i != n)
+                    results[j++] = members[i];
+            }
+            members = results;
+        }
+    }
+    
+    /**
+     * Runs a refresh cycle and returns a list of members that has expired.
+     * This also removes the members from the membership, in such a way that
+     * getMembers() = getMembers() - expire()
+     * @param maxtime - the max time a member can remain unannounced before it is considered dead.
+     * @return the list of expired members
+     */
+    public synchronized MemberImpl[] expire(long maxtime) {
+        if(!hasMembers() )
+           return EMPTY_MEMBERS;
+       
+        ArrayList list = null;
+        Iterator i = map.values().iterator();
+        while(i.hasNext()) {
+            MbrEntry entry = (MbrEntry)i.next();
+            if( entry.hasExpired(maxtime) ) {
+                if(list == null) // only need a list when members are expired (smaller gc)
+                    list = new java.util.ArrayList();
+                list.add(entry.getMember());
+            }
+        }
+        
+        if(list != null) {
+            MemberImpl[] result = new MemberImpl[list.size()];
+            list.toArray(result);
+            for( int j=0; j<result.length; j++) {
+                removeMember(result[j]);
+            }
+            return result;
+        } else {
+            return EMPTY_MEMBERS ;
+        }
+    }
+
+    /**
+     * Returning that service has members or not
+     */
+    public boolean hasMembers() {
+        return members.length > 0 ;
+    }
+    
+    
+    public MemberImpl getMember(Member mbr) {
+        if(hasMembers()) {
+            MemberImpl result = null;
+            for ( int i=0; i<this.members.length && result==null; i++ ) {
+                if ( members[i].equals(mbr) ) result = members[i];
+            }//for
+            return result;
+        } else {
+            return null;
+        }
+    }
+    
+    public boolean contains(Member mbr) { 
+        return getMember(mbr)!=null;
+    }
+    /**
+     * Returning a list of all the members in the membership
+     * We not need a copy: add and remove generate new arrays.
+     */
+    public MemberImpl[] getMembers() {
+        if(hasMembers()) {
+            return members;
+        } else {
+            return EMPTY_MEMBERS;
+        }
+    }
+
+    /**
+     * get a copy from all member entries
+     */
+    protected synchronized MbrEntry[] getMemberEntries()
+    {
+        MbrEntry[] result = new MbrEntry[map.size()];
+        java.util.Iterator i = map.entrySet().iterator();
+        int pos = 0;
+        while ( i.hasNext() )
+            result[pos++] = ((MbrEntry)((java.util.Map.Entry)i.next()).getValue());
+        return result;
+    }
+    
+    // --------------------------------------------- Inner Class
+
+    private class MemberComparator implements java.util.Comparator {
+
+        public int compare(Object o1, Object o2) {
+            try {
+                return compare((MemberImpl) o1, (MemberImpl) o2);
+            } catch (ClassCastException x) {
+                return 0;
+            }
+        }
+
+        public int compare(MemberImpl m1, MemberImpl m2) {
+            //longer alive time, means sort first
+            long result = m2.getMemberAliveTime() - m1.getMemberAliveTime();
+            if (result < 0)
+                return -1;
+            else if (result == 0)
+                return 0;
+            else
+                return 1;
+        }
+    }
+    
+    /**
+     * Inner class that represents a member entry
+     */
+    protected static class MbrEntry
+    {
+
+        protected MemberImpl mbr;
+        protected long lastHeardFrom;
+
+        public MbrEntry(MemberImpl mbr) {
+           this.mbr = mbr;
+        }
+
+        /**
+         * Indicate that this member has been accessed.
+         */
+        public void accessed(){
+           lastHeardFrom = System.currentTimeMillis();
+        }
+
+        /**
+         * Return the actual Member object
+         */
+        public MemberImpl getMember() {
+            return mbr;
+        }
+
+        /**
+         * Check if this dude has expired
+         * @param maxtime The time threshold
+         */
+        public boolean hasExpired(long maxtime) {
+            long delta = System.currentTimeMillis() - lastHeardFrom;
+            return delta > maxtime;
+        }
+    }
+}
diff --git a/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..d6e3fc4
--- /dev/null
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mbeans-descriptors PUBLIC
+   "-//Apache Software Foundation//DTD Model MBeans Configuration File"
+   "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
+<mbeans-descriptors>
+
+  <mbean         name="McastService"
+           description="Cluster Membership service implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.cluster.mcast.McastService">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="mcastAddr"
+          description="Multicast IP Address"
+                 type="java.lang.String"/>
+    <attribute   name="mcastBindAddress"
+          description="Multicast IP Interface address (default auto)"
+                 type="java.lang.String"/>
+    <attribute   name="mcastPort"
+          description="Multicast UDP Port"
+                 type="int"/>
+    <attribute   name="mcastFrequency"
+          description="Ping Frequency at msec"
+                 type="long"/>
+    <attribute   name="mcastClusterDomain"
+          description="Cluster Domain of this member"
+                 type="java.lang.String"/>
+    <attribute   name="mcastDropTime"
+          description="Timeout from frequency ping after member disapper notify"
+                 type="long"/>
+    <attribute   name="mcastSoTimeout"
+          description="Multicast Socket Timeout"
+                 type="int"/>
+    <attribute   name="mcastTTL"
+          description=""
+                 type="int"/>
+    <attribute   name="localMemberName"
+          description="Complete local receiver information"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="membersByName"
+          description="Complete remote sender information"
+                 type="[Ljava.lang.String;"
+                 writeable="false"/>
+
+    <operation   name="start"
+               description="Start the cluster membership"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster membership"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/tribes/package.html b/java/org/apache/catalina/tribes/package.html
new file mode 100644 (file)
index 0000000..85a60e9
--- /dev/null
@@ -0,0 +1,70 @@
+<body>
+<head><title>Apache Tribes - The Tomcat Cluster Communication Module</title>
+<h3>QuickStart</h3>
+    <pre><code>
+            //create a channel
+            Channel myChannel = new GroupChannel();
+
+            //create my listeners
+            MyMessageListener msgListener = new MyMessageListener();
+            MyMemberListener mbrListener = new MyMemberListener();
+
+            //attach the listeners to the channel
+            myChannel.addMembershipListener(mbrListener);
+            myChannel.addChannelListener(msgListener);
+
+            //start the channel
+            myChannel.start(Channel.DEFAULT);
+
+            //create a message to be sent, message must implement java.io.Serializable
+            //for performance reasons you probably want them to implement java.io.Externalizable
+            Serializable myMsg = new MyMessage();
+
+            //retrieve my current members
+            Member[] group = myChannel.getMembers();
+
+            //send the message
+            channel.send(group,myMsg,Channel.SEND_OPTIONS_DEFAULT);
+
+    </code></pre>
+<h3>Interfaces for the Application Developer</h3>
+    <ol>
+        <li><code>org.apache.catalina.tribes.Channel</code>
+            Main component to interact with to send messages
+        </li>
+        <li><code>org.apache.catalina.tribes.MembershipListener</code>
+            Listen to membership changes
+        </li>
+        <li><code>org.apache.catalina.tribes.ChannelListener</code>
+            Listen to data messages
+        </li>
+        <li><code>org.apache.catalina.tribes.Member</code>
+            Identifies a node, implementation specific, default is org.apache.catalina.tribes.membership.MemberImpl
+        </li>
+    </ol>
+    <h3>Interfaces for the Tribes Component Developer</h3>
+    <ol>
+        <li><code>org.apache.catalina.tribes.Channel</code>
+            Main component to that the application interacts with
+        </li>
+        <li><code>org.apache.catalina.tribes.ChannelReceiver</code>
+            IO Component to receive messages over some network transport
+        </li>
+        <li><code>org.apache.catalina.tribes.ChannelSender</code>
+            IO Component to send messages over some network transport
+        </li>
+        <li><code>org.apache.catalina.tribes.MembershipService</code>
+            IO Component that handles membership discovery and 
+        </li>
+        <li><code>org.apache.catalina.tribes.ChannelInterceptor</code>
+            interceptors between the Channel and the IO layer
+        </li>
+        <li><code>org.apache.catalina.tribes.ChannelMessage</code>
+            The message that is sent through the interceptor stack down to the IO layer 
+        </li>
+
+        <li><code>org.apache.catalina.tribes.Member</code>
+            Identifies a node, implementation specific to the underlying IO logic
+        </li>
+    </ol>
+</body>
diff --git a/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java
new file mode 100644 (file)
index 0000000..13459e4
--- /dev/null
@@ -0,0 +1,1271 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.tipis;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.Heartbeat;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.group.Response;
+import org.apache.catalina.tribes.group.RpcCallback;
+import org.apache.catalina.tribes.group.RpcChannel;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.catalina.tribes.util.Arrays;
+import java.util.ConcurrentModificationException;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public abstract class AbstractReplicatedMap extends LinkedHashMap implements RpcCallback, ChannelListener, MembershipListener, Heartbeat {
+    protected static Log log = LogFactory.getLog(AbstractReplicatedMap.class);
+
+    /**
+     * The default initial capacity - MUST be a power of two.
+     */
+    public static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+    /**
+     * The load factor used when none specified in constructor.
+     **/
+    public static final float DEFAULT_LOAD_FACTOR = 0.75f;
+    
+    /**
+     * Used to identify the map
+     */
+    final String chset = "ISO-8859-1";
+
+//------------------------------------------------------------------------------
+//              INSTANCE VARIABLES
+//------------------------------------------------------------------------------
+    private transient long rpcTimeout = 5000;
+    private transient Channel channel;
+    private transient RpcChannel rpcChannel;
+    private transient byte[] mapContextName;
+    private transient boolean stateTransferred = false;
+    private transient Object stateMutex = new Object();
+    private transient HashMap mapMembers = new HashMap();
+    private transient int channelSendOptions = Channel.SEND_OPTIONS_DEFAULT;
+    private transient Object mapOwner;
+    private transient ClassLoader[] externalLoaders;
+    protected transient int currentNode = 0;
+    private transient long accessTimeout = 5000;
+    private transient String mapname = "";
+    
+
+//------------------------------------------------------------------------------
+//              CONSTRUCTORS
+//------------------------------------------------------------------------------
+
+    /**
+     * Creates a new map
+     * @param channel The channel to use for communication
+     * @param timeout long - timeout for RPC messags
+     * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+     * @param initialCapacity int - the size of this map, see HashMap
+     * @param loadFactor float - load factor, see HashMap
+     * @param cls - a list of classloaders to be used for deserialization of objects.
+     */
+    public AbstractReplicatedMap(Object owner,
+                                 Channel channel, 
+                                 long timeout, 
+                                 String mapContextName, 
+                                 int initialCapacity,
+                                 float loadFactor,
+                                 int channelSendOptions,
+                                 ClassLoader[] cls) {
+        super(initialCapacity, loadFactor);
+        init(owner, channel, mapContextName, timeout, channelSendOptions, cls);
+        
+    }
+
+    protected Member[] wrap(Member m) {
+        if ( m == null ) return new Member[0];
+        else return new Member[] {m};
+    }
+
+    private void init(Object owner, Channel channel, String mapContextName, long timeout, int channelSendOptions,ClassLoader[] cls) {
+        log.info("Initializing AbstractReplicatedMap with context name:"+mapContextName);
+        this.mapOwner = owner;
+        this.externalLoaders = cls;
+        this.channelSendOptions = channelSendOptions;
+        this.channel = channel;
+        this.rpcTimeout = timeout;
+
+        try {
+            this.mapname = mapContextName;
+            //unique context is more efficient if it is stored as bytes
+            this.mapContextName = mapContextName.getBytes(chset);
+        } catch (UnsupportedEncodingException x) {
+            log.warn("Unable to encode mapContextName[" + mapContextName + "] using getBytes(" + chset +") using default getBytes()", x);
+            this.mapContextName = mapContextName.getBytes();
+        }
+        if ( log.isTraceEnabled() ) log.trace("Created Lazy Map with name:"+mapContextName+", bytes:"+Arrays.toString(this.mapContextName));
+
+        //create an rpc channel and add the map as a listener
+        this.rpcChannel = new RpcChannel(this.mapContextName, channel, this);
+        this.channel.addChannelListener(this);
+        this.channel.addMembershipListener(this);
+        
+        
+        try {
+            broadcast(MapMessage.MSG_INIT, true);
+            //transfer state from another map
+            transferState();
+            broadcast(MapMessage.MSG_START, true);
+        } catch (ChannelException x) {
+            log.warn("Unable to send map start message.");
+            throw new RuntimeException("Unable to start replicated map.",x);
+        }
+
+    }
+    
+    
+    private void ping(long timeout) throws ChannelException {
+        //send out a map membership message, only wait for the first reply
+        MapMessage msg = new MapMessage(this.mapContextName, MapMessage.MSG_INIT,
+                                        false, null, null, null, wrap(channel.getLocalMember(false)));
+        Response[] resp = rpcChannel.send(channel.getMembers(), msg, rpcChannel.ALL_REPLY, (channelSendOptions), (int)accessTimeout);
+        for (int i = 0; i < resp.length; i++) {
+            memberAlive(resp[i].getSource());
+        }//for
+
+        synchronized (mapMembers) {
+            Iterator it = mapMembers.entrySet().iterator();
+            long now = System.currentTimeMillis();
+            while ( it.hasNext() ) {
+                Map.Entry entry = (Map.Entry)it.next();
+                long access = ((Long)entry.getValue()).longValue(); 
+                if ( (now - access) > timeout ) memberDisappeared((Member)entry.getKey());
+            }
+        }//synch
+    }
+
+    private void memberAlive(Member member) {
+        synchronized (mapMembers) {
+            if (!mapMembers.containsKey(member)) {
+                mapMemberAdded(member);
+            } //end if
+            mapMembers.put(member, new Long(System.currentTimeMillis()));
+        }
+    }
+
+    private void broadcast(int msgtype, boolean rpc) throws ChannelException {
+        //send out a map membership message, only wait for the first reply
+        MapMessage msg = new MapMessage(this.mapContextName, msgtype,
+                                        false, null, null, null, wrap(channel.getLocalMember(false)));
+        if ( rpc) {
+            Response[] resp = rpcChannel.send(channel.getMembers(), msg, rpcChannel.FIRST_REPLY, (channelSendOptions),rpcTimeout);
+            for (int i = 0; i < resp.length; i++) {
+                mapMemberAdded(resp[i].getSource());
+                messageReceived(resp[i].getMessage(), resp[i].getSource());
+            }
+        } else {
+            channel.send(channel.getMembers(),msg,channelSendOptions);
+        }
+    }
+
+    public void breakdown() {
+        finalize();
+    }
+
+    public void finalize() {
+        try {broadcast(MapMessage.MSG_STOP,false); }catch ( Exception ignore){}
+        //cleanup
+        if (this.rpcChannel != null) {
+            this.rpcChannel.breakdown();
+        }
+        if (this.channel != null) {
+            this.channel.removeChannelListener(this);
+            this.channel.removeMembershipListener(this);
+        }
+        this.rpcChannel = null;
+        this.channel = null;
+        this.mapMembers.clear();
+        super.clear();
+        this.stateTransferred = false;
+        this.externalLoaders = null;
+    }
+    
+    public int hashCode() {
+        return Arrays.hashCode(this.mapContextName);
+    }
+    
+    public boolean equals(Object o) {
+        if ( o == null ) return false;
+        if ( !(o instanceof AbstractReplicatedMap)) return false;
+        if ( !(o.getClass().equals(this.getClass())) ) return false;
+        AbstractReplicatedMap other = (AbstractReplicatedMap)o;
+        return Arrays.equals(mapContextName,other.mapContextName);
+    }
+
+//------------------------------------------------------------------------------
+//              GROUP COM INTERFACES
+//------------------------------------------------------------------------------
+    public Member[] getMapMembers(HashMap members) {
+        synchronized (members) {
+            Member[] result = new Member[members.size()];
+            members.keySet().toArray(result);
+            return result;
+        }
+    }
+    public Member[] getMapMembers() {
+        return getMapMembers(this.mapMembers);
+    }
+    
+    public Member[] getMapMembersExcl(Member[] exclude) {
+        synchronized (mapMembers) {
+            HashMap list = (HashMap)mapMembers.clone();
+            for (int i=0; i<exclude.length;i++) list.remove(exclude[i]);
+            return getMapMembers(list);
+        }
+    }
+
+
+    /**
+     * Replicates any changes to the object since the last time
+     * The object has to be primary, ie, if the object is a proxy or a backup, it will not be replicated<br>
+     * @param complete - if set to true, the object is replicated to its backup
+     * if set to false, only objects that implement ReplicatedMapEntry and the isDirty() returns true will
+     * be replicated
+     */
+    public void replicate(Object key, boolean complete) {
+        if ( log.isTraceEnabled() )
+            log.trace("Replicate invoked on key:"+key);
+        MapEntry entry = (MapEntry)super.get(key);
+        if ( !entry.isSerializable() ) return;
+        if (entry != null && entry.isPrimary() && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0) {
+            Object value = entry.getValue();
+            //check to see if we need to replicate this object isDirty()||complete
+            boolean repl = complete || ( (value instanceof ReplicatedMapEntry) && ( (ReplicatedMapEntry) value).isDirty());
+            
+            if (!repl) {
+                if ( log.isTraceEnabled() )
+                    log.trace("Not replicating:"+key+", no change made");
+                
+                return;
+            }
+            //check to see if the message is diffable
+            boolean diff = ( (value instanceof ReplicatedMapEntry) && ( (ReplicatedMapEntry) value).isDiffable());
+            MapMessage msg = null;
+            if (diff) {
+                ReplicatedMapEntry rentry = (ReplicatedMapEntry)entry.getValue();
+                try {
+                    rentry.lock();
+                    //construct a diff message
+                    msg = new MapMessage(mapContextName, MapMessage.MSG_BACKUP,
+                                         true, (Serializable) entry.getKey(), null,
+                                         rentry.getDiff(),
+                                         entry.getBackupNodes());
+                } catch (IOException x) {
+                    log.error("Unable to diff object. Will replicate the entire object instead.", x);
+                } finally {
+                    rentry.unlock();
+                }
+                
+            }
+            if (msg == null) {
+                //construct a complete
+                msg = new MapMessage(mapContextName, MapMessage.MSG_BACKUP,
+                                     false, (Serializable) entry.getKey(),
+                                     (Serializable) entry.getValue(),
+                                     null, entry.getBackupNodes());
+
+            }
+            try {
+                if ( channel!=null && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0 ) {
+                    channel.send(entry.getBackupNodes(), msg, channelSendOptions);
+                }
+            } catch (ChannelException x) {
+                log.error("Unable to replicate data.", x);
+            }
+        } //end if
+
+    }
+
+    /**
+     * This can be invoked by a periodic thread to replicate out any changes.
+     * For maps that don't store objects that implement ReplicatedMapEntry, this
+     * method should be used infrequently to avoid large amounts of data transfer
+     * @param complete boolean
+     */
+    public void replicate(boolean complete) {
+        Iterator i = super.entrySet().iterator();
+        while (i.hasNext()) {
+            Map.Entry e = (Map.Entry) i.next();
+            replicate(e.getKey(), complete);
+        } //while
+
+    }
+
+    public void transferState() {
+        try {
+            Member[] members = getMapMembers();
+            Member backup = members.length > 0 ? (Member) members[0] : null;
+            if (backup != null) {
+                MapMessage msg = new MapMessage(mapContextName, MapMessage.MSG_STATE, false,
+                                                null, null, null, null);
+                Response[] resp = rpcChannel.send(new Member[] {backup}, msg, rpcChannel.FIRST_REPLY, channelSendOptions, rpcTimeout);
+                if (resp.length > 0) {
+                    synchronized (stateMutex) {
+                        msg = (MapMessage) resp[0].getMessage();
+                        msg.deserialize(getExternalLoaders());
+                        ArrayList list = (ArrayList) msg.getValue();
+                        for (int i = 0; i < list.size(); i++) {
+                            messageReceived( (Serializable) list.get(i), resp[0].getSource());
+                        } //for
+                    }
+                } else {
+                    log.warn("Transfer state, 0 replies, probably a timeout.");
+                }
+            }
+        } catch (ChannelException x) {
+            log.error("Unable to transfer LazyReplicatedMap state.", x);
+        } catch (IOException x) {
+            log.error("Unable to transfer LazyReplicatedMap state.", x);
+        } catch (ClassNotFoundException x) {
+            log.error("Unable to transfer LazyReplicatedMap state.", x);
+        }
+        stateTransferred = true;
+    }
+
+    /**
+     * @todo implement state transfer
+     * @param msg Serializable
+     * @return Serializable - null if no reply should be sent
+     */
+    public Serializable replyRequest(Serializable msg, final Member sender) {
+        if (! (msg instanceof MapMessage))return null;
+        MapMessage mapmsg = (MapMessage) msg;
+
+        //map init request
+        if (mapmsg.getMsgType() == mapmsg.MSG_INIT) {
+            mapmsg.setBackUpNodes(wrap(channel.getLocalMember(false)));
+            return mapmsg;
+        }
+        
+        //map start request
+        if (mapmsg.getMsgType() == mapmsg.MSG_START) {
+            mapmsg.setBackUpNodes(wrap(channel.getLocalMember(false)));
+            mapMemberAdded(sender);
+            return mapmsg;
+        }
+
+        //backup request
+        if (mapmsg.getMsgType() == mapmsg.MSG_RETRIEVE_BACKUP) {
+            MapEntry entry = (MapEntry)super.get(mapmsg.getKey());
+            if (entry == null || (!entry.isSerializable()) )return null;
+            mapmsg.setValue( (Serializable) entry.getValue());
+            return mapmsg;
+        }
+
+        //state transfer request
+        if (mapmsg.getMsgType() == mapmsg.MSG_STATE) {
+            synchronized (stateMutex) { //make sure we dont do two things at the same time
+                ArrayList list = new ArrayList();
+                Iterator i = super.entrySet().iterator();
+                while (i.hasNext()) {
+                    Map.Entry e = (Map.Entry) i.next();
+                    MapEntry entry = (MapEntry) e.getValue();
+                    if ( entry.isSerializable() ) {
+                        MapMessage me = new MapMessage(mapContextName, MapMessage.MSG_PROXY,
+                            false, (Serializable) entry.getKey(), null, null, entry.getBackupNodes());
+                        list.add(me);
+                    }
+                }
+                mapmsg.setValue(list);
+                return mapmsg;
+                
+            } //synchronized
+        }
+
+        return null;
+
+    }
+
+    /**
+     * If the reply has already been sent to the requesting thread,
+     * the rpc callback can handle any data that comes in after the fact.
+     * @param msg Serializable
+     * @param sender Member
+     */
+    public void leftOver(Serializable msg, Member sender) {
+        //left over membership messages
+        if (! (msg instanceof MapMessage))return;
+
+        MapMessage mapmsg = (MapMessage) msg;
+        try {
+            mapmsg.deserialize(getExternalLoaders());
+            if (mapmsg.getMsgType() == MapMessage.MSG_START) {
+                mapMemberAdded(mapmsg.getBackupNodes()[0]);
+            } else if (mapmsg.getMsgType() == MapMessage.MSG_INIT) {
+                memberAlive(mapmsg.getBackupNodes()[0]);
+            }
+        } catch (IOException x ) {
+            log.error("Unable to deserialize MapMessage.",x);
+        } catch (ClassNotFoundException x ) {
+            log.error("Unable to deserialize MapMessage.",x);
+        }
+    }
+
+    public void messageReceived(Serializable msg, Member sender) {
+        if (! (msg instanceof MapMessage)) return;
+
+        MapMessage mapmsg = (MapMessage) msg;
+        if ( log.isTraceEnabled() ) {
+            log.trace("Map["+mapname+"] received message:"+mapmsg);
+        }
+        
+        try {
+            mapmsg.deserialize(getExternalLoaders());
+        } catch (IOException x) {
+            log.error("Unable to deserialize MapMessage.", x);
+            return;
+        } catch (ClassNotFoundException x) {
+            log.error("Unable to deserialize MapMessage.", x);
+            return;
+        }
+        if ( log.isTraceEnabled() ) 
+            log.trace("Map message received from:"+sender.getName()+" msg:"+mapmsg);
+        if (mapmsg.getMsgType() == MapMessage.MSG_START) {
+            mapMemberAdded(mapmsg.getBackupNodes()[0]);
+        }
+
+        if (mapmsg.getMsgType() == MapMessage.MSG_STOP) {
+            memberDisappeared(mapmsg.getBackupNodes()[0]);
+        }
+
+        if (mapmsg.getMsgType() == MapMessage.MSG_PROXY) {
+            MapEntry entry = (MapEntry)super.get(mapmsg.getKey());
+            if ( entry==null ) {
+                entry = new MapEntry(mapmsg.getKey(), mapmsg.getValue());
+                entry.setBackup(false);
+                entry.setProxy(true);
+                entry.setBackupNodes(mapmsg.getBackupNodes());
+                super.put(entry.getKey(), entry);
+            } else {
+                entry.setProxy(true);
+                entry.setBackup(false);
+                entry.setBackupNodes(mapmsg.getBackupNodes());
+            }
+        }
+
+        if (mapmsg.getMsgType() == MapMessage.MSG_REMOVE) {
+            super.remove(mapmsg.getKey());
+        }
+
+        if (mapmsg.getMsgType() == MapMessage.MSG_BACKUP) {
+            MapEntry entry = (MapEntry)super.get(mapmsg.getKey());
+            if (entry == null) {
+                entry = new MapEntry(mapmsg.getKey(), mapmsg.getValue());
+                entry.setBackup(true);
+                entry.setProxy(false);
+                entry.setBackupNodes(mapmsg.getBackupNodes());
+                if (mapmsg.getValue()!=null && mapmsg.getValue() instanceof ReplicatedMapEntry ) {
+                    ((ReplicatedMapEntry)mapmsg.getValue()).setOwner(getMapOwner());
+                }
+            } else {
+                entry.setBackup(true);
+                entry.setProxy(false);
+                entry.setBackupNodes(mapmsg.getBackupNodes());
+                if (entry.getValue() instanceof ReplicatedMapEntry) {
+                    ReplicatedMapEntry diff = (ReplicatedMapEntry) entry.getValue();
+                    if (mapmsg.isDiff()) {
+                        try {
+                            diff.lock();
+                            diff.applyDiff(mapmsg.getDiffValue(), 0, mapmsg.getDiffValue().length);
+                        } catch (Exception x) {
+                            log.error("Unable to apply diff to key:" + entry.getKey(), x);
+                        } finally {
+                            diff.unlock();
+                        }
+                    } else {
+                        if ( mapmsg.getValue()!=null ) entry.setValue(mapmsg.getValue());
+                        ((ReplicatedMapEntry)entry.getValue()).setOwner(getMapOwner());
+                    } //end if
+                } else if  (mapmsg.getValue() instanceof ReplicatedMapEntry) {
+                    ReplicatedMapEntry re = (ReplicatedMapEntry)mapmsg.getValue();
+                    re.setOwner(getMapOwner());
+                    entry.setValue(re);
+                } else {
+                    if ( mapmsg.getValue()!=null ) entry.setValue(mapmsg.getValue());
+                } //end if
+            } //end if
+            super.put(entry.getKey(), entry);
+        } //end if
+    }
+
+    public boolean accept(Serializable msg, Member sender) {
+        boolean result = false;
+        if (msg instanceof MapMessage) {
+            if ( log.isTraceEnabled() ) log.trace("Map["+mapname+"] accepting...."+msg);
+            result = Arrays.equals(mapContextName, ( (MapMessage) msg).getMapId());
+            if ( log.isTraceEnabled() ) log.trace("Msg["+mapname+"] accepted["+result+"]...."+msg);
+        }
+        return result;
+    }
+
+    public void mapMemberAdded(Member member) {
+        if ( member.equals(getChannel().getLocalMember(false)) ) return;
+        boolean memberAdded = false;
+        //select a backup node if we don't have one
+        synchronized (mapMembers) {
+            if (!mapMembers.containsKey(member) ) {
+                mapMembers.put(member, new Long(System.currentTimeMillis()));
+                memberAdded = true;
+            }
+        }
+        if ( memberAdded ) {
+            synchronized (stateMutex) {
+                Iterator i = super.entrySet().iterator();
+                while (i.hasNext()) {
+                    Map.Entry e = (Map.Entry) i.next();
+                    MapEntry entry = (MapEntry) e.getValue();
+                    if ( entry == null ) continue;
+                    if (entry.isPrimary() && (entry.getBackupNodes() == null || entry.getBackupNodes().length == 0)) {
+                        try {
+                            Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue());
+                            entry.setBackupNodes(backup);
+                        } catch (ChannelException x) {
+                            log.error("Unable to select backup node.", x);
+                        } //catch
+                    } //end if
+                } //while
+            } //synchronized
+        }//end if
+    }
+    
+    public boolean inSet(Member m, Member[] set) {
+        if ( set == null ) return false;
+        boolean result = false;
+        for (int i=0; i<set.length && (!result); i++ )
+            if ( m.equals(set[i]) ) result = true;
+        return result;
+    }
+
+    public Member[] excludeFromSet(Member[] mbrs, Member[] set) {
+        ArrayList result = new ArrayList();
+        for (int i=0; i<set.length; i++ ) {
+            boolean include = true;
+            for (int j=0; j<mbrs.length; j++ ) 
+                if ( mbrs[j].equals(set[i]) ) include = false;
+            if ( include ) result.add(set[i]);
+        }
+        return (Member[])result.toArray(new Member[result.size()]);
+    }
+
+    public void memberAdded(Member member) {
+        //do nothing
+    }
+
+    public void memberDisappeared(Member member) {
+        boolean removed = false;
+        synchronized (mapMembers) {
+            removed = (mapMembers.remove(member) != null );
+        }
+        Iterator i = super.entrySet().iterator();
+        while (i.hasNext()) {
+            Map.Entry e = (Map.Entry) i.next();
+            MapEntry entry = (MapEntry) e.getValue();
+            if (entry.isPrimary() && inSet(member,entry.getBackupNodes())) {
+                try {
+                    Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue());
+                    entry.setBackupNodes(backup);
+                } catch (ChannelException x) {
+                    log.error("Unable to relocate[" + entry.getKey() + "] to a new backup node", x);
+                }
+            } //end if
+        } //while
+    }
+
+    public int getNextBackupIndex() {
+        int size = mapMembers.size();
+        if (mapMembers.size() == 0)return -1;
+        int node = currentNode++;
+        if (node >= size) {
+            node = 0;
+            currentNode = 0;
+        }
+        return node;
+    }
+    public Member getNextBackupNode() {
+        Member[] members = getMapMembers();
+        int node = getNextBackupIndex();
+        if ( members.length == 0 || node==-1) return null;
+        if ( node >= members.length ) node = 0;
+        return members[node];
+    }
+
+    protected abstract Member[] publishEntryInfo(Object key, Object value) throws ChannelException;
+    
+    public void heartbeat() {
+        try {
+            ping(accessTimeout);
+        }catch ( Exception x ) {
+            log.error("Unable to send AbstractReplicatedMap.ping message",x);
+        }
+    }
+
+//------------------------------------------------------------------------------    
+//              METHODS TO OVERRIDE    
+//------------------------------------------------------------------------------
+  
+    /**
+     * Removes an object from this map, it will also remove it from 
+     * 
+     * @param key Object
+     * @return Object
+     */
+    public Object remove(Object key) {
+        MapEntry entry = (MapEntry)super.remove(key);
+
+        try {
+            MapMessage msg = new MapMessage(getMapContextName(),MapMessage.MSG_REMOVE,false,(Serializable)key,null,null,null);
+            getChannel().send(getMapMembers(), msg,getChannelSendOptions());
+        } catch ( ChannelException x ) {
+            log.error("Unable to replicate out data for a LazyReplicatedMap.remove operation",x);
+        }
+        return entry!=null?entry.getValue():null;
+    }
+    
+    public Object get(Object key) {
+            MapEntry entry = (MapEntry)super.get(key);
+            if (log.isTraceEnabled()) log.trace("Requesting id:"+key+" entry:"+entry);
+            if ( entry == null ) return null;
+            if ( !entry.isPrimary() ) {
+                //if the message is not primary, we need to retrieve the latest value
+                try {
+                    Member[] backup = null;
+                    MapMessage msg = null;
+                    if ( !entry.isBackup() ) {
+                        //make sure we don't retrieve from ourselves
+                        msg = new MapMessage(getMapContextName(), MapMessage.MSG_RETRIEVE_BACKUP, false,
+                                             (Serializable) key, null, null, null);
+                        Response[] resp = getRpcChannel().send(entry.getBackupNodes(),msg, this.getRpcChannel().FIRST_REPLY, Channel.SEND_OPTIONS_DEFAULT, getRpcTimeout());
+                        if (resp == null || resp.length == 0) {
+                            //no responses
+                            log.warn("Unable to retrieve remote object for key:" + key);
+                            return null;
+                        }
+                        msg = (MapMessage) resp[0].getMessage();
+                        msg.deserialize(getExternalLoaders());
+                        backup = entry.getBackupNodes();
+                        if ( entry.getValue() instanceof ReplicatedMapEntry ) {
+                            ReplicatedMapEntry val = (ReplicatedMapEntry)entry.getValue();
+                            val.setOwner(getMapOwner());
+                        }
+                        if ( msg.getValue()!=null ) entry.setValue(msg.getValue());
+                    }
+                    if (entry.isBackup()) {
+                        //select a new backup node
+                        backup = publishEntryInfo(key, entry.getValue());
+                    } else if ( entry.isProxy() ) {
+                        //invalidate the previous primary
+                        msg = new MapMessage(getMapContextName(),MapMessage.MSG_PROXY,false,(Serializable)key,null,null,backup);
+                        Member[] dest = getMapMembersExcl(backup);
+                        if ( dest!=null && dest.length >0) {
+                            getChannel().send(dest, msg, getChannelSendOptions());
+                        }
+                    }
+    
+                    entry.setBackupNodes(backup);
+                    entry.setBackup(false);
+                    entry.setProxy(false);
+    
+    
+                } catch (Exception x) {
+                    log.error("Unable to replicate out data for a LazyReplicatedMap.get operation", x);
+                    return null;
+                }
+            }
+            if (log.isTraceEnabled()) log.trace("Requesting id:"+key+" result:"+entry.getValue());
+            return entry.getValue();
+    }    
+
+    
+    protected void printMap(String header) {
+        try {
+            System.out.println("\nDEBUG MAP:"+header);
+            System.out.println("Map["+ new String(mapContextName, chset) + ", Map Size:" + super.size());
+            Member[] mbrs = getMapMembers();
+            for ( int i=0; i<mbrs.length;i++ ) {
+                System.out.println("Mbr["+(i+1)+"="+mbrs[i].getName());
+            }
+            Iterator i = super.entrySet().iterator();
+            int cnt = 0;
+
+            while (i.hasNext()) {
+                Map.Entry e = (Map.Entry) i.next();
+                System.out.println( (++cnt) + ". " + e.getValue());
+            }
+            System.out.println("EndMap]\n\n");
+        }catch ( Exception ignore) {
+            ignore.printStackTrace();
+        }
+    }
+    
+    /**
+         * Returns true if the key has an entry in the map.
+         * The entry can be a proxy or a backup entry, invoking <code>get(key)</code>
+         * will make this entry primary for the group
+         * @param key Object
+         * @return boolean
+         */
+        public boolean containsKey(Object key) {
+            return super.containsKey(key);
+        }
+    
+    
+        public Object put(Object key, Object value) {
+            MapEntry entry = new MapEntry(key,value);
+            entry.setBackup(false);
+            entry.setProxy(false);
+    
+            Object old = null;
+    
+            //make sure that any old values get removed
+            if ( containsKey(key) ) old = remove(key);
+            try {
+                Member[] backup = publishEntryInfo(key, value);
+                entry.setBackupNodes(backup);
+            } catch (ChannelException x) {
+                log.error("Unable to replicate out data for a LazyReplicatedMap.put operation", x);
+            }
+            super.put(key,entry);
+            return old;
+        }
+    
+    
+        /**
+         * Copies all values from one map to this instance
+         * @param m Map
+         */
+        public void putAll(Map m) {
+            Iterator i = m.entrySet().iterator();
+            while ( i.hasNext() ) {
+                Map.Entry entry = (Map.Entry)i.next();
+                put(entry.getKey(),entry.getValue());
+            }
+        }
+    
+        public void clear() {
+            //only delete active keys
+            Iterator keys = keySet().iterator();
+            while ( keys.hasNext() ) remove(keys.next());
+        }
+    
+        public boolean containsValue(Object value) {
+            if ( value == null ) {
+                return super.containsValue(value);
+            } else {
+                Iterator i = super.entrySet().iterator();
+                while (i.hasNext()) {
+                    Map.Entry e = (Map.Entry) i.next();
+                    MapEntry entry = (MapEntry) e.getValue();
+                    if (entry.isPrimary() && value.equals(entry.getValue())) return true;
+                }//while
+                return false;
+            }//end if
+        }
+    
+        public Object clone() {
+            throw new UnsupportedOperationException("This operation is not valid on a replicated map");
+        }
+    
+        /**
+         * Returns the entire contents of the map
+         * Map.Entry.getValue() will return a LazyReplicatedMap.MapEntry object containing all the information 
+         * about the object.
+         * @return Set
+         */
+        public Set entrySetFull() {
+            return super.entrySet();
+        }
+    
+        public Set keySetFull() {
+            return super.keySet();
+        }
+    
+        public int sizeFull() {
+            return super.size();
+        }
+    
+        public Set entrySet() {
+            LinkedHashSet set = new LinkedHashSet(super.size());
+            Iterator i = super.entrySet().iterator();
+            while ( i.hasNext() ) {
+                Map.Entry e = (Map.Entry)i.next();
+                MapEntry entry = (MapEntry)e.getValue();
+                if ( entry.isPrimary() ) set.add(entry);
+            }
+            return Collections.unmodifiableSet(set);
+        }
+    
+        public Set keySet() {
+            //todo implement
+            //should only return keys where this is active.
+            LinkedHashSet set = new LinkedHashSet(super.size());
+            Iterator i = super.entrySet().iterator();
+            while ( i.hasNext() ) {
+                Map.Entry e = (Map.Entry)i.next();
+                MapEntry entry = (MapEntry)e.getValue();
+                if ( entry.isPrimary() ) set.add(entry.getKey());
+            }
+            return Collections.unmodifiableSet(set);
+        }
+    
+    
+        public int size() {
+            //todo, implement a counter variable instead
+            //only count active members in this node
+            int counter = 0;
+            Object[] items = super.entrySet().toArray();
+            for (int i=0; i<items.length; i++ ) {
+                Map.Entry e = (Map.Entry) items[i];
+                if ( e != null ) {
+                    MapEntry entry = (MapEntry) e.getValue();
+                    if (entry.isPrimary() && entry.getValue() != null) counter++;
+                }
+            }
+            return counter;
+        }
+    
+        protected boolean removeEldestEntry(Map.Entry eldest) {
+            return false;
+        }
+    
+        public boolean isEmpty() {
+            return size()==0;
+        }
+    
+        public Collection values() {
+            ArrayList values = new ArrayList(super.size());
+            Iterator i = super.entrySet().iterator();
+            while ( i.hasNext() ) {
+                Map.Entry e = (Map.Entry)i.next();
+                MapEntry entry = (MapEntry)e.getValue();
+                if ( entry.isPrimary() && entry.getValue()!=null) values.add(entry.getValue());
+            }
+            return Collections.unmodifiableCollection(values);
+        }
+        
+
+//------------------------------------------------------------------------------
+//                Map Entry class
+//------------------------------------------------------------------------------
+    public static class MapEntry implements Map.Entry {
+        private boolean backup;
+        private boolean proxy;
+        private Member[] backupNodes;
+
+        private Object key;
+        private Object value;
+
+        public MapEntry(Object key, Object value) {
+            setKey(key);
+            setValue(value);
+            
+        }
+        
+        public boolean isKeySerializable() {
+            return (key == null) || (key instanceof Serializable);
+        }
+        
+        public boolean isValueSerializable() {
+            return (value==null) || (value instanceof Serializable);
+        }
+        
+        public boolean isSerializable() {
+            return isKeySerializable() && isValueSerializable();
+        }
+        
+        public boolean isBackup() {
+            return backup;
+        }
+
+        public void setBackup(boolean backup) {
+            this.backup = backup;
+        }
+
+        public boolean isProxy() {
+            return proxy;
+        }
+
+        public boolean isPrimary() {
+            return ( (!proxy) && (!backup));
+        }
+
+        public void setProxy(boolean proxy) {
+            this.proxy = proxy;
+        }
+
+        public boolean isDiffable() {
+            return (value instanceof ReplicatedMapEntry) &&
+                   ((ReplicatedMapEntry)value).isDiffable();
+        }
+
+        public void setBackupNodes(Member[] nodes) {
+            this.backupNodes = nodes;
+        }
+
+        public Member[] getBackupNodes() {
+            return backupNodes;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public Object setValue(Object value) {
+            Object old = this.value;
+            this.value = (Serializable) value;
+            return old;
+        }
+
+        public Object getKey() {
+            return key;
+        }
+        
+        public Object setKey(Object key) {
+            Object old = this.key;
+            this.key = (Serializable)key;
+            return old;
+        }
+
+        public int hashCode() {
+            return key.hashCode();
+        }
+
+        public boolean equals(Object o) {
+            return key.equals(o);
+        }
+
+        /**
+         * apply a diff, or an entire object
+         * @param data byte[]
+         * @param offset int
+         * @param length int
+         * @param diff boolean
+         * @throws IOException
+         * @throws ClassNotFoundException
+         */
+        public void apply(byte[] data, int offset, int length, boolean diff) throws IOException, ClassNotFoundException {
+            if (isDiffable() && diff) {
+                ReplicatedMapEntry rentry = (ReplicatedMapEntry) value;
+                try {
+                    rentry.lock();
+                    rentry.applyDiff(data, offset, length);
+                } finally {
+                    rentry.unlock();
+                }
+            } else if (length == 0) {
+                value = null;
+                proxy = true;
+            } else {
+                value = XByteBuffer.deserialize(data, offset, length);
+            }
+        }
+        
+        public String toString() {
+            StringBuffer buf = new StringBuffer("MapEntry[key:");
+            buf.append(getKey()).append("; ");
+            buf.append("value:").append(getValue()).append("; ");
+            buf.append("primary:").append(isPrimary()).append("; ");
+            buf.append("backup:").append(isBackup()).append("; ");
+            buf.append("proxy:").append(isProxy()).append(";]");
+            return buf.toString();
+        }
+
+    }
+
+//------------------------------------------------------------------------------
+//                map message to send to and from other maps
+//------------------------------------------------------------------------------
+
+    public static class MapMessage implements Serializable {
+        public static final int MSG_BACKUP = 1;
+        public static final int MSG_RETRIEVE_BACKUP = 2;
+        public static final int MSG_PROXY = 3;
+        public static final int MSG_REMOVE = 4;
+        public static final int MSG_STATE = 5;
+        public static final int MSG_START = 6;
+        public static final int MSG_STOP = 7;
+        public static final int MSG_INIT = 8;
+
+        private byte[] mapId;
+        private int msgtype;
+        private boolean diff;
+        private transient Serializable key;
+        private transient Serializable value;
+        private byte[] valuedata;
+        private byte[] keydata;
+        private byte[] diffvalue;
+        private Member[] nodes;
+        
+        public String toString() {
+            StringBuffer buf = new StringBuffer("MapMessage[context=");
+            buf.append(new String(mapId));
+            buf.append("; type=");
+            buf.append(getTypeDesc());
+            buf.append("; key=");
+            buf.append(key);
+            buf.append("; value=");
+            buf.append(value);
+            return buf.toString();
+        }
+        
+        public String getTypeDesc() {
+            switch (msgtype) {
+                case MSG_BACKUP: return "MSG_BACKUP";
+                case MSG_RETRIEVE_BACKUP: return "MSG_RETRIEVE_BACKUP";
+                case MSG_PROXY: return "MSG_PROXY";
+                case MSG_REMOVE: return "MSG_REMOVE";
+                case MSG_STATE: return "MSG_STATE";
+                case MSG_START: return "MSG_START";
+                case MSG_STOP: return "MSG_STOP";
+                case MSG_INIT: return "MSG_INIT";
+                default : return "UNKNOWN";
+            }
+        }
+
+        public MapMessage() {}
+
+        public MapMessage(byte[] mapId,int msgtype, boolean diff,
+                          Serializable key, Serializable value,
+                          byte[] diffvalue, Member[] nodes)  {
+            this.mapId = mapId;
+            this.msgtype = msgtype;
+            this.diff = diff;
+            this.key = key;
+            this.value = value;
+            this.diffvalue = diffvalue;
+            this.nodes = nodes;
+            setValue(value);
+            setKey(key);
+        }
+        
+        public void deserialize(ClassLoader[] cls) throws IOException, ClassNotFoundException {
+            key(cls);
+            value(cls);
+        }
+
+        public int getMsgType() {
+            return msgtype;
+        }
+
+        public boolean isDiff() {
+            return diff;
+        }
+
+        public Serializable getKey() {
+            try {
+                return key(null);
+            } catch ( Exception x ) {
+                log.error("Deserialization error of the MapMessage.key",x);
+                return null;
+            }
+        }
+
+        public Serializable key(ClassLoader[] cls) throws IOException, ClassNotFoundException {
+            if ( key!=null ) return key;
+            if ( keydata == null || keydata.length == 0 ) return null;
+            key = XByteBuffer.deserialize(keydata,0,keydata.length,cls);
+            keydata = null;
+            return key;
+        }
+        
+        public byte[] getKeyData() {
+            return keydata;
+        }
+        
+        public Serializable getValue() {
+            try {
+                return value(null);
+            } catch ( Exception x ) {
+                log.error("Deserialization error of the MapMessage.value",x);
+                return null;
+            }
+        }
+
+        public Serializable value(ClassLoader[] cls) throws IOException, ClassNotFoundException  {
+            if ( value!=null ) return value;
+            if ( valuedata == null || valuedata.length == 0 ) return null;
+            value = XByteBuffer.deserialize(valuedata,0,valuedata.length,cls);
+            valuedata = null;;
+            return value;
+        }
+        
+        public byte[] getValueData() {
+            return valuedata;
+        }
+
+        public byte[] getDiffValue() {
+            return diffvalue;
+        }
+
+        public Member[] getBackupNodes() {
+            return nodes;
+        }
+
+        private void setBackUpNodes(Member[] nodes) {
+            this.nodes = nodes;
+        }
+
+        public byte[] getMapId() {
+            return mapId;
+        }
+
+        public void setValue(Serializable value) {
+            try {
+                if ( value != null ) valuedata = XByteBuffer.serialize(value);
+                this.value = value;
+            }catch ( IOException x ) {
+                throw new RuntimeException(x);
+            }
+        }
+        
+        public void setKey(Serializable key) {
+            try {
+                if (key != null) keydata = XByteBuffer.serialize(key);
+                this.key = key;
+            } catch (IOException x) {
+                throw new RuntimeException(x);
+            }
+        }
+        
+        protected Member[] readMembers(ObjectInput in) throws IOException, ClassNotFoundException {
+            int nodecount = in.readInt();
+            Member[] members = new Member[nodecount];
+            for ( int i=0; i<members.length; i++ ) {
+                byte[] d = new byte[in.readInt()];
+                in.read(d);
+                if (d.length > 0) members[i] = MemberImpl.getMember(d);
+            }
+            return members;
+        }
+        
+        protected void writeMembers(ObjectOutput out,Member[] members) throws IOException {
+            if ( members == null ) members = new Member[0];
+            out.writeInt(members.length);
+            for (int i=0; i<members.length; i++ ) {
+                if ( members[i] != null ) {
+                    byte[] d = members[i] != null ? ( (MemberImpl)members[i]).getData(false) : new byte[0];
+                    out.writeInt(d.length);
+                    out.write(d);
+                }
+            }
+        }
+        
+        
+        /**
+         * shallow clone
+         * @return Object
+         */
+        public Object clone() {
+            MapMessage msg = new MapMessage(this.mapId, this.msgtype, this.diff, this.key, this.value, this.diffvalue, this.nodes);
+            msg.keydata = this.keydata;
+            msg.valuedata = this.valuedata;
+            return msg;
+        }
+    } //MapMessage
+
+
+    public Channel getChannel() {
+        return channel;
+    }
+
+    public byte[] getMapContextName() {
+        return mapContextName;
+    }
+
+    public RpcChannel getRpcChannel() {
+        return rpcChannel;
+    }
+
+    public long getRpcTimeout() {
+        return rpcTimeout;
+    }
+
+    public Object getStateMutex() {
+        return stateMutex;
+    }
+
+    public boolean isStateTransferred() {
+        return stateTransferred;
+    }
+
+    public Object getMapOwner() {
+        return mapOwner;
+    }
+
+    public ClassLoader[] getExternalLoaders() {
+        return externalLoaders;
+    }
+
+    public int getChannelSendOptions() {
+        return channelSendOptions;
+    }
+
+    public long getAccessTimeout() {
+        return accessTimeout;
+    }
+
+    public void setMapOwner(Object mapOwner) {
+        this.mapOwner = mapOwner;
+    }
+
+    public void setExternalLoaders(ClassLoader[] externalLoaders) {
+        this.externalLoaders = externalLoaders;
+    }
+
+    public void setChannelSendOptions(int channelSendOptions) {
+        this.channelSendOptions = channelSendOptions;
+    }
+
+    public void setAccessTimeout(long accessTimeout) {
+        this.accessTimeout = accessTimeout;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java
new file mode 100644 (file)
index 0000000..ccfc1d4
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.tipis;
+
+import java.io.Serializable;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.group.RpcCallback;
+import org.apache.catalina.tribes.util.Arrays;
+import org.apache.catalina.tribes.UniqueId;
+
+/**
+ * A smart implementation of a stateful replicated map. uses primary/secondary backup strategy. 
+ * One node is always the primary and one node is always the backup.
+ * This map is synchronized across a cluster, and only has one backup member.<br/>
+ * A perfect usage for this map would be a session map for a session manager in a clustered environment.<br/>
+ * The only way to modify this list is to use the <code>put, putAll, remove</code> methods.
+ * entrySet, entrySetFull, keySet, keySetFull, returns all non modifiable sets.<br><br>
+ * If objects (values) in the map change without invoking <code>put()</code> or <code>remove()</code>
+ * the data can be distributed using two different methods:<br>
+ * <code>replicate(boolean)</code> and <code>replicate(Object, boolean)</code><br>
+ * These two methods are very important two understand. The map can work with two set of value objects:<br>
+ * 1. Serializable - the entire object gets serialized each time it is replicated<br>
+ * 2. ReplicatedMapEntry - this interface allows for a isDirty() flag and to replicate diffs if desired.<br>
+ * Implementing the <code>ReplicatedMapEntry</code> interface allows you to decide what objects 
+ * get replicated and how much data gets replicated each time.<br>
+ * If you implement a smart AOP mechanism to detect changes in underlying objects, you can replicate
+ * only those changes by implementing the ReplicatedMapEntry interface, and return true when isDiffable()
+ * is invoked.<br><br>
+ * 
+ * This map implementation doesn't have a background thread running to replicate changes.
+ * If you do have changes without invoking put/remove then you need to invoke one of the following methods:
+ * <ul>
+ * <li><code>replicate(Object,boolean)</code> - replicates only the object that belongs to the key</li>
+ * <li><code>replicate(boolean)</code> - Scans the entire map for changes and replicates data</li>
+ *  </ul>
+ * the <code>boolean</code> value in the <code>replicate</code> method used to decide 
+ * whether to only replicate objects that implement the <code>ReplicatedMapEntry</code> interface
+ * or to replicate all objects. If an object doesn't implement the <code>ReplicatedMapEntry</code> interface
+ * each time the object gets replicated the entire object gets serialized, hence a call to <code>replicate(true)</code>
+ * will replicate all objects in this map that are using this node as primary.
+ * 
+ * <br><br><b>REMBER TO CALL <code>breakdown()</code> or <code>finalize()</code> when you are done with the map to 
+ * avoid memory leaks.<br><br>
+ * @todo implement periodic sync/transfer thread
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class LazyReplicatedMap extends AbstractReplicatedMap 
+    implements RpcCallback, ChannelListener, MembershipListener {
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LazyReplicatedMap.class);
+    
+    
+    
+//------------------------------------------------------------------------------    
+//              CONSTRUCTORS / DESTRUCTORS
+//------------------------------------------------------------------------------   
+    /**
+         * Creates a new map
+         * @param channel The channel to use for communication
+         * @param timeout long - timeout for RPC messags
+         * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+         * @param initialCapacity int - the size of this map, see HashMap
+         * @param loadFactor float - load factor, see HashMap
+         */
+        public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, float loadFactor, ClassLoader[] cls) {
+            super(owner,channel,timeout,mapContextName,initialCapacity,loadFactor, Channel.SEND_OPTIONS_DEFAULT,cls);
+        }
+
+        /**
+         * Creates a new map
+         * @param channel The channel to use for communication
+         * @param timeout long - timeout for RPC messags
+         * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+         * @param initialCapacity int - the size of this map, see HashMap
+         */
+        public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) {
+            super(owner, channel,timeout,mapContextName,initialCapacity, LazyReplicatedMap.DEFAULT_LOAD_FACTOR, Channel.SEND_OPTIONS_DEFAULT, cls);
+        }
+
+        /**
+         * Creates a new map
+         * @param channel The channel to use for communication
+         * @param timeout long - timeout for RPC messags
+         * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+         */
+        public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) {
+            super(owner, channel,timeout,mapContextName, LazyReplicatedMap.DEFAULT_INITIAL_CAPACITY,LazyReplicatedMap.DEFAULT_LOAD_FACTOR,Channel.SEND_OPTIONS_DEFAULT, cls);
+        }
+
+
+
+
+    
+//------------------------------------------------------------------------------    
+//              METHODS TO OVERRIDE    
+//------------------------------------------------------------------------------
+    /**
+     * publish info about a map pair (key/value) to other nodes in the cluster
+     * @param key Object
+     * @param value Object
+     * @return Member - the backup node
+     * @throws ChannelException
+     */
+    protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException {
+        if  (! (key instanceof Serializable && value instanceof Serializable)  ) return new Member[0];
+        Member[] members = getMapMembers();
+        int firstIdx = getNextBackupIndex();
+        int nextIdx = firstIdx;
+        Member[] backup = new Member[0];
+        
+        //there are no backups
+        if ( members.length == 0 || firstIdx == -1 ) return backup;
+        
+        boolean success = false;
+        do {
+            //select a backup node
+            Member next = members[firstIdx];
+            
+            //increment for the next round of back up selection
+            nextIdx = firstIdx + 1;
+            if ( nextIdx >= members.length ) nextIdx = 0;
+            
+            if (next == null) {
+                continue;
+            }
+            MapMessage msg = null;
+            try {
+                backup = wrap(next);
+                //publish the backup data to one node
+                msg = new MapMessage(getMapContextName(), MapMessage.MSG_BACKUP, false,
+                                     (Serializable) key, (Serializable) value, null, backup);
+                if ( log.isTraceEnabled() ) 
+                    log.trace("Publishing backup data:"+msg+" to: "+next.getName());
+                UniqueId id = getChannel().send(backup, msg, getChannelSendOptions());
+                if ( log.isTraceEnabled() )
+                    log.trace("Data published:"+msg+" msg Id:"+id);
+                //we published out to a backup, mark the test success
+                success = true;
+            }catch ( ChannelException x ) {
+                log.error("Unable to replicate backup key:"+key+" to backup:"+next+". Reason:"+x.getMessage(),x);
+            }
+            try {
+                //publish the data out to all nodes
+                Member[] proxies = excludeFromSet(backup, getMapMembers());
+                if (success && proxies.length > 0 ) {
+                    msg = new MapMessage(getMapContextName(), MapMessage.MSG_PROXY, false,
+                                         (Serializable) key, null, null, backup);
+                    if ( log.isTraceEnabled() ) 
+                    log.trace("Publishing proxy data:"+msg+" to: "+Arrays.toNameString(proxies));
+                    getChannel().send(proxies, msg, getChannelSendOptions());
+                }
+            }catch  ( ChannelException x ) {
+                //log the error, but proceed, this should only happen if a node went down,
+                //and if the node went down, then it can't receive the message, the others
+                //should still get it.
+                log.error("Unable to replicate proxy key:"+key+" to backup:"+next+". Reason:"+x.getMessage(),x);
+            }
+        } while ( !success && (firstIdx!=nextIdx));
+        return backup;
+    }
+    
+    
+
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java
new file mode 100644 (file)
index 0000000..01bb3ba
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.tipis;
+
+import java.io.Serializable;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.group.RpcCallback;
+
+/**
+ * All-to-all replication for a hash map implementation. Each node in the cluster will carry an identical 
+ * copy of the map.<br><br>
+ * This map implementation doesn't have a background thread running to replicate changes.
+ * If you do have changes without invoking put/remove then you need to invoke one of the following methods:
+ * <ul>
+ * <li><code>replicate(Object,boolean)</code> - replicates only the object that belongs to the key</li>
+ * <li><code>replicate(boolean)</code> - Scans the entire map for changes and replicates data</li>
+ *  </ul>
+ * the <code>boolean</code> value in the <code>replicate</code> method used to decide
+ * whether to only replicate objects that implement the <code>ReplicatedMapEntry</code> interface
+ * or to replicate all objects. If an object doesn't implement the <code>ReplicatedMapEntry</code> interface
+ * each time the object gets replicated the entire object gets serialized, hence a call to <code>replicate(true)</code>
+ * will replicate all objects in this map that are using this node as primary.
+ *
+ * <br><br><b>REMBER TO CALL <code>breakdown()</code> or <code>finalize()</code> when you are done with the map to
+ * avoid memory leaks.<br><br>
+ * @todo implement periodic sync/transfer thread
+ * @author Filip Hanik
+ * @version 1.0
+ * 
+ * @todo memberDisappeared, should do nothing except change map membership
+ *       by default it relocates the primary objects
+ */
+public class ReplicatedMap extends AbstractReplicatedMap implements RpcCallback, ChannelListener, MembershipListener {
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReplicatedMap.class);
+
+//------------------------------------------------------------------------------
+//              CONSTRUCTORS / DESTRUCTORS
+//------------------------------------------------------------------------------
+    /**
+     * Creates a new map
+     * @param channel The channel to use for communication
+     * @param timeout long - timeout for RPC messags
+     * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+     * @param initialCapacity int - the size of this map, see HashMap
+     * @param loadFactor float - load factor, see HashMap
+     */
+    public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity,float loadFactor, ClassLoader[] cls) {
+        super(owner,channel, timeout, mapContextName, initialCapacity, loadFactor, Channel.SEND_OPTIONS_DEFAULT, cls);
+    }
+
+    /**
+     * Creates a new map
+     * @param channel The channel to use for communication
+     * @param timeout long - timeout for RPC messags
+     * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+     * @param initialCapacity int - the size of this map, see HashMap
+     */
+    public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) {
+        super(owner,channel, timeout, mapContextName, initialCapacity, AbstractReplicatedMap.DEFAULT_LOAD_FACTOR,Channel.SEND_OPTIONS_DEFAULT, cls);
+    }
+
+    /**
+     * Creates a new map
+     * @param channel The channel to use for communication
+     * @param timeout long - timeout for RPC messags
+     * @param mapContextName String - unique name for this map, to allow multiple maps per channel
+     */
+    public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) {
+        super(owner, channel, timeout, mapContextName,AbstractReplicatedMap.DEFAULT_INITIAL_CAPACITY, AbstractReplicatedMap.DEFAULT_LOAD_FACTOR, Channel.SEND_OPTIONS_DEFAULT, cls);
+    }
+
+//------------------------------------------------------------------------------
+//              METHODS TO OVERRIDE
+//------------------------------------------------------------------------------
+    /**
+     * publish info about a map pair (key/value) to other nodes in the cluster
+     * @param key Object
+     * @param value Object
+     * @return Member - the backup node
+     * @throws ChannelException
+     */
+    protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException {
+        if  (! (key instanceof Serializable && value instanceof Serializable)  ) return new Member[0];
+        //select a backup node
+        Member[] backup = getMapMembers();
+
+        if (backup == null || backup.length == 0) return null;
+
+        //publish the data out to all nodes
+        MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_BACKUP, false,
+                                        (Serializable) key, null, null, backup);
+
+        getChannel().send(getMapMembers(), msg, getChannelSendOptions());
+
+        return backup;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java
new file mode 100644 (file)
index 0000000..bc5b404
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.tipis;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * 
+ * For smarter replication, an object can implement this interface to replicate diffs<br>
+ * The replication logic will call the methods in the following order:<br>
+ * <code>
+ * 1. if ( entry.isDirty() ) <br>
+ *      try {
+ * 2.     entry.lock();<br>
+ * 3.     byte[] diff = entry.getDiff();<br>
+ * 4.     entry.reset();<br>
+ *      } finally {<br>
+ * 5.     entry.unlock();<br>
+ *      }<br>
+ *    }<br>
+ * </code>
+ * <br>
+ * <br>
+ * When the data is deserialized the logic is called in the following order<br>
+ * <code>
+ * 1. ReplicatedMapEntry entry = (ReplicatedMapEntry)objectIn.readObject();<br>
+ * 2. if ( isBackup(entry)||isPrimary(entry) ) entry.setOwner(owner); <br>
+ * </code>
+ * <br>
+ * 
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public interface ReplicatedMapEntry extends Serializable {
+    
+    /**
+     * Has the object changed since last replication
+     * and is not in a locked state
+     * @return boolean
+     */
+    public boolean isDirty();
+    
+    /**
+     * If this returns true, the map will extract the diff using getDiff()
+     * Otherwise it will serialize the entire object.
+     * @return boolean
+     */
+    public boolean isDiffable();
+    
+    /**
+     * Returns a diff and sets the dirty map to false
+     * @return byte[]
+     * @throws IOException
+     */
+    public byte[] getDiff() throws IOException;
+    
+    
+    /**
+     * Applies a diff to an existing object.
+     * @param diff byte[]
+     * @param offset int
+     * @param length int
+     * @throws IOException
+     */
+    public void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException;
+    
+    /**
+     * Resets the current diff state and resets the dirty flag
+     */
+    public void resetDiff();
+    
+    /**
+     * Lock during serialization
+     */
+    public void lock();
+    
+    /**
+     * Unlock after serialization
+     */
+    public void unlock();
+    
+    /**
+     * This method is called after the object has been 
+     * created on a remote map. On this method,
+     * the object can initialize itself for any data that wasn't 
+     * 
+     * @param owner Object
+     */
+    public void setOwner(Object owner);
+    
+    /**
+     * For accuracy checking, a serialized attribute can contain a version number
+     * This number increases as modifications are made to the data.
+     * The replicated map can use this to ensure accuracy on a periodic basis
+     * @return long - the version number or -1 if the data is not versioned
+     */
+    public long getVersion();
+    
+    /**
+     * Forces a certain version to a replicated map entry<br>
+     * @param version long
+     */
+    public void setVersion(long version);
+    
+    
+    
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/tipis/Streamable.java b/java/org/apache/catalina/tribes/tipis/Streamable.java
new file mode 100644 (file)
index 0000000..e1e7c9b
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.tipis;
+
+import java.io.IOException;
+
+/**
+ * Example usage:
+ * <code><pre>
+ * byte[] data = new byte[1024];
+ * Streamable st = ....;
+ * while ( !st.eof() ) {
+ * &nbsp;&nbsp;int length = st.read(data,0,data.length);
+ * &nbsp;&nbsp;String s = new String(data,0,length);
+ * &nbsp;&nbsp;System.out.println(s);
+ * }
+ * </pre></code>
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public interface Streamable {
+    
+    /**
+     * returns true if the stream has reached its end
+     * @return boolean
+     */
+    public boolean eof();
+   
+    /**
+     * write data into the byte array starting at offset, maximum bytes read are (data.length-offset)
+     * @param data byte[] - the array to read data into
+     * @param offset int - start position for writing data
+     * @return int - the number of bytes written into the data buffer
+     */
+    public int write(byte[] data, int offset, int length) throws IOException;
+    
+    /**
+     * read data into the byte array starting at offset
+     * @param data byte[] - the array to read data into
+     * @param offset int - start position for writing data
+     * @param length - the desired read length
+     * @return int - the number of bytes read from the data buffer
+     */
+    public int read(byte[] data, int offset, int length) throws IOException;
+
+   
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/AbstractSender.java b/java/org/apache/catalina/tribes/transport/AbstractSender.java
new file mode 100644 (file)
index 0000000..5ffb7ca
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.catalina.tribes.transport;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.apache.catalina.tribes.Member;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public abstract class AbstractSender implements DataSender {
+    
+    private boolean connected = false;
+    private int rxBufSize = 25188;
+    private int txBufSize = 43800;
+    private boolean directBuffer = false;
+    private int keepAliveCount = -1;
+    private int requestCount = 0;
+    private long connectTime;
+    private long keepAliveTime = -1;
+    private long timeout = 3000;
+    private Member destination;
+    private InetAddress address;
+    private int port;
+    private int maxRetryAttempts = 1;//1 resends
+    private int attempt;
+    private boolean tcpNoDelay = true;
+    private boolean soKeepAlive = false;
+    private boolean ooBInline = true;
+    private boolean soReuseAddress = true;
+    private boolean soLingerOn = false;
+    private int soLingerTime = 3;
+    private int soTrafficClass = 0x04 | 0x08 | 0x010;
+    private boolean throwOnFailedAck = false;
+    
+    /**
+     * transfers sender properties from one sender to another
+     * @param from AbstractSender
+     * @param to AbstractSender
+     */
+    public static void transferProperties(AbstractSender from, AbstractSender to) {
+        to.rxBufSize = from.rxBufSize;
+        to.txBufSize = from.txBufSize;
+        to.directBuffer = from.directBuffer;
+        to.keepAliveCount = from.keepAliveCount;
+        to.keepAliveTime = from.keepAliveTime;
+        to.timeout = from.timeout;
+        to.destination = from.destination;
+        to.address = from.address;
+        to.port = from.port;
+        to.maxRetryAttempts = from.maxRetryAttempts;
+        to.tcpNoDelay = from.tcpNoDelay;
+        to.soKeepAlive = from.soKeepAlive;
+        to.ooBInline = from.ooBInline;
+        to.soReuseAddress = from.soReuseAddress;
+        to.soLingerOn = from.soLingerOn;
+        to.soLingerTime = from.soLingerTime;
+        to.soTrafficClass = from.soTrafficClass;
+        to.throwOnFailedAck = from.throwOnFailedAck;
+    }   
+
+    
+    public AbstractSender() {
+        
+    }
+    
+    /**
+     * connect
+     *
+     * @throws IOException
+     * @todo Implement this org.apache.catalina.tribes.transport.DataSender method
+     */
+    public abstract void connect() throws IOException;
+
+    /**
+     * disconnect
+     *
+     * @todo Implement this org.apache.catalina.tribes.transport.DataSender method
+     */
+    public abstract void disconnect();
+
+    /**
+     * keepalive
+     *
+     * @return boolean
+     * @todo Implement this org.apache.catalina.tribes.transport.DataSender method
+     */
+    public boolean keepalive() {
+        boolean disconnect = false;
+        if ( keepAliveCount >= 0 && requestCount>keepAliveCount ) disconnect = true;
+        else if ( keepAliveTime >= 0 && keepAliveTime> (System.currentTimeMillis()-connectTime) ) disconnect = true;
+        if ( disconnect ) disconnect();
+        return disconnect;
+    }
+    
+    protected void setConnected(boolean connected){
+        this.connected = connected;
+    }
+    
+    public boolean isConnected() {
+        return connected;
+    }
+
+    public long getConnectTime() {
+        return connectTime;
+    }
+
+    public Member getDestination() {
+        return destination;
+    }
+
+
+    public int getKeepAliveCount() {
+        return keepAliveCount;
+    }
+
+    public long getKeepAliveTime() {
+        return keepAliveTime;
+    }
+
+    public int getRequestCount() {
+        return requestCount;
+    }
+
+    public int getRxBufSize() {
+        return rxBufSize;
+    }
+
+    public long getTimeout() {
+        return timeout;
+    }
+
+    public int getTxBufSize() {
+        return txBufSize;
+    }
+
+    public InetAddress getAddress() {
+        return address;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public int getMaxRetryAttempts() {
+        return maxRetryAttempts;
+    }
+
+    public void setDirectBuffer(boolean directBuffer) {
+        this.directBuffer = directBuffer;
+    }
+
+    public boolean getDirectBuffer() {
+        return this.directBuffer;
+    }
+
+    public int getAttempt() {
+        return attempt;
+    }
+
+    public boolean getTcpNoDelay() {
+        return tcpNoDelay;
+    }
+
+    public boolean getSoKeepAlive() {
+        return soKeepAlive;
+    }
+
+    public boolean getOoBInline() {
+        return ooBInline;
+    }
+
+    public boolean getSoReuseAddress() {
+        return soReuseAddress;
+    }
+
+    public boolean getSoLingerOn() {
+        return soLingerOn;
+    }
+
+    public int getSoLingerTime() {
+        return soLingerTime;
+    }
+
+    public int getSoTrafficClass() {
+        return soTrafficClass;
+    }
+
+    public boolean getThrowOnFailedAck() {
+        return throwOnFailedAck;
+    }
+
+    public void setKeepAliveCount(int keepAliveCount) {
+        this.keepAliveCount = keepAliveCount;
+    }
+
+    public void setKeepAliveTime(long keepAliveTime) {
+        this.keepAliveTime = keepAliveTime;
+    }
+
+    public void setRequestCount(int requestCount) {
+        this.requestCount = requestCount;
+    }
+
+    public void setRxBufSize(int rxBufSize) {
+        this.rxBufSize = rxBufSize;
+    }
+
+    public void setTimeout(long timeout) {
+        this.timeout = timeout;
+    }
+
+    public void setTxBufSize(int txBufSize) {
+        this.txBufSize = txBufSize;
+    }
+
+    public void setConnectTime(long connectTime) {
+        this.connectTime = connectTime;
+    }
+
+    public void setMaxRetryAttempts(int maxRetryAttempts) {
+        this.maxRetryAttempts = maxRetryAttempts;
+    }
+
+    public void setAttempt(int attempt) {
+        this.attempt = attempt;
+    }
+
+    public void setTcpNoDelay(boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+    }
+
+    public void setSoKeepAlive(boolean soKeepAlive) {
+        this.soKeepAlive = soKeepAlive;
+    }
+
+    public void setOoBInline(boolean ooBInline) {
+        this.ooBInline = ooBInline;
+    }
+
+    public void setSoReuseAddress(boolean soReuseAddress) {
+        this.soReuseAddress = soReuseAddress;
+    }
+
+    public void setSoLingerOn(boolean soLingerOn) {
+        this.soLingerOn = soLingerOn;
+    }
+
+    public void setSoLingerTime(int soLingerTime) {
+        this.soLingerTime = soLingerTime;
+    }
+
+    public void setSoTrafficClass(int soTrafficClass) {
+        this.soTrafficClass = soTrafficClass;
+    }
+
+    public void setThrowOnFailedAck(boolean throwOnFailedAck) {
+        this.throwOnFailedAck = throwOnFailedAck;
+    }
+
+    public void setDestination(Member destination) throws UnknownHostException {
+        this.destination = destination;
+        this.address = InetAddress.getByAddress(destination.getHost());
+        this.port = destination.getPort();
+
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public void setAddress(InetAddress address) {
+        this.address = address;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/Constants.java b/java/org/apache/catalina/tribes/transport/Constants.java
new file mode 100644 (file)
index 0000000..c738adb
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.io.XByteBuffer;
+
+/**
+ * Manifest constants for the <code>org.apache.catalina.tribes.transport</code>
+ * package.
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 303753 $ $Date: 2005-03-14 15:24:30 -0600 (Mon, 14 Mar 2005) $
+ */
+
+public class Constants {
+
+    public static final String Package = "org.apache.catalina.tribes.transport";
+    
+    /*
+     * Do not change any of these values!
+     */
+    public static final byte[] ACK_DATA = new byte[] {6, 2, 3};
+    public static final byte[] FAIL_ACK_DATA = new byte[] {11, 0, 5};
+    public static final byte[] ACK_COMMAND = XByteBuffer.createDataPackage(ACK_DATA);
+    public static final byte[] FAIL_ACK_COMMAND = XByteBuffer.createDataPackage(FAIL_ACK_DATA);
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/DataSender.java b/java/org/apache/catalina/tribes/transport/DataSender.java
new file mode 100644 (file)
index 0000000..51cd3fc
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.transport;
+
+import java.io.IOException;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public interface DataSender {
+    public void connect() throws IOException;
+    public void disconnect();
+    public boolean isConnected();
+    public void setRxBufSize(int size);
+    public void setTxBufSize(int size);
+    public boolean keepalive();
+    public void setTimeout(long timeout);
+    public void setKeepAliveCount(int maxRequests);
+    public void setKeepAliveTime(long keepAliveTimeInMs);
+    public int getRequestCount();
+    public long getConnectTime();
+    
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings.properties b/java/org/apache/catalina/tribes/transport/LocalStrings.properties
new file mode 100644 (file)
index 0000000..ad54ca0
--- /dev/null
@@ -0,0 +1,69 @@
+AsyncSocketSender.create.thread=Create sender [{0}:{1,number,integer}] queue thread to tcp background replication
+AsyncSocketSender.queue.message=Queue message to [{0}:{1,number,integer}] id=[{2}] size={3}
+AsyncSocketSender.send.error=Unable to asynchronously send session with id=[{0}] - message will be ignored.
+AsyncSocketSender.queue.empty=Queue in sender [{0}:{1,number,integer}] returned null element!
+cluster.mbean.register.already=MBean {0} already registered!
+IDataSender.ack.eof=EOF reached at local port [{0}:{1,number,integer}]
+IDataSender.ack.receive=Got ACK at local port [{0}:{1,number,integer}]
+IDataSender.ack.missing=Unable to read acknowledgement from [{0}:{1,number,integer}] in {2,number,integer} ms. Disconnecting socket, and trying again.
+IDataSender.ack.read=Read wait ack char '{2}' [{0}:{1,number,integer}]
+IDataSender.ack.start=Waiting for ACK message [{0}:{1,number,integer}]
+IDataSender.ack.wrong=Missing correct ACK after 10 bytes read at local port [{0}:{1,number,integer}]
+IDataSender.closeSocket=Sender close socket to [{0}:{1,number,integer}] (close count {2,number,integer})
+IDataSender.connect=Sender connect to [{0}:{1,number,integer}] (connect count {2,number,integer})
+IDataSender.create=Create sender [{0}:{1,number,integer}]
+IDataSender.disconnect=Sender disconnect from [{0}:{1,number,integer}] (disconnect count {2,number,integer})
+IDataSender.message.disconnect=Message transfered: Sender can't disconnect from [{0}:{1,number,integer}]
+IDataSender.message.create=Message transfered: Sender can't create current socket [{0}:{1,number,integer}]
+IDataSender.openSocket=Sender open socket to [{0}:{1,number,integer}] (open count {2,number,integer})
+IDataSender.openSocket.failure=Open sender socket [{0}:{1,number,integer}] failure! (open failure count {2,number,integer})
+IDataSender.send.again=Send data again to [{0}:{1,number,integer}]
+IDataSender.send.crash=Send message crashed [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.send.message=Send message to [{0}:{1,number,integer}] id=[{2}] size={3,number,integer}
+IDataSender.send.lost=Message lost: [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.senderModes.Configured=Configured a data replication sender for mode {0}
+IDataSender.senderModes.Instantiate=Can't instantiate a data replication sender of class {0}
+IDataSender.senderModes.Missing=Can't configure a data replication sender for mode {0}
+IDataSender.senderModes.Resources=Can't load data replication sender mapping list
+IDataSender.stats=Send stats from [{0}:{1,number,integer}], Nr of bytes sent={2,number,integer} over {3} = {4,number,integer} bytes/request, processing time {5,number,integer} msec, avg processing time {6,number,integer} msec
+PoolSocketSender.senderQueue.sender.failed=PoolSocketSender create new sender to [{0}:{1,number,integer}] failed
+PoolSocketSender.noMoreSender=No socket sender available for client [{0}:{1,number,integer}] did it disappeared?
+ReplicationTransmitter.getProperty=get property {0}
+ReplicationTransmitter.setProperty=set property {0}: {1} old value {2}
+ReplicationTransmitter.started=Start ClusterSender at cluster {0} with name {1}
+ReplicationTransmitter.stopped=Stopped ClusterSender at cluster {0} with name {1}
+ReplicationValve.crossContext.add=add Cross Context session replication container to replicationValve threadlocal
+ReplicationValve.crossContext.registerSession=register Cross context session id={0} from context {1}
+ReplicationValve.crossContext.remove=remove Cross Context session replication container from replicationValve threadlocal
+ReplicationValve.crossContext.sendDelta=send Cross Context session delta from context {0}.
+ReplicationValve.filter.loading=Loading request filters={0}
+ReplicationValve.filter.token=Request filter={0}
+ReplicationValve.filter.token.failure=Unable to compile filter={0}
+ReplicationValve.invoke.uri=Invoking replication request on {0}
+ReplicationValve.nocluster=No cluster configured for this request.
+ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context {0}
+ReplicationValve.send.failure=Unable to perform replication request.
+ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster.
+ReplicationValve.session.found=Context {0}: Found session {1} but it isn't a ClusterSession.
+ReplicationValve.session.indicator=Context {0}: Primarity of session {0} in request attribute {1} is {2}.
+ReplicationValve.session.invalid=Context {0}: Requested session {1} is invalid, removed or not replicated at this node.
+ReplicationValve.stats=Average request time= {0} ms for Cluster overhead time={1} ms for {2} requests {3} filter requests {4} send requests {5} cross context requests (Request={6} ms Cluster={7} ms).
+SimpleTcpCluster.event.log=Cluster receive listener event {0} with data {1}
+SimpleTcpCluster.getProperty=get property {0}
+SimpleTcpCluster.setProperty=set property {0}: {1} old value {2}
+SimpleTcpCluster.default.addClusterListener=Add Default ClusterListener at cluster {0}
+SimpleTcpCluster.default.addClusterValves=Add Default ClusterValves at cluster {0}
+SimpleTcpCluster.default.addClusterReceiver=Add Default ClusterReceiver at cluster {0}
+SimpleTcpCluster.default.addClusterSender=Add Default ClusterSender at cluster {0}
+SimpleTcpCluster.default.addMembershipService=Add Default Membership Service at cluster {0}
+SimpleTcpCluster.log.receive=RECEIVE {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} {5}
+SimpleTcpCluster.log.send=SEND {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} 
+SimpleTcpCluster.log.send.all=SEND {0,date}:{0,time} {1,number} - {2}
+SocketReplictionListener.allreadyExists=ServerSocket [{0}:{1}] allready started!
+SocketReplictionListener.accept.failure=ServerSocket [{0}:{1}] - Exception to start thread or accept server socket
+SocketReplictionListener.open=Open Socket at [{0}:{1}]
+SocketReplictionListener.openclose.failure=ServerSocket [{0}:{1}] - Exception to open or close server socket
+SocketReplictionListener.portbusy=Port busy at [{0}:{i}] - reason [{2}]
+SocketReplictionListener.serverSocket.notExists=Fatal error: Receiver socket not bound address={0} port={1} maxport={2}
+SocketReplictionListener.timeout=Receiver ServerSocket no started [{0}:{1}] - reason: timeout={2} or listen={3}
+SocketReplictionListener.unlockSocket.failure=UnLocksocket failure at ServerSocket [{0:{1}]
diff --git a/java/org/apache/catalina/tribes/transport/MultiPointSender.java b/java/org/apache/catalina/tribes/transport/MultiPointSender.java
new file mode 100644 (file)
index 0000000..51722da
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.Member;
+
+/**
+ * @author Filip Hanik
+ * @version $Revision: 303993 $ $Date: 2005-07-16 16:05:54 -0500 (Sat, 16 Jul 2005) $
+ * @since 5.5.16
+ */
+
+public interface MultiPointSender extends DataSender
+{
+    public void sendMessage(Member[] destination, ChannelMessage data) throws ChannelException;
+    public void setRxBufSize(int size);
+    public void setTxBufSize(int size);
+    public void setMaxRetryAttempts(int attempts);
+    public void setDirectBuffer(boolean directBuf);
+    public void memberAdded(Member member);
+    public void memberDisappeared(Member member);
+}
diff --git a/java/org/apache/catalina/tribes/transport/PooledSender.java b/java/org/apache/catalina/tribes/transport/PooledSender.java
new file mode 100644 (file)
index 0000000..221a923
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.transport;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public abstract class PooledSender extends AbstractSender implements MultiPointSender {
+    
+    private SenderQueue queue = null;
+    private int poolSize = 25;
+    public PooledSender() {
+        queue = new SenderQueue(this,poolSize);
+    }
+    
+    public abstract DataSender getNewDataSender();
+    
+    public DataSender getSender() {
+        return queue.getSender(getTimeout());
+    }
+    
+    public void returnSender(DataSender sender) {
+        sender.keepalive();
+        queue.returnSender(sender);
+    }
+    
+    public synchronized void connect() throws IOException {
+        //do nothing, happens in the socket sender itself
+        queue.open();
+        setConnected(true);
+    }
+    
+    public synchronized void disconnect() {
+        queue.close();
+        setConnected(false);
+    }
+    
+    
+    public int getInPoolSize() {
+        return queue.getInPoolSize();
+    }
+
+    public int getInUsePoolSize() {
+        return queue.getInUsePoolSize();
+    }
+
+
+    public void setPoolSize(int poolSize) {
+        this.poolSize = poolSize;
+        queue.setLimit(poolSize);
+    }
+
+    public int getPoolSize() {
+        return poolSize;
+    }
+
+    public boolean keepalive() {
+        //do nothing, the pool checks on every return
+        return false;
+    }
+
+    
+
+    //  ----------------------------------------------------- Inner Class
+
+    private class SenderQueue {
+        private int limit = 25;
+
+        PooledSender parent = null;
+
+        private List notinuse = null;
+
+        private List inuse = null;
+
+        private boolean isOpen = true;
+
+        public SenderQueue(PooledSender parent, int limit) {
+            this.limit = limit;
+            this.parent = parent;
+            notinuse = new java.util.LinkedList();
+            inuse = new java.util.LinkedList();
+        }
+
+        /**
+         * @return Returns the limit.
+         */
+        public int getLimit() {
+            return limit;
+        }
+        /**
+         * @param limit The limit to set.
+         */
+        public void setLimit(int limit) {
+            this.limit = limit;
+        }
+        /**
+         * @return
+         */
+        public int getInUsePoolSize() {
+            return inuse.size();
+        }
+
+        /**
+         * @return
+         */
+        public int getInPoolSize() {
+            return notinuse.size();
+        }
+
+        public synchronized DataSender getSender(long timeout) {
+            long start = System.currentTimeMillis();
+            while ( true ) {
+                if (!isOpen)throw new IllegalStateException("Queue is closed");
+                DataSender sender = null;
+                if (notinuse.size() == 0 && inuse.size() < limit) {
+                    sender = parent.getNewDataSender();
+                } else if (notinuse.size() > 0) {
+                    sender = (DataSender) notinuse.remove(0);
+                }
+                if (sender != null) {
+                    inuse.add(sender);
+                    return sender;
+                }//end if
+                long delta = System.currentTimeMillis() - start;
+                if ( delta > timeout && timeout>0) return null;
+                else {
+                    try {
+                        wait(Math.max(timeout - delta,1));
+                    }catch (InterruptedException x){}
+                }//end if
+            }
+        }
+
+        public synchronized void returnSender(DataSender sender) {
+            if ( !isOpen) {
+                sender.disconnect();
+                return;
+            }
+            //to do
+            inuse.remove(sender);
+            //just in case the limit has changed
+            if ( notinuse.size() < this.getLimit() ) notinuse.add(sender);
+            else try {sender.disconnect(); } catch ( Exception ignore){}
+            notify();
+        }
+
+        public synchronized void close() {
+            isOpen = false;
+            Object[] unused = notinuse.toArray();
+            Object[] used = inuse.toArray();
+            for (int i = 0; i < unused.length; i++) {
+                DataSender sender = (DataSender) unused[i];
+                sender.disconnect();
+            }//for
+            for (int i = 0; i < used.length; i++) {
+                DataSender sender = (DataSender) used[i];
+                sender.disconnect();
+            }//for
+            notinuse.clear();
+            inuse.clear();
+            notify();
+            
+
+
+        }
+
+        public synchronized void open() {
+            isOpen = true;
+            notify();
+        }
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java b/java/org/apache/catalina/tribes/transport/ReceiverBase.java
new file mode 100644 (file)
index 0000000..f09b956
--- /dev/null
@@ -0,0 +1,383 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.catalina.tribes.transport;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ChannelReceiver;
+import org.apache.catalina.tribes.MessageListener;
+import org.apache.catalina.tribes.io.ListenCallback;
+import org.apache.commons.logging.Log;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public abstract class ReceiverBase implements ChannelReceiver, ListenCallback, ThreadPool.ThreadCreator {
+
+    public static final int OPTION_DIRECT_BUFFER = 0x0004;
+
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReceiverBase.class);
+    
+    private MessageListener listener;
+    private String host = "auto";
+    private InetAddress bind;
+    private int port  = 4000;
+    private int rxBufSize = 43800;
+    private int txBufSize = 25188;
+    private boolean listen = false;
+    private ThreadPool pool;
+    private boolean direct = true;
+    private long tcpSelectorTimeout = 5000;
+    //how many times to search for an available socket
+    private int autoBind = 10;
+    private int maxThreads = 15;
+    private int minThreads = 6;
+    private boolean tcpNoDelay = true;
+    private boolean soKeepAlive = false;
+    private boolean ooBInline = true;
+    private boolean soReuseAddress = true;
+    private boolean soLingerOn = true;
+    private int soLingerTime = 3;
+    private int soTrafficClass = 0x04 | 0x08 | 0x010;
+    private int timeout = 3000; //3 seconds
+    private boolean useBufferPool = true;
+
+
+    public ReceiverBase() {
+    }
+    
+    /**
+     * getMessageListener
+     *
+     * @return MessageListener
+     * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method
+     */
+    public MessageListener getMessageListener() {
+        return listener;
+    }
+
+    /**
+     *
+     * @return The port
+     * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method
+     */
+    public int getPort() {
+        return port;
+    }
+
+    public int getRxBufSize() {
+        return rxBufSize;
+    }
+
+    public int getTxBufSize() {
+        return txBufSize;
+    }
+    
+    /**
+     * @deprecated use getMinThreads()/getMaxThreads()
+     * @return int
+     */
+    public int getTcpThreadCount() {
+        return getMinThreads();
+    }
+
+    /**
+     * setMessageListener
+     *
+     * @param listener MessageListener
+     * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method
+     */
+    public void setMessageListener(MessageListener listener) {
+        this.listener = listener;
+    }
+
+    public void setTcpListenPort(int tcpListenPort) {
+        this.port = tcpListenPort;
+    }
+
+    public void setTcpListenAddress(String tcpListenHost) {
+        this.host = tcpListenHost;
+    }
+
+    public void setRxBufSize(int rxBufSize) {
+        this.rxBufSize = rxBufSize;
+    }
+
+    public void setTxBufSize(int txBufSize) {
+        this.txBufSize = txBufSize;
+    }
+
+    public void setTcpThreadCount(int tcpThreadCount) {
+        setMinThreads(tcpThreadCount);
+    }
+
+    /**
+     * @return Returns the bind.
+     */
+    public InetAddress getBind() {
+        if (bind == null) {
+            try {
+                if ("auto".equals(host)) {
+                    host = java.net.InetAddress.getLocalHost().getHostAddress();
+                }
+                if (log.isDebugEnabled())
+                    log.debug("Starting replication listener on address:"+ host);
+                bind = java.net.InetAddress.getByName(host);
+            } catch (IOException ioe) {
+                log.error("Failed bind replication listener on address:"+ host, ioe);
+            }
+        }
+        return bind;
+    }
+    
+    /**
+     * recursive bind to find the next available port
+     * @param socket ServerSocket
+     * @param portstart int
+     * @param retries int
+     * @return int
+     * @throws IOException
+     */
+    protected int bind(ServerSocket socket, int portstart, int retries) throws IOException {
+        InetSocketAddress addr = null;
+        while ( retries > 0 ) {
+            try {
+                addr = new InetSocketAddress(getBind(), portstart);
+                socket.bind(addr);
+                setTcpListenPort(portstart);
+                log.info("Receiver Server Socket bound to:"+addr);
+                return 0;
+            }catch ( IOException x) {
+                retries--;
+                if ( retries <= 0 ) {
+                    log.info("Unable to bind server socket to:"+addr+" throwing error.");
+                    throw x;
+                }
+                portstart++;
+                try {Thread.sleep(25);}catch( InterruptedException ti){Thread.currentThread().interrupted();}
+                retries = bind(socket,portstart,retries);
+            }
+        }
+        return retries;
+    }
+    
+    public void messageDataReceived(ChannelMessage data) {
+        if ( this.listener != null ) {
+            if ( listener.accept(data) ) listener.messageReceived(data);
+        }
+    }
+    
+    public int getWorkerThreadOptions() {
+        int options = 0;
+        if ( getDirect() ) options = options | OPTION_DIRECT_BUFFER;
+        return options;
+    }
+
+
+    /**
+     * @param bind The bind to set.
+     */
+    public void setBind(java.net.InetAddress bind) {
+        this.bind = bind;
+    }
+
+
+    public int getTcpListenPort() {
+        return this.port;
+    }
+
+    public boolean getDirect() {
+        return direct;
+    }
+
+
+
+    public void setDirect(boolean direct) {
+        this.direct = direct;
+    }
+
+
+
+    public String getHost() {
+        getBind();
+        return this.host;
+    }
+
+    public long getTcpSelectorTimeout() {
+        return tcpSelectorTimeout;
+    }
+
+    public boolean doListen() {
+        return listen;
+    }
+
+    public MessageListener getListener() {
+        return listener;
+    }
+
+    public ThreadPool getPool() {
+        return pool;
+    }
+
+    public String getTcpListenAddress() {
+        return getHost();
+    }
+
+    public int getAutoBind() {
+        return autoBind;
+    }
+
+    public int getMaxThreads() {
+        return maxThreads;
+    }
+
+    public int getMinThreads() {
+        return minThreads;
+    }
+
+    public boolean getTcpNoDelay() {
+        return tcpNoDelay;
+    }
+
+    public boolean getSoKeepAlive() {
+        return soKeepAlive;
+    }
+
+    public boolean getOoBInline() {
+        return ooBInline;
+    }
+
+
+    public boolean getSoLingerOn() {
+        return soLingerOn;
+    }
+
+    public int getSoLingerTime() {
+        return soLingerTime;
+    }
+
+    public boolean getSoReuseAddress() {
+        return soReuseAddress;
+    }
+
+    public int getSoTrafficClass() {
+        return soTrafficClass;
+    }
+
+    public int getTimeout() {
+        return timeout;
+    }
+
+    public boolean getUseBufferPool() {
+        return useBufferPool;
+    }
+
+    public void setTcpSelectorTimeout(long selTimeout) {
+        tcpSelectorTimeout = selTimeout;
+    }
+
+    public void setListen(boolean doListen) {
+        this.listen = doListen;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public void setListener(MessageListener listener) {
+        this.listener = listener;
+    }
+
+    public void setLog(Log log) {
+        this.log = log;
+    }
+
+    public void setPool(ThreadPool pool) {
+        this.pool = pool;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public void setAutoBind(int autoBind) {
+        this.autoBind = autoBind;
+        if ( this.autoBind <= 0 ) this.autoBind = 1;
+    }
+
+    public void setMaxThreads(int maxThreads) {
+        this.maxThreads = maxThreads;
+    }
+
+    public void setMinThreads(int minThreads) {
+        this.minThreads = minThreads;
+    }
+
+    public void setTcpNoDelay(boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+    }
+
+    public void setSoKeepAlive(boolean soKeepAlive) {
+        this.soKeepAlive = soKeepAlive;
+    }
+
+    public void setOoBInline(boolean ooBInline) {
+        this.ooBInline = ooBInline;
+    }
+
+
+    public void setSoLingerOn(boolean soLingerOn) {
+        this.soLingerOn = soLingerOn;
+    }
+
+    public void setSoLingerTime(int soLingerTime) {
+        this.soLingerTime = soLingerTime;
+    }
+
+    public void setSoReuseAddress(boolean soReuseAddress) {
+        this.soReuseAddress = soReuseAddress;
+    }
+
+    public void setSoTrafficClass(int soTrafficClass) {
+        this.soTrafficClass = soTrafficClass;
+    }
+
+    public void setTimeout(int timeout) {
+        this.timeout = timeout;
+    }
+
+    public void setUseBufferPool(boolean useBufferPool) {
+        this.useBufferPool = useBufferPool;
+    }
+
+    public void heartbeat() {
+        //empty operation
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java
new file mode 100644 (file)
index 0000000..6b3b18a
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ChannelSender;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.util.StringManager;
+import org.apache.catalina.tribes.transport.nio.PooledParallelSender;
+
+/**
+ * Transmit message to other cluster members
+ * Actual senders are created based on the replicationMode
+ * type 
+ * 
+ * @author Filip Hanik
+ * @version $Revision: 379956 $ $Date: 2006-02-22 16:57:35 -0600 (Wed, 22 Feb 2006) $
+ */
+public class ReplicationTransmitter implements ChannelSender {
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReplicationTransmitter.class);
+
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "ReplicationTransmitter/3.0";
+
+    /**
+     * The string manager for this package.
+     */
+    protected StringManager sm = StringManager.getManager(Constants.Package);
+
+    
+
+    public ReplicationTransmitter() {
+    }
+
+    private MultiPointSender transport = new PooledParallelSender();
+
+    /**
+     * Return descriptive information about this implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+        return (info);
+    }
+
+    public MultiPointSender getTransport() {
+        return transport;
+    }
+
+    public void setTransport(MultiPointSender transport) {
+        this.transport = transport;
+    }
+    
+    // ------------------------------------------------------------- public
+    
+    /**
+     * Send data to one member
+     * @see org.apache.catalina.tribes.ClusterSender#sendMessage(org.apache.catalina.tribes.ClusterMessage, org.apache.catalina.tribes.Member)
+     */
+    public void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException {
+        MultiPointSender sender = getTransport();
+        sender.sendMessage(destination,message);
+    }
+    
+    
+    /**
+     * start the sender and register transmitter mbean
+     * 
+     * @see org.apache.catalina.tribes.ClusterSender#start()
+     */
+    public void start() throws java.io.IOException {
+        getTransport().connect();
+    }
+
+    /*
+     * stop the sender and deregister mbeans (transmitter, senders)
+     * 
+     * @see org.apache.catalina.tribes.ClusterSender#stop()
+     */
+    public synchronized void stop() {
+        getTransport().disconnect();
+    }
+
+    /**
+     * Call transmitter to check for sender socket status
+     * 
+     * @see SimpleTcpCluster#backgroundProcess()
+     */
+
+    public void heartbeat() {
+        
+    }
+
+    /**
+     * add new cluster member and create sender ( s. replicationMode) transfer
+     * current properties to sender
+     * 
+     * @see org.apache.catalina.tribes.ClusterSender#add(org.apache.catalina.tribes.Member)
+     */
+    public synchronized void add(Member member) {
+        getTransport().memberAdded(member);
+    }
+
+    /**
+     * remove sender from transmitter. ( deregister mbean and disconnect sender )
+     * 
+     * @see org.apache.catalina.tribes.ClusterSender#remove(org.apache.catalina.tribes.Member)
+     */
+    public synchronized void remove(Member member) {
+        getTransport().memberDisappeared(member);
+    }
+
+    // ------------------------------------------------------------- protected
+
+    
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/SenderState.java b/java/org/apache/catalina/tribes/transport/SenderState.java
new file mode 100644 (file)
index 0000000..be71523
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.Member;
+import java.util.HashMap;
+
+
+/**
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ * @since 5.5.16
+ */
+
+public class SenderState {
+    
+    public static final int READY = 0;
+    public static final int SUSPECT = 1;
+    public static final int FAILING = 2;
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "SenderState/1.0";
+    
+    
+    protected static HashMap memberStates = new HashMap();
+    
+    public static SenderState getSenderState(Member member) {
+        return getSenderState(member,true);
+    }
+
+    public static SenderState getSenderState(Member member, boolean create) {
+        SenderState state = (SenderState)memberStates.get(member);
+        if ( state == null && create) {
+            synchronized ( memberStates ) {
+                state = (SenderState)memberStates.get(member);
+                if ( state == null ) {
+                    state = new SenderState();
+                    memberStates.put(member,state);
+                }
+            }
+        }
+        return state;
+    }
+    
+    public static void removeSenderState(Member member) {
+        synchronized ( memberStates ) {
+            memberStates.remove(member);
+        }
+    }
+    
+
+    // ----------------------------------------------------- Instance Variables
+
+    private int state = READY;
+
+    //  ----------------------------------------------------- Constructor
+
+    
+    private SenderState() {
+        this(READY);
+    }
+
+    private SenderState(int state) {
+        this.state = state;
+    }
+    
+    /**
+     * 
+     * @return boolean
+     */
+    public boolean isSuspect() {
+        return (state == SUSPECT) || (state == FAILING);
+    }
+
+    public void setSuspect() {
+        state = SUSPECT;
+    }
+    
+    public boolean isReady() {
+        return state == READY;
+    }
+    
+    public void setReady() {
+        state = READY;
+    }
+    
+    public boolean isFailing() {
+        return state == FAILING;
+    }
+    
+    public void setFailing() {
+        state = FAILING;
+    }
+    
+
+    //  ----------------------------------------------------- Public Properties
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/ThreadPool.java b/java/org/apache/catalina/tribes/transport/ThreadPool.java
new file mode 100644 (file)
index 0000000..32f3dd3
--- /dev/null
@@ -0,0 +1,164 @@
+/*\r
+ * Copyright 1999,2004 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.apache.catalina.tribes.transport;\r
+import java.util.Iterator;\r
+import java.util.LinkedList;\r
+import java.util.List;\r
+\r
+/**\r
+ * @author not attributable\r
+ * @version 1.0\r
+ */\r
+\r
+public class ThreadPool\r
+{\r
+    /**\r
+     * A very simple thread pool class.  The pool size is set at\r
+     * construction time and remains fixed.  Threads are cycled\r
+     * through a FIFO idle queue.\r
+     */\r
+\r
+    List idle = new LinkedList();\r
+    List used = new LinkedList();\r
+    \r
+    Object mutex = new Object();\r
+    boolean running = true;\r
+    \r
+    private static int counter = 1;\r
+    private int maxThreads;\r
+    private int minThreads;\r
+    \r
+    private ThreadCreator creator = null;\r
+\r
+    private static synchronized int inc() {\r
+        return counter++;\r
+    }\r
+\r
+    \r
+    public ThreadPool (int maxThreads, int minThreads, ThreadCreator creator) throws Exception {\r
+        // fill up the pool with worker threads\r
+        this.maxThreads = maxThreads;\r
+        this.minThreads = minThreads;\r
+        this.creator = creator;\r
+        //for (int i = 0; i < minThreads; i++) {\r
+        for (int i = 0; i < maxThreads; i++) { //temporary fix for thread hand off problem\r
+            WorkerThread thread = creator.getWorkerThread();\r
+            setupThread(thread);\r
+            idle.add (thread);\r
+        }\r
+    }\r
+    \r
+    protected void setupThread(WorkerThread thread) {\r
+        synchronized (thread) {\r
+            thread.setPool(this);\r
+            thread.setName(thread.getClass().getName() + "[" + inc() + "]");\r
+            thread.setDaemon(true);\r
+            thread.setPriority(Thread.MAX_PRIORITY);\r
+            thread.start();\r
+            try {thread.wait(500); }catch ( InterruptedException x ) {}\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Find an idle worker thread, if any.  Could return null.\r
+     */\r
+    public WorkerThread getWorker()\r
+    {\r
+        WorkerThread worker = null;\r
+        synchronized (mutex) {\r
+            while ( worker == null && running ) {\r
+                if (idle.size() > 0) {\r
+                    try {\r
+                        worker = (WorkerThread) idle.remove(0);\r
+                    } catch (java.util.NoSuchElementException x) {\r
+                        //this means that there are no available workers\r
+                        worker = null;\r
+                    }\r
+                } else if ( used.size() < this.maxThreads && creator != null) {\r
+                    worker = creator.getWorkerThread();\r
+                    setupThread(worker);\r
+                } else {\r
+                    try { mutex.wait(); } catch ( java.lang.InterruptedException x ) {Thread.currentThread().interrupted();}\r
+                }\r
+            }//while\r
+            if ( worker != null ) used.add(worker);\r
+        }\r
+        return (worker);\r
+    }\r
+    \r
+    public int available() {\r
+        return idle.size();\r
+    }\r
+\r
+    /**\r
+     * Called by the worker thread to return itself to the\r
+     * idle pool.\r
+     */\r
+    public void returnWorker (WorkerThread worker) {\r
+        if ( running ) {\r
+            synchronized (mutex) {\r
+                used.remove(worker);\r
+                //if ( idle.size() < minThreads && !idle.contains(worker)) idle.add(worker);\r
+                if ( idle.size() < maxThreads && !idle.contains(worker)) idle.add(worker); //let max be the upper limit\r
+                else {\r
+                    worker.setDoRun(false);\r
+                    synchronized (worker){worker.notify();}\r
+                }\r
+                mutex.notify();\r
+            }\r
+        }else {\r
+            worker.setDoRun(false);\r
+            synchronized (worker){worker.notify();}\r
+        }\r
+    }\r
+\r
+    public int getMaxThreads() {\r
+        return maxThreads;\r
+    }\r
+\r
+    public int getMinThreads() {\r
+        return minThreads;\r
+    }\r
+\r
+    public void stop() {\r
+        running = false;\r
+        synchronized (mutex) {\r
+            Iterator i = idle.iterator();\r
+            while ( i.hasNext() ) {\r
+                WorkerThread worker = (WorkerThread)i.next();\r
+                returnWorker(worker);\r
+                i.remove();\r
+            }\r
+        }\r
+    }\r
+\r
+    public void setMaxThreads(int maxThreads) {\r
+        this.maxThreads = maxThreads;\r
+    }\r
+\r
+    public void setMinThreads(int minThreads) {\r
+        this.minThreads = minThreads;\r
+    }\r
+\r
+    public ThreadCreator getThreadCreator() {\r
+        return this.creator;\r
+    }\r
+    \r
+    public static interface ThreadCreator {\r
+        public WorkerThread getWorkerThread();\r
+    }\r
+}\r
diff --git a/java/org/apache/catalina/tribes/transport/WorkerThread.java b/java/org/apache/catalina/tribes/transport/WorkerThread.java
new file mode 100644 (file)
index 0000000..70d9512
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.io.ListenCallback;
+
+
+
+
+/**
+ * @author Filip Hanik
+ * @version $Revision: 366253 $ $Date: 2006-01-05 13:30:42 -0600 (Thu, 05 Jan 2006) $
+ */
+public abstract class WorkerThread extends Thread 
+{
+    
+    public static final int OPTION_DIRECT_BUFFER = ReceiverBase.OPTION_DIRECT_BUFFER;
+    
+    private ListenCallback callback;
+    private ThreadPool pool;
+    private boolean doRun = true;
+    private int options;
+    protected boolean useBufferPool = true;
+
+    public WorkerThread(ListenCallback callback) {
+        this.callback = callback;
+    }
+
+    public void setPool(ThreadPool pool) {
+        this.pool = pool;
+    }
+
+    public void setOptions(int options) {
+        this.options = options;
+    }
+
+    public void setCallback(ListenCallback callback) {
+        this.callback = callback;
+    }
+
+    public void setDoRun(boolean doRun) {
+        this.doRun = doRun;
+    }
+
+    public ThreadPool getPool() {
+        return pool;
+    }
+
+    public int getOptions() {
+        return options;
+    }
+
+    public ListenCallback getCallback() {
+        return callback;
+    }
+
+    public boolean isDoRun() {
+        return doRun;
+    }
+
+    public void close()
+    {
+        doRun = false;
+        notify();
+    }
+    
+    public void setUseBufferPool(boolean usebufpool) {
+        useBufferPool = usebufpool;
+    }
+    
+    public boolean getUseBufferPool() {
+        return useBufferPool;
+    }
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/BioReceiver.java b/java/org/apache/catalina/tribes/transport/bio/BioReceiver.java
new file mode 100644 (file)
index 0000000..ab4b67b
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.catalina.tribes.transport.bio;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.apache.catalina.tribes.ChannelReceiver;
+import org.apache.catalina.tribes.io.ListenCallback;
+import org.apache.catalina.tribes.io.ObjectReader;
+import org.apache.catalina.tribes.transport.ReceiverBase;
+import org.apache.catalina.tribes.transport.ThreadPool;
+import org.apache.catalina.tribes.transport.WorkerThread;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class BioReceiver extends ReceiverBase implements Runnable, ChannelReceiver, ListenCallback {
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(BioReceiver.class);
+
+    protected ServerSocket serverSocket;
+
+    public BioReceiver() {
+    }
+
+    /**
+     *
+     * @throws IOException
+     * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method
+     */
+    public void start() throws IOException {
+        try {
+            setPool(new ThreadPool(getMaxThreads(),getMinThreads(),this));
+        } catch (Exception x) {
+            log.fatal("ThreadPool can initilzed. Listener not started", x);
+            if ( x instanceof IOException ) throw (IOException)x;
+            else throw new IOException(x.getMessage());
+        }
+        try {
+            getBind();
+            bind();
+            Thread t = new Thread(this, "BioReceiver");
+            t.setDaemon(true);
+            t.start();
+        } catch (Exception x) {
+            log.fatal("Unable to start cluster receiver", x);
+            if ( x instanceof IOException ) throw (IOException)x;
+            else throw new IOException(x.getMessage());
+        }
+    }
+    
+    public WorkerThread getWorkerThread() {
+        return getReplicationThread();
+    }
+    
+    protected BioReplicationThread getReplicationThread() {
+        BioReplicationThread result = new BioReplicationThread(this);
+        result.setOptions(getWorkerThreadOptions());
+        result.setUseBufferPool(this.getUseBufferPool());
+        return result;
+    }
+
+    /**
+     *
+     * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method
+     */
+    public void stop() {
+        setListen(false);
+        try {
+            this.serverSocket.close();
+        }catch ( Exception x ) {}
+    }
+
+    
+    
+    
+    protected void bind() throws IOException {
+        // allocate an unbound server socket channel
+        serverSocket = new ServerSocket();
+        // set the port the server channel will listen to
+        //serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort()));
+        bind(serverSocket,getPort(),getAutoBind());
+    }
+    
+    
+    
+    public void run() {
+        try {
+            listen();
+        } catch (Exception x) {
+            log.error("Unable to run replication listener.", x);
+        }
+    }
+    
+    public void listen() throws Exception {
+        if (doListen()) {
+            log.warn("ServerSocket already started");
+            return;
+        }
+        setListen(true);
+
+        while ( doListen() ) {
+            Socket socket = null;
+            if ( getPool().available() < 1 ) {
+                if ( log.isWarnEnabled() )
+                    log.warn("All BIO server replication threads are busy, unable to handle more requests until a thread is freed up.");
+            }
+            BioReplicationThread thread = (BioReplicationThread)getPool().getWorker();
+            if ( thread == null ) continue; //should never happen
+            try {
+                socket = serverSocket.accept();
+            }catch ( Exception x ) {
+                if ( doListen() ) throw x;
+            }
+            if ( !doListen() ) {
+                thread.setDoRun(false);
+                thread.serviceSocket(null,null);
+                break; //regular shutdown
+            }
+            if ( socket == null ) continue;
+            socket.setReceiveBufferSize(getRxBufSize());
+            socket.setSendBufferSize(getRxBufSize());
+            socket.setTcpNoDelay(getTcpNoDelay());
+            socket.setKeepAlive(getSoKeepAlive());
+            socket.setOOBInline(getOoBInline());
+            socket.setReuseAddress(getSoReuseAddress());
+            socket.setSoLinger(getSoLingerOn(),getSoLingerTime());
+            socket.setTrafficClass(getSoTrafficClass());
+            socket.setSoTimeout(getTimeout());
+            ObjectReader reader = new ObjectReader(socket);
+            thread.serviceSocket(socket,reader);
+        }//while
+    }
+    
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java b/java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java
new file mode 100644 (file)
index 0000000..eacf6eb
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport.bio;
+import java.io.IOException;
+
+import org.apache.catalina.tribes.io.ObjectReader;
+import org.apache.catalina.tribes.transport.Constants;
+import org.apache.catalina.tribes.transport.WorkerThread;
+import java.net.Socket;
+import java.io.InputStream;
+import org.apache.catalina.tribes.transport.ReceiverBase;
+import java.io.OutputStream;
+import org.apache.catalina.tribes.io.ListenCallback;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.BufferPool;
+
+/**
+ * A worker thread class which can drain channels and echo-back the input. Each
+ * instance is constructed with a reference to the owning thread pool object.
+ * When started, the thread loops forever waiting to be awakened to service the
+ * channel associated with a SelectionKey object. The worker is tasked by
+ * calling its serviceChannel() method with a SelectionKey object. The
+ * serviceChannel() method stores the key reference in the thread object then
+ * calls notify() to wake it up. When the channel has been drained, the worker
+ * thread returns itself to its parent pool.
+ * 
+ * @author Filip Hanik
+ * 
+ * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $
+ */
+public class BioReplicationThread extends WorkerThread {
+
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( BioReplicationThread.class );
+    
+    protected Socket socket;
+    protected ObjectReader reader;
+    
+    public BioReplicationThread (ListenCallback callback) {
+        super(callback);
+    }
+
+    // loop forever waiting for work to do
+    public synchronized void run()
+    {
+        this.notify();
+        while (isDoRun()) {
+            try {
+                // sleep and release object lock
+                this.wait();
+            } catch (InterruptedException e) {
+                if(log.isInfoEnabled())
+                    log.info("TCP worker thread interrupted in cluster",e);
+                // clear interrupt status
+                Thread.interrupted();
+            }
+            if ( socket == null ) continue;
+            try {
+                drainSocket();
+            } catch ( Exception x ) {
+                log.error("Unable to service bio socket");
+            }finally {
+                try {socket.close();}catch ( Exception ignore){}
+                try {reader.close();}catch ( Exception ignore){}
+                reader = null;
+                socket = null;
+            }
+            // done, ready for more, return to pool
+            if ( getPool() != null ) getPool().returnWorker (this);
+            else setDoRun(false);
+        }
+    }
+
+    
+    public synchronized void serviceSocket(Socket socket, ObjectReader reader) {
+        this.socket = socket;
+        this.reader = reader;
+        this.notify();         // awaken the thread
+    }
+    
+    protected void execute(ObjectReader reader) throws Exception{
+        int pkgcnt = reader.count();
+
+        if ( pkgcnt > 0 ) {
+            ChannelMessage[] msgs = reader.execute();
+            for ( int i=0; i<msgs.length; i++ ) {
+                /**
+                 * Use send ack here if you want to ack the request to the remote 
+                 * server before completing the request
+                 * This is considered an asynchronized request
+                 */
+                if (ChannelData.sendAckAsync(msgs[i].getOptions())) sendAck(Constants.ACK_COMMAND);
+                try {
+                    //process the message
+                    getCallback().messageDataReceived(msgs[i]);
+                    /**
+                     * Use send ack here if you want the request to complete on this
+                     * server before sending the ack to the remote server
+                     * This is considered a synchronized request
+                     */
+                    if (ChannelData.sendAckSync(msgs[i].getOptions())) sendAck(Constants.ACK_COMMAND);
+                }catch  ( Exception x ) {
+                    if (ChannelData.sendAckSync(msgs[i].getOptions())) sendAck(Constants.FAIL_ACK_COMMAND);
+                    log.error("Error thrown from messageDataReceived.",x);
+                }
+                if ( getUseBufferPool() ) {
+                    BufferPool.getBufferPool().returnBuffer(msgs[i].getMessage());
+                    msgs[i].setMessage(null);
+                }
+            }                       
+        }
+
+       
+    }
+
+    /**
+     * The actual code which drains the channel associated with
+     * the given key.  This method assumes the key has been
+     * modified prior to invocation to turn off selection
+     * interest in OP_READ.  When this method completes it
+     * re-enables OP_READ and calls wakeup() on the selector
+     * so the selector will resume watching this channel.
+     */
+    protected void drainSocket () throws Exception {
+        InputStream in = socket.getInputStream();
+        // loop while data available, channel is non-blocking
+        byte[] buf = new byte[1024];
+        int length = in.read(buf);
+        while ( length >= 0 ) {
+            int count = reader.append(buf,0,length,true);
+            if ( count > 0 ) execute(reader);
+            length = in.read(buf);
+        }
+    }
+
+
+
+
+    /**
+     * send a reply-acknowledgement (6,2,3)
+     * @param key
+     * @param channel
+     */
+    protected void sendAck(byte[] command) {
+        try {
+            OutputStream out = socket.getOutputStream();
+            out.write(command);
+            out.flush();
+            if (log.isTraceEnabled()) {
+                log.trace("ACK sent to " + socket.getPort());
+            }
+        } catch ( java.io.IOException x ) {
+            log.warn("Unable to send ACK back through channel, channel disconnected?: "+x.getMessage());
+        }
+    }
+    
+    public void close() {
+        setDoRun(false);
+        try {socket.close();}catch ( Exception ignore){}
+        try {reader.close();}catch ( Exception ignore){}
+        reader = null;
+        socket = null;
+        super.close();
+    }
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/BioSender.java b/java/org/apache/catalina/tribes/transport/bio/BioSender.java
new file mode 100644 (file)
index 0000000..8a1aead
--- /dev/null
@@ -0,0 +1,305 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.catalina.tribes.transport.bio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.RemoteProcessException;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.transport.AbstractSender;
+import org.apache.catalina.tribes.transport.Constants;
+import org.apache.catalina.tribes.transport.DataSender;
+import org.apache.catalina.tribes.transport.SenderState;
+import org.apache.catalina.tribes.util.StringManager;
+
+/**
+ * Send cluster messages with only one socket. Ack and keep Alive Handling is
+ * supported
+ * 
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 377484 $ $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ * @since 5.5.16
+ */
+public class BioSender extends AbstractSender implements DataSender {
+
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(BioSender.class);
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm = StringManager.getManager(Constants.Package);
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * The descriptive information about this implementation.
+     */
+    private static final String info = "DataSender/3.0";
+
+    
+    /**
+     * current sender socket
+     */
+    private Socket socket = null;
+    private OutputStream soOut = null;
+    private InputStream soIn = null;
+    
+    protected XByteBuffer ackbuf = new XByteBuffer(Constants.ACK_COMMAND.length,true);
+
+
+    // ------------------------------------------------------------- Constructor
+    
+    public BioSender()  {
+    }
+
+
+    // ------------------------------------------------------------- Properties
+
+    /**
+     * Return descriptive information about this implementation and the
+     * corresponding version number, in the format
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.
+     */
+    public String getInfo() {
+        return (info);
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Connect other cluster member receiver 
+     * @see org.apache.catalina.tribes.transport.IDataSender#connect()
+     */
+    public  void connect() throws IOException {
+        openSocket();
+   }
+
+    /**
+     * disconnect and close socket
+     * 
+     * @see IDataSender#disconnect()
+     */
+    public  void disconnect() {
+        boolean connect = isConnected();
+        closeSocket();
+        if (connect) {
+            if (log.isDebugEnabled())
+                log.debug(sm.getString("IDataSender.disconnect", getAddress().getHostAddress(), new Integer(getPort()), new Long(0)));
+        }
+        
+    }
+
+    /**
+     * Send message
+     * 
+     * @see org.apache.catalina.tribes.transport.IDataSender#sendMessage(,
+     *      ChannelMessage)
+     */
+    public  void sendMessage(byte[] data, boolean waitForAck) throws IOException {
+        boolean messageTransfered = false ;
+        IOException exception = null;
+        setAttempt(0);
+        try {
+             // first try with existing connection
+             pushMessage(data,false,waitForAck);
+             messageTransfered = true ;
+        } catch (IOException x) {
+            SenderState.getSenderState(getDestination()).setSuspect();
+            exception = x;
+            if (log.isTraceEnabled()) log.trace(sm.getString("IDataSender.send.again", getAddress().getHostAddress(),new Integer(getPort())),x);
+            while ( getAttempt()<getMaxRetryAttempts() ) {
+                try {
+                    setAttempt(getAttempt()+1);
+                    // second try with fresh connection
+                    pushMessage(data, true,waitForAck);
+                    messageTransfered = true;
+                    exception = null;
+                } catch (IOException xx) {
+                    exception = xx;
+                    closeSocket();
+                }
+            }
+        } finally {
+            setRequestCount(getRequestCount()+1);
+            keepalive();
+            if(messageTransfered) {
+
+            } else {
+                if ( exception != null ) throw exception;
+            }
+        }
+
+    }
+
+    
+    /**
+     * Name of this SockerSender
+     */
+    public String toString() {
+        StringBuffer buf = new StringBuffer("DataSender[(");
+        buf.append(super.toString()).append(")");
+        buf.append(getAddress()).append(":").append(getPort()).append("]");
+        return buf.toString();
+    }
+
+    // --------------------------------------------------------- Protected Methods
+    /**
+     * open real socket and set time out when waitForAck is enabled
+     * is socket open return directly
+     */
+    protected  void openSocket() throws IOException {
+       if(isConnected()) return ;
+       try {
+           socket = new Socket();
+           InetSocketAddress sockaddr = new InetSocketAddress(getAddress(), getPort());
+           socket.connect(sockaddr,(int)getTimeout());
+           socket.setSendBufferSize(getTxBufSize());
+           socket.setReceiveBufferSize(getRxBufSize());
+           socket.setSoTimeout( (int) getTimeout());
+           socket.setTcpNoDelay(getTcpNoDelay());
+           socket.setKeepAlive(getSoKeepAlive());
+           socket.setReuseAddress(getSoReuseAddress());
+           socket.setOOBInline(getOoBInline());
+           socket.setSoLinger(getSoLingerOn(),getSoLingerTime());
+           socket.setTrafficClass(getSoTrafficClass());
+           setConnected(true);
+           soOut = socket.getOutputStream();
+           soIn  = socket.getInputStream();
+           setRequestCount(0);
+           setConnectTime(System.currentTimeMillis());
+           if (log.isDebugEnabled())
+               log.debug(sm.getString("IDataSender.openSocket", getAddress().getHostAddress(), new Integer(getPort()), new Long(0)));
+      } catch (IOException ex1) {
+          SenderState.getSenderState(getDestination()).setSuspect();
+          if (log.isDebugEnabled())
+              log.debug(sm.getString("IDataSender.openSocket.failure",getAddress().getHostAddress(), new Integer(getPort()),new Long(0)), ex1);
+          throw (ex1);
+        }
+        
+     }
+
+    /**
+     * close socket
+     * 
+     * @see DataSender#disconnect()
+     * @see DataSender#closeSocket()
+     */
+    protected  void closeSocket() {
+        if(isConnected()) {
+             if (socket != null) {
+                try {
+                    socket.close();
+                } catch (IOException x) {
+                } finally {
+                    socket = null;
+                    soOut = null;
+                    soIn = null;
+                }
+            }
+            setRequestCount(0);
+            setConnected(false);
+            if (log.isDebugEnabled())
+                log.debug(sm.getString("IDataSender.closeSocket",getAddress().getHostAddress(), new Integer(getPort()),new Long(0)));
+       }
+    }
+
+    /**
+     * Push messages with only one socket at a time
+     * Wait for ack is needed and make auto retry when write message is failed.
+     * After sending error close and reopen socket again.
+     * 
+     * After successfull sending update stats
+     * 
+     * WARNING: Subclasses must be very carefull that only one thread call this pushMessage at once!!!
+     * 
+     * @see #closeSocket()
+     * @see #openSocket()
+     * @see #writeData(ChannelMessage)
+     * 
+     * @param data
+     *            data to send
+     * @since 5.5.10
+     */
+    
+    protected  void pushMessage(byte[] data, boolean reconnect, boolean waitForAck) throws IOException {
+        keepalive();
+        if ( reconnect ) closeSocket();
+        if (!isConnected()) openSocket();
+        soOut.write(data);
+        soOut.flush();
+        if (waitForAck) waitForAck();
+        SenderState.getSenderState(getDestination()).setReady();
+
+    }
+    
+    /**
+     * Wait for Acknowledgement from other server
+     * FIXME Please, not wait only for three charcters, better control that the wait ack message is correct.
+     * @param timeout
+     * @throws java.io.IOException
+     * @throws java.net.SocketTimeoutException
+     */
+    protected  void waitForAck() throws java.io.IOException {
+        try {
+            boolean ackReceived = false;
+            boolean failAckReceived = false;
+            ackbuf.clear();
+            int bytesRead = 0;
+            int i = soIn.read();
+            while ((i != -1) && (bytesRead < Constants.ACK_COMMAND.length)) {
+                bytesRead++;
+                byte d = (byte)i;
+                ackbuf.append(d);
+                if (ackbuf.doesPackageExist() ) {
+                    byte[] ackcmd = ackbuf.extractDataPackage(true).getBytes();
+                    ackReceived = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.ACK_DATA);
+                    failAckReceived = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA);
+                    ackReceived = ackReceived || failAckReceived;
+                    break;
+                }
+                i = soIn.read();
+            }
+            if (!ackReceived) {
+                if (i == -1) throw new IOException(sm.getString("IDataSender.ack.eof",getAddress(), new Integer(socket.getLocalPort())));
+                else throw new IOException(sm.getString("IDataSender.ack.wrong",getAddress(), new Integer(socket.getLocalPort())));
+            } else if ( failAckReceived && getThrowOnFailedAck()) {
+                throw new RemoteProcessException("Received a failed ack:org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA");
+            }
+        } catch (IOException x) {
+            String errmsg = sm.getString("IDataSender.ack.missing", getAddress(),new Integer(socket.getLocalPort()), new Long(getTimeout()));
+            if ( SenderState.getSenderState(getDestination()).isReady() ) {
+                SenderState.getSenderState(getDestination()).setSuspect();
+                if ( log.isWarnEnabled() ) log.warn(errmsg, x);
+            } else {
+                if ( log.isDebugEnabled() )log.debug(errmsg, x);
+            }
+            throw x;
+        } finally {
+            ackbuf.clear();
+        }
+    }
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/MultipointBioSender.java b/java/org/apache/catalina/tribes/transport/bio/MultipointBioSender.java
new file mode 100644 (file)
index 0000000..9a45082
--- /dev/null
@@ -0,0 +1,134 @@
+package org.apache.catalina.tribes.transport.bio;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.transport.MultiPointSender;
+import org.apache.catalina.tribes.transport.AbstractSender;
+import org.apache.catalina.tribes.Channel;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class MultipointBioSender extends AbstractSender implements MultiPointSender {
+    public MultipointBioSender() {
+    }
+    
+    protected long selectTimeout = 1000; 
+    protected HashMap bioSenders = new HashMap();
+    private boolean autoConnect;
+
+    public synchronized void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException {
+        byte[] data = XByteBuffer.createDataPackage((ChannelData)msg);
+        BioSender[] senders = setupForSend(destination);
+        ChannelException cx = null;
+        for ( int i=0; i<senders.length; i++ ) {
+            try {
+                senders[i].sendMessage(data,(msg.getOptions()&Channel.SEND_OPTIONS_USE_ACK)==Channel.SEND_OPTIONS_USE_ACK);
+            } catch (Exception x) {
+                if (cx == null) cx = new ChannelException(x);
+                cx.addFaultyMember(destination[i],x);
+            }
+        }
+        if (cx!=null ) throw cx;
+    }
+
+
+
+    protected BioSender[] setupForSend(Member[] destination) throws ChannelException {
+        ChannelException cx = null;
+        BioSender[] result = new BioSender[destination.length];
+        for ( int i=0; i<destination.length; i++ ) {
+            try {
+                BioSender sender = (BioSender) bioSenders.get(destination[i]);
+                if (sender == null) {
+                    sender = new BioSender();
+                    sender.transferProperties(this,sender);
+                    sender.setDestination(destination[i]);
+                    bioSenders.put(destination[i], sender);
+                }
+                result[i] = sender;
+                if (!result[i].isConnected() ) result[i].connect();
+                result[i].keepalive();
+            }catch (Exception x ) {
+                if ( cx== null ) cx = new ChannelException(x);
+                cx.addFaultyMember(destination[i],x);
+            }
+        }
+        if ( cx!=null ) throw cx;
+        else return result;
+    }
+
+    public void connect() throws IOException {
+        //do nothing, we connect on demand
+        setConnected(true);
+    }
+
+
+    private synchronized void close() throws ChannelException  {
+        ChannelException x = null;
+        Object[] members = bioSenders.keySet().toArray();
+        for (int i=0; i<members.length; i++ ) {
+            Member mbr = (Member)members[i];
+            try {
+                BioSender sender = (BioSender)bioSenders.get(mbr);
+                sender.disconnect();
+            }catch ( Exception e ) {
+                if ( x == null ) x = new ChannelException(e);
+                x.addFaultyMember(mbr,e);
+            }
+            bioSenders.remove(mbr);
+        }
+        if ( x != null ) throw x;
+    }
+
+    public void memberAdded(Member member) {
+
+    }
+
+    public void memberDisappeared(Member member) {
+        //disconnect senders
+        BioSender sender = (BioSender)bioSenders.remove(member);
+        if ( sender != null ) sender.disconnect();
+    }
+
+
+    public synchronized void disconnect() {
+        try {close(); }catch (Exception x){}
+        setConnected(false);
+    }
+
+    public void finalize() {
+        try {disconnect(); }catch ( Exception ignore){}
+    }
+
+
+    public boolean keepalive() {
+        //throw new UnsupportedOperationException("Method ParallelBioSender.checkKeepAlive() not implemented");
+        boolean result = false;
+        Map.Entry[] entries = (Map.Entry[])bioSenders.entrySet().toArray(new Map.Entry[bioSenders.size()]);
+        for ( int i=0; i<entries.length; i++ ) {
+            BioSender sender = (BioSender)entries[i].getValue();
+            if ( sender.keepalive() ) {
+                bioSenders.remove(entries[i].getKey());
+            }
+        }
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/bio/PooledMultiSender.java b/java/org/apache/catalina/tribes/transport/bio/PooledMultiSender.java
new file mode 100644 (file)
index 0000000..4ef5a39
--- /dev/null
@@ -0,0 +1,67 @@
+package org.apache.catalina.tribes.transport.bio;
+
+import org.apache.catalina.tribes.transport.DataSender;
+import org.apache.catalina.tribes.transport.PooledSender;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.transport.MultiPointSender;
+import org.apache.catalina.tribes.ChannelMessage;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class PooledMultiSender extends PooledSender {
+    
+
+    public PooledMultiSender() {
+    }
+    
+    public void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException {
+        MultiPointSender sender = null;
+        try {
+            sender = (MultiPointSender)getSender();
+            if (sender == null) {
+                ChannelException cx = new ChannelException("Unable to retrieve a data sender, time out error.");
+                for (int i = 0; i < destination.length; i++) cx.addFaultyMember(destination[i], new NullPointerException("Unable to retrieve a sender from the sender pool"));
+                throw cx;
+            } else {
+                sender.sendMessage(destination, msg);
+            }
+            sender.keepalive();
+        }finally {
+            if ( sender != null ) returnSender(sender);
+        }
+    }
+
+    /**
+     * getNewDataSender
+     *
+     * @return DataSender
+     * @todo Implement this org.apache.catalina.tribes.transport.PooledSender
+     *   method
+     */
+    public DataSender getNewDataSender() {
+        MultipointBioSender sender = new MultipointBioSender();
+        sender.transferProperties(this,sender);
+        return sender;
+    }
+
+
+    public void memberAdded(Member member) {
+
+    }
+
+    public void memberDisappeared(Member member) {
+        //disconnect senders
+    } 
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java b/java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java
new file mode 100644 (file)
index 0000000..0c34e3c
--- /dev/null
@@ -0,0 +1,393 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport.bio.util;
+
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ErrorHandler;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+
+
+
+/**
+ * A fast queue that remover thread lock the adder thread. <br/>Limit the queue
+ * length when you have strange producer thread problemes.
+ * 
+ * FIXME add i18n support to log messages
+ * @author Rainer Jung
+ * @author Peter Rossbach
+ * @version $Revision: 345567 $ $Date: 2005-11-18 15:07:23 -0600 (Fri, 18 Nov 2005) $
+ */
+public class FastQueue {
+
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(FastQueue.class);
+
+    /**
+     * This is the actual queue
+     */
+    private SingleRemoveSynchronizedAddLock lock = null;
+
+    /**
+     * First Object at queue (consumer message)
+     */
+    private LinkObject first = null;
+
+    /**
+     * Last object in queue (producer Object)
+     */
+    private LinkObject last = null;
+
+    /**
+     * Current Queue elements size
+     */
+    private int size = 0;
+
+    /**
+     * check lock to detect strange threadings things
+     */
+    private boolean checkLock = false;
+
+    /**
+     * protocol the thread wait times
+     */
+    private boolean timeWait = false;
+
+    private boolean inAdd = false;
+
+    private boolean inRemove = false;
+
+    private boolean inMutex = false;
+
+    /**
+     * limit the queue legnth ( default is unlimited)
+     */
+    private int maxQueueLength = 0;
+
+    /**
+     * addWaitTimeout for producer
+     */
+    private long addWaitTimeout = 10000L;
+
+    
+    /**
+     * removeWaitTimeout for consumer
+     */
+    private long removeWaitTimeout = 30000L;
+
+    /**
+     * enabled the queue
+     */
+    private boolean enabled = true;
+
+    /**
+     *  max queue size
+     */
+    private int maxSize = 0;
+
+    /**
+     *  avg size sample interval
+     */
+    private int sampleInterval = 100;
+
+    /**
+     * Generate Queue SingleRemoveSynchronizedAddLock and set add and wait
+     * Timeouts
+     */
+    public FastQueue() {
+        lock = new SingleRemoveSynchronizedAddLock();
+        lock.setAddWaitTimeout(addWaitTimeout);
+        lock.setRemoveWaitTimeout(removeWaitTimeout);
+    }
+
+    /**
+     * get current add wait timeout
+     * 
+     * @return current wait timeout
+     */
+    public long getAddWaitTimeout() {
+        addWaitTimeout = lock.getAddWaitTimeout();
+        return addWaitTimeout;
+    }
+
+    /**
+     * Set add wait timeout (default 10000 msec)
+     * 
+     * @param timeout
+     */
+    public void setAddWaitTimeout(long timeout) {
+        addWaitTimeout = timeout;
+        lock.setAddWaitTimeout(addWaitTimeout);
+    }
+
+    /**
+     * get current remove wait timeout
+     * 
+     * @return The timeout
+     */
+    public long getRemoveWaitTimeout() {
+        removeWaitTimeout = lock.getRemoveWaitTimeout();
+        return removeWaitTimeout;
+    }
+
+    /**
+     * set remove wait timeout ( default 30000 msec)
+     * 
+     * @param timeout
+     */
+    public void setRemoveWaitTimeout(long timeout) {
+        removeWaitTimeout = timeout;
+        lock.setRemoveWaitTimeout(removeWaitTimeout);
+    }
+
+    /**
+     * get Max Queue length
+     * 
+     * @see org.apache.catalina.tribes.util.IQueue#getMaxQueueLength()
+     */
+    public int getMaxQueueLength() {
+        return maxQueueLength;
+    }
+
+    public void setMaxQueueLength(int length) {
+        maxQueueLength = length;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enable) {
+        enabled = enable;
+        if (!enabled) {
+            lock.abortRemove();
+            last = first = null;
+        }
+    }
+
+    /**
+     * @return Returns the checkLock.
+     */
+    public boolean isCheckLock() {
+        return checkLock;
+    }
+
+    /**
+     * @param checkLock The checkLock to set.
+     */
+    public void setCheckLock(boolean checkLock) {
+        this.checkLock = checkLock;
+    }
+
+    
+    /**
+     * @return The max size
+     */
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+    /**
+     * @param size
+     */
+    public void setMaxSize(int size) {
+        maxSize = size;
+    }
+
+    
+    /**
+     * unlock queue for next add 
+     */
+    public void unlockAdd() {
+        lock.unlockAdd(size > 0 ? true : false);
+    }
+
+    /**
+     * unlock queue for next remove 
+     */
+    public void unlockRemove() {
+        lock.unlockRemove();
+    }
+
+    /**
+     * start queuing
+     */
+    public void start() {
+        setEnabled(true);
+    }
+
+    /**
+     * start queuing
+     */
+    public void stop() {
+        setEnabled(false);
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public SingleRemoveSynchronizedAddLock getLock() {
+        return lock;
+    }
+
+    /**
+     * Add new data to the queue
+     * @see org.apache.catalina.tribes.util.IQueue#add(java.lang.String, java.lang.Object)
+     * FIXME extract some method
+     */
+    public boolean add(ChannelMessage msg, Member[] destination, InterceptorPayload payload) {
+        boolean ok = true;
+        long time = 0;
+
+        if (!enabled) {
+            if (log.isInfoEnabled())
+                log.info("FastQueue.add: queue disabled, add aborted");
+            return false;
+        }
+
+        if (timeWait) {
+            time = System.currentTimeMillis();
+        }
+        lock.lockAdd();
+        try {
+            if (log.isTraceEnabled()) {
+                log.trace("FastQueue.add: starting with size " + size);
+            }
+            if (checkLock) {
+                if (inAdd)
+                    log.warn("FastQueue.add: Detected other add");
+                inAdd = true;
+                if (inMutex)
+                    log.warn("FastQueue.add: Detected other mutex in add");
+                inMutex = true;
+            }
+
+            if ((maxQueueLength > 0) && (size >= maxQueueLength)) {
+                ok = false;
+                if (log.isTraceEnabled()) {
+                    log.trace("FastQueue.add: Could not add, since queue is full (" + size + ">=" + maxQueueLength + ")");
+                }
+            } else {
+                LinkObject element = new LinkObject(msg,destination, payload);
+                if (size == 0) {
+                    first = last = element;
+                    size = 1;
+                } else {
+                    if (last == null) {
+                        ok = false;
+                        log.error("FastQueue.add: Could not add, since last is null although size is "+ size + " (>0)");
+                    } else {
+                        last.append(element);
+                        last = element;
+                        size++;
+                    }
+                }
+            }
+
+            if (first == null) {
+                log.error("FastQueue.add: first is null, size is " + size + " at end of add");
+            }
+            if (last == null) {
+                log.error("FastQueue.add: last is null, size is " + size+ " at end of add");
+            }
+
+            if (checkLock) {
+                if (!inMutex) log.warn("FastQueue.add: Cancelled by other mutex in add");
+                inMutex = false;
+                if (!inAdd) log.warn("FastQueue.add: Cancelled by other add");
+                inAdd = false;
+            }
+            if (log.isTraceEnabled()) log.trace("FastQueue.add: add ending with size " + size);
+
+        } finally {
+            lock.unlockAdd(true);
+        }
+        return ok;
+    }
+
+    /**
+     * remove the complete queued object list
+     * @see org.apache.catalina.tribes.util.IQueue#remove()
+     * FIXME extract some method
+     */
+    public LinkObject remove() {
+        LinkObject element;
+        boolean gotLock;
+        long time = 0;
+
+        if (!enabled) {
+            if (log.isInfoEnabled())
+                log.info("FastQueue.remove: queue disabled, remove aborted");
+            return null;
+        }
+
+        if (timeWait) {
+            time = System.currentTimeMillis();
+        }
+        gotLock = lock.lockRemove();
+        try {
+
+            if (!gotLock) {
+                if (enabled) {
+                    if (log.isInfoEnabled())
+                        log.info("FastQueue.remove: Remove aborted although queue enabled");
+                } else {
+                    if (log.isInfoEnabled())
+                        log.info("FastQueue.remove: queue disabled, remove aborted");
+                }
+                return null;
+            }
+
+            if (log.isTraceEnabled()) {
+                log.trace("FastQueue.remove: remove starting with size " + size);
+            }
+            if (checkLock) {
+                if (inRemove)
+                    log.warn("FastQueue.remove: Detected other remove");
+                inRemove = true;
+                if (inMutex)
+                    log.warn("FastQueue.remove: Detected other mutex in remove");
+                inMutex = true;
+            }
+
+            element = first;
+
+            first = last = null;
+            size = 0;
+
+            if (checkLock) {
+                if (!inMutex)
+                    log.warn("FastQueue.remove: Cancelled by other mutex in remove");
+                inMutex = false;
+                if (!inRemove)
+                    log.warn("FastQueue.remove: Cancelled by other remove");
+                inRemove = false;
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("FastQueue.remove: remove ending with size " + size);
+            }
+
+            if (timeWait) {
+                time = System.currentTimeMillis();
+            }
+        } finally {
+            lock.unlockRemove();
+        }
+        return element;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java b/java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java
new file mode 100644 (file)
index 0000000..9000f9c
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport.bio.util;
+
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.ErrorHandler;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+
+/**
+ * The class <b>LinkObject</b> implements an element
+ * for a linked list, consisting of a general
+ * data object and a pointer to the next element.
+ *
+ * @author Rainer Jung
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 304032 $ $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+
+ */
+
+public class LinkObject {
+
+    private ChannelMessage msg;
+    private LinkObject next;
+    private byte[] key ;
+    private Member[] destination;
+    private InterceptorPayload payload;
+
+    /**
+     * Construct a new element from the data object.
+     * Sets the pointer to null.
+     *
+     * @param key The key
+     * @param payload The data object.
+     */
+    public LinkObject(ChannelMessage msg, Member[] destination, InterceptorPayload payload) {
+        this.msg = msg;
+        this.next = null;
+        this.key = msg.getUniqueId();
+        this.payload = payload;
+        this.destination = destination;
+    }
+
+    /**
+     * Set the next element.
+     * @param next The next element.
+     */
+    public void append(LinkObject next) {
+        this.next = next;
+    }
+
+    /**
+     * Get the next element.
+     * @return The next element.
+     */
+    public LinkObject next() {
+        return next;
+    }
+    
+    public void setNext(LinkObject next) {
+        this.next = next;
+    }
+
+    /**
+     * Get the data object from the element.
+     * @return The data object from the element.
+     */
+    public ChannelMessage data() {
+        return msg;
+    }
+
+    /**
+     * Get the unique message id
+     * @return the unique message id
+     */
+    public byte[] getKey() {
+        return key;
+    }
+
+    public ErrorHandler getHandler() {
+        return payload!=null?payload.getErrorHandler():null;
+    }
+
+    public InterceptorPayload getPayload() {
+        return payload;
+    }
+
+    public Member[] getDestination() {
+        return destination;
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java b/java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java
new file mode 100644 (file)
index 0000000..fe030db
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport.bio.util;
+
+/**
+ * The class <b>SingleRemoveSynchronizedAddLock</b> implement locking for accessing the queue
+ * by a single remove thread and multiple add threads.
+ *
+ * A thread is only allowed to be either the remove or
+ * an add thread.
+ *
+ * The lock can either be owned by the remove thread
+ * or by a single add thread.
+ *
+ * If the remove thread tries to get the lock,
+ * but the queue is empty, it will block (poll)
+ * until an add threads adds an entry to the queue and
+ * releases the lock.
+ * 
+ * If the remove thread and add threads compete for
+ * the lock and an add thread releases the lock, then
+ * the remove thread will get the lock first.
+ *
+ * The remove thread removes all entries in the queue
+ * at once and proceeses them without further
+ * polling the queue.
+ *
+ * The lock is not reentrant, in the sense, that all
+ * threads must release an owned lock before competing
+ * for the lock again!
+ *
+ * @author Rainer Jung
+ * @author Peter Rossbach
+ * @version 1.1
+ */
+public class SingleRemoveSynchronizedAddLock {
+    
+    public SingleRemoveSynchronizedAddLock() {
+    }
+    
+    public SingleRemoveSynchronizedAddLock(boolean dataAvailable) {
+        this.dataAvailable=dataAvailable;
+    }
+    
+    /**
+     * Time in milliseconds after which threads
+     * waiting for an add lock are woken up.
+     * This is used as a safety measure in case
+     * thread notification via the unlock methods
+     * has a bug.
+     */
+    private long addWaitTimeout = 10000L;
+
+    /**
+     * Time in milliseconds after which threads
+     * waiting for a remove lock are woken up.
+     * This is used as a safety measure in case
+     * thread notification via the unlock methods
+     * has a bug.
+     */
+    private long removeWaitTimeout = 30000L;
+
+    /**
+     * The current remove thread.
+     * It is set to the remove thread polling for entries.
+     * It is reset to null when the remove thread
+     * releases the lock and proceeds processing
+     * the removed entries.
+     */
+    private Thread remover = null;
+
+    /**
+     * A flag indicating, if an add thread owns the lock.
+     */
+    private boolean addLocked = false;
+
+    /**
+     * A flag indicating, if the remove thread owns the lock.
+     */
+    private boolean removeLocked = false;
+
+    /**
+     * A flag indicating, if the remove thread is allowed
+     * to wait for the lock. The flag is set to false, when aborting.
+     */
+    private boolean removeEnabled = true;
+
+    /**
+     * A flag indicating, if the remover needs polling.
+     * It indicates, if the locked object has data available
+     * to be removed.
+     */
+    private boolean dataAvailable = false;
+
+    /**
+     * @return Value of addWaitTimeout
+     */
+    public synchronized long getAddWaitTimeout() {
+        return addWaitTimeout;
+    }
+
+    /**
+     * Set value of addWaitTimeout
+     */
+    public synchronized void setAddWaitTimeout(long timeout) {
+        addWaitTimeout = timeout;
+    }
+
+    /**
+     * @return Value of removeWaitTimeout
+     */
+    public synchronized long getRemoveWaitTimeout() {
+        return removeWaitTimeout;
+    }
+
+    /**
+     * Set value of removeWaitTimeout
+     */
+    public synchronized void setRemoveWaitTimeout(long timeout) {
+        removeWaitTimeout = timeout;
+    }
+
+    /**
+     * Check if the locked object has data available
+     * i.e. the remover can stop poling and get the lock.
+     * @return True iff the lock Object has data available.
+     */
+    public synchronized boolean isDataAvailable() {
+        return dataAvailable;
+    }
+
+    /**
+     * Check if an add thread owns the lock.
+     * @return True iff an add thread owns the lock.
+     */
+    public synchronized boolean isAddLocked() {
+        return addLocked;
+    }
+
+    /**
+     * Check if the remove thread owns the lock.
+     * @return True iff the remove thread owns the lock.
+     */
+    public synchronized boolean isRemoveLocked() {
+        return removeLocked;
+    }
+
+    /**
+     * Check if the remove thread is polling.
+     * @return True iff the remove thread is polling.
+     */
+    public synchronized boolean isRemovePolling() {
+        if ( remover != null ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Acquires the lock by an add thread and sets the add flag.
+     * If any add thread or the remove thread already acquired the lock
+     * this add thread will block until the lock is released.
+     */
+    public synchronized void lockAdd() {
+        if ( addLocked || removeLocked ) {
+            do {
+                try {
+                    wait(addWaitTimeout);
+                } catch ( InterruptedException e ) {
+                    Thread.currentThread().interrupted();
+                }
+            } while ( addLocked || removeLocked );
+        }
+        addLocked=true;
+    }
+
+    /**
+     * Acquires the lock by the remove thread and sets the remove flag.
+     * If any add thread already acquired the lock or the queue is
+     * empty, the remove thread will block until the lock is released
+     * and the queue is not empty.
+     */
+    public synchronized boolean lockRemove() {
+        removeLocked=false;
+        removeEnabled=true;
+        if ( ( addLocked || ! dataAvailable ) && removeEnabled ) {
+            remover=Thread.currentThread();
+            do {
+                try {
+                    wait(removeWaitTimeout);
+                } catch ( InterruptedException e ) {
+                    Thread.currentThread().interrupted();
+                }
+            } while ( ( addLocked || ! dataAvailable ) && removeEnabled );
+            remover=null;
+        }
+        if ( removeEnabled ) {
+            removeLocked=true;
+        } 
+        return removeLocked;
+    }
+
+    /**
+     * Releases the lock by an add thread and reset the remove flag.
+     * If the reader thread is polling, notify it.
+     */
+    public synchronized void unlockAdd(boolean dataAvailable) {
+        addLocked=false;
+        this.dataAvailable=dataAvailable;
+        if ( ( remover != null ) && ( dataAvailable || ! removeEnabled ) ) {
+            remover.interrupt();
+        } else {
+            notifyAll();
+        }
+    }
+
+    /**
+     * Releases the lock by the remove thread and reset the add flag.
+     * Notify all waiting add threads,
+     * that the lock has been released by the remove thread.
+     */
+    public synchronized void unlockRemove() {
+        removeLocked=false;
+        dataAvailable=false;
+        notifyAll();
+    }
+
+    /**
+     * Abort any polling remover thread
+     */
+    public synchronized void abortRemove() {
+        removeEnabled=false;
+        if ( remover != null ) {
+            remover.interrupt();
+        }
+    }
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml
new file mode 100644 (file)
index 0000000..e7b8624
--- /dev/null
@@ -0,0 +1,835 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mbeans-descriptors PUBLIC
+   "-//Apache Software Foundation//DTD Model MBeans Configuration File"
+   "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
+<mbeans-descriptors>
+
+  <mbean         name="SimpleTcpCluster"
+           description="Tcp Cluster implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.cluster.tcp.SimpleTcpCluster">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="notifyLifecycleListenerOnFailure"
+          description="notify lifecycleListener from message transfer failure"
+                            is="true"
+                 type="boolean"/>                 
+    <attribute   name="clusterName"
+          description="name of cluster"
+                 type="java.lang.String"/>
+    <attribute   name="managerClassName"
+          description="session mananager classname"
+                 type="java.lang.String"/>
+    <attribute   name="clusterLogName"
+          description="Name of cluster transfer log device"
+                 type="java.lang.String"/>
+    <attribute   name="doClusterLog"
+                            is="true"
+          description="enable cluster log transfer logging"
+                 type="boolean"/>
+    <operation   name="setProperty"
+               description="set a property to all cluster managers (with prefix 'manager.')"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="key"
+                 description="Property name"
+                 type="java.lang.String"/>
+      <parameter name="value"
+                 description="Property value"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="send"
+               description="send message to all cluster members"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina.cluster.ClusterMessage"/>
+    </operation>
+    
+    <operation   name="sendClusterDomain"
+               description="send message to all cluster members with same domain"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina.cluster.ClusterMessage"/>
+    </operation>
+        
+    <operation   name="sendToMember"
+               description="send message to one cluster member"
+               impact="ACTION"
+               returnType="void">
+      <parameter name="message"
+                 description="replication message"
+                 type="org.apache.catalina..cluster.ClusterMessage"/>
+      <parameter name="member"
+                 description="cluster member"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+  <mbean         name="ClusterReceiverBase"
+           description="Tcp Cluster NioReceiver implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.cluster.tcp.ClusterReceiverBase">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="tcpListenAddress"
+          description="tcp listener address"
+                 type="java.lang.String"/>
+    <attribute   name="tcpListenPort"
+          description="tcp listener port"
+                 type="int"/>
+    <attribute   name="tcpThreadCount"
+          description="number of tcp listener worker threads"
+                 type="int"/>
+    <attribute   name="tcpSelectorTimeout"
+          description="tcp listener Selector timeout"
+                 type="long"/>
+    <attribute   name="nrOfMsgsReceived"
+          description="number of messages received from other nodes"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedTime"
+          description="total time message send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedProcessingTime"
+          description="received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minReceivedProcessingTime"
+          description="minimal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgReceivedProcessingTime"
+          description="received processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxReceivedProcessingTime"
+          description="maximal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doReceivedProcessingStats"
+          description="create received processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="avgTotalReceivedBytes"
+          description="received totalReceivedBytes / nrOfMsgsReceived"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalReceivedBytes"
+          description="number of bytes received"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="sendAck"
+          description="send ack after data received"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="compress"
+          description="data received compressed"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="doListen"
+          description="is port really started"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+                 
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+  <mbean         name="SocketReplicationListener"
+           description="Tcp Cluster SocketReplicationListener implementation"
+               domain="Catalina"
+                group="Cluster"
+                 type="org.apache.catalina.cluster.tcp.SocketReplicationListener">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="tcpListenAddress"
+          description="tcp listener address"
+                 type="java.lang.String"/>
+    <attribute   name="tcpListenPort"
+          description="tcp listener port"
+                 type="int"/>
+    <attribute   name="tcpListenMaxPort"
+          description="max tcp listen used port"
+                 type="int"/>
+    <attribute   name="tcpListenTimeout"
+          description="max tcp listen timeout (sec) wait for ServerSocket start"
+                 type="int"/>                
+    <attribute   name="nrOfMsgsReceived"
+          description="number of messages received from other nodes"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedTime"
+          description="total time message send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="receivedProcessingTime"
+          description="received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minReceivedProcessingTime"
+          description="minimal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgReceivedProcessingTime"
+          description="received processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxReceivedProcessingTime"
+          description="maximal received processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doReceivedProcessingStats"
+          description="create received processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="avgTotalReceivedBytes"
+          description="received totalReceivedBytes / nrOfMsgsReceived"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalReceivedBytes"
+          description="number of bytes received"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="sendAck"
+          description="send ack after data received"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+   <attribute   name="compress"
+          description="data received compressed"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+   <attribute   name="doListen"
+          description="is port really started"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+                 
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+  
+  <mbean         name="ReplicationTransmitter"
+          description="Tcp replication transmitter"
+               domain="Catalina"
+                group="ClusterSender"
+                 type="org.apache.catalina.cluster.tcp.ReplicationTransmitter">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="replicationMode"
+          description="replication mode (synchnous,pooled.asynchnous,fastasyncqueue)"
+                 type="java.lang.String"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="autoConnect"
+          description="is sender disabled, fork a new socket"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doTransmitterProcessingStats"
+          description="create processing time stats"
+                            is="true"
+                 type="boolean" />                
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="failureCounter"
+          description="number of wrong transfers"
+                 type="long"
+                 writeable="false"/>
+       <attribute name="senderObjectNames"
+               description="get all sender object names"
+               type="[Ljavax.management.ObjectName;"
+               writeable="false"/>
+    <operation   name="start"
+               description="Start the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>    
+    <operation name="stop"
+               description="Stop the cluster"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>       
+       <operation name="checkKeepAlive"
+               description="Check all sender connection for close socket (keepalive)"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+  </mbean>
+
+  <mbean         name="AsyncSocketSender"
+          description="Async Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.cluster.tcp.AsyncSocketSender">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>                 
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="queueSize"
+                 writeable="false"
+          description="queue size"
+                 type="int"/>
+    <attribute   name="queuedNrOfBytes"
+                 writeable="false"
+          description="number of bytes over all queued messages"
+                 type="long"/>
+    <attribute   name="messageTransferStarted"
+          description="message is in transfer"
+                 type="boolean"
+                 is="true"
+                 writeable="false"/>
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="keepAliveCount"
+          description="keep Alive request count"
+                 type="int"
+                 writeable="false"/>
+    <attribute   name="keepAliveConnectTime"
+          description="Connect time for keep alive"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doProcessingStats"
+          description="create processing time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitAckTime"
+          description="sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minWaitAckTime"
+          description="minimal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgWaitAckTime"
+          description="waitAck time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxWaitAckTime"
+          description="maximal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doWaitAckStats"
+          description="create waitAck time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenCounter"
+          description="counts open socket (KeepAlive and connects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenFailureCounter"
+          description="counts open socket failures"
+                 type="long"
+                 writeable="false"/>                                            
+    <attribute   name="socketCloseCounter"
+          description="counts closed socket (KeepAlive and disconnects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="missingAckCounter"
+          description="counts missing ack"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataResendCounter"
+          description="counts data resends"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataFailureCounter"
+          description="counts data send failures"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="inQueueCounter"
+          description="counts all queued messages"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="outQueueCounter"
+          description="counts all successfully sended messages"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="connect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="disconnect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="checkKeepAlive"
+               description="Check connection for close socket"
+               impact="ACTION"
+               returnType="boolean">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+
+  <mbean         name="MultiSocketSender"
+          description="Multi Socket Sender, more than one socket per member"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.cluster.tcp.PooledSocketSender">
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="maxPoolSocketLimit"
+          description="Max parallel sockets"
+                 type="int"/>
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="start Queue to connect to ohter replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="stop Queue to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+
+  <mbean         name="SocketSender"
+          description="Sync Cluster Sender"
+               domain="Catalina"
+                group="IDataSender"
+                 type="org.apache.catalina.cluster.tcp.SocketSender">
+    <attribute   name="address"
+          description="sender ip address"
+                 type="java.net.InetAddress"
+                 writeable="false"/>
+    <attribute   name="port"
+          description="sender port"
+                 type="int"
+                 writeable="false" />
+    <attribute   name="suspect"
+          description="Socket is gone"
+                 type="boolean"/>
+    <attribute   name="ackTimeout"
+          description="acknowledge timeout"
+                 type="long"/>
+    <attribute   name="waitForAck"
+          description="Wait for ack after data send"
+                            is="true"
+                 type="boolean"
+                 writeable="false" />
+    <attribute   name="keepAliveTimeout"
+          description="active socket keep alive timeout"
+                 type="long"/>
+    <attribute   name="keepAliveMaxRequestCount"
+          description="max request over this socket"
+                 type="int"/>
+    <attribute   name="messageTransferStarted"
+          description="message is in transfer"
+                 type="boolean"
+                 is="true"
+                 writeable="false"/>
+    <attribute   name="keepAliveCount"
+          description="keep Alive request count"
+                 type="int"
+                 writeable="false"/>
+    <attribute   name="keepAliveConnectTime"
+          description="Connect time for keep alive"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="resend"
+          description="after send failure make a resend"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connected"
+                 is="true"
+          description="socket connected"
+                 type="boolean"
+                 writeable="false"/>
+    <attribute   name="avgMessageSize"
+                 writeable="false"
+          description="avg message size (totalbytes/nrOfRequests"
+                 type="long"/>
+    <attribute   name="nrOfRequests"
+          description="number of send messages to other members"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalBytes"
+          description="number of bytes transfered"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="processingTime"
+          description="sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minProcessingTime"
+          description="minimal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgProcessingTime"
+          description="processing time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxProcessingTime"
+          description="maximal sending processing time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doProcessingStats"
+          description="create Processing time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="waitAckTime"
+          description="sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="minWaitAckTime"
+          description="minimal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="avgWaitAckTime"
+          description="waitAck time / nrOfRequests"
+                 type="double"
+                 writeable="false"/>
+    <attribute   name="maxWaitAckTime"
+          description="maximal sending waitAck time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="doWaitAckStats"
+          description="create waitAck time stats"
+                            is="true"
+                 type="boolean" />
+    <attribute   name="connectCounter"
+          description="counts connects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="disconnectCounter"
+          description="counts disconnects"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketCloseCounter"
+          description="counts closed socket (KeepAlive and disconnects)"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="socketOpenFailureCounter"
+          description="counts open socket failures"
+                 type="long"
+                 writeable="false"/>                                            
+    <attribute   name="socketOpenCounter"
+          description="counts open socket (KeepAlive and connects)"
+                 type="long"
+                 writeable="false"/>                            
+    <attribute   name="missingAckCounter"
+          description="counts missing ack"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataResendCounter"
+          description="counts data resends"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="dataFailureCounter"
+          description="counts data send failures"
+                 type="long"
+                 writeable="false"/>
+       <operation name="connect"
+               description="connect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="disconnect"
+               description="disconnect to other replication node"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+       <operation name="checkKeepAlive"
+               description="Check connection for close socket"
+               impact="ACTION"
+               returnType="boolean">
+    </operation>
+       <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                                
+  </mbean>
+    
+  <mbean         name="ReplicationValve"
+          description="Valve for simple tcp replication"
+               domain="Catalina"
+                group="Valve"
+                 type="org.apache.catalina.cluster.tcp.ReplicationValve">
+    <attribute   name="info"
+          description="Class version info"
+                 type="java.lang.String"
+                 writeable="false"/>
+    <attribute   name="filter"
+          description="resource filter to disable session replication check"
+                 type="java.lang.String"/>
+    <attribute   name="primaryIndicator"
+                            is="true"
+          description="set indicator that request processing is at primary session node"
+                 type="boolean"/>
+    <attribute   name="primaryIndicatorName"
+          description="Request attribute name to indicate that request processing is at primary session node"
+                 type="java.lang.String"/>
+    <attribute   name="doProcessingStats"
+                            is="true"
+          description="active statistics counting"
+                 type="boolean"/>
+       <attribute   name="nrOfRequests"
+          description="number of replicated requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfFilterRequests"
+          description="number of filtered requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfSendRequests"
+          description="number of send requests"
+                 type="long"
+                 writeable="false"/>
+       <attribute   name="nrOfCrossContextSendRequests"
+          description="number of send cross context session requests"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalRequestTime"
+          description="total replicated request time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="totalSendTime"
+          description="total replicated send time"
+                 type="long"
+                 writeable="false"/>
+    <attribute   name="lastSendTime"
+          description="last replicated request time"
+                 type="long"
+                 writeable="false"/>
+    <operation name="resetStatistics"
+               description="Reset all statistics"
+               impact="ACTION"
+               returnType="void">
+    </operation>
+                 
+  </mbean>
+
+
+</mbeans-descriptors>
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
new file mode 100644 (file)
index 0000000..7cf5e28
--- /dev/null
@@ -0,0 +1,382 @@
+/*\r
+ * Copyright 1999,2004-2005 The Apache Software Foundation.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.apache.catalina.tribes.transport.nio;\r
+\r
+import java.io.IOException;\r
+import java.net.ServerSocket;\r
+import java.nio.channels.SelectableChannel;\r
+import java.nio.channels.SelectionKey;\r
+import java.nio.channels.Selector;\r
+import java.nio.channels.ServerSocketChannel;\r
+import java.nio.channels.SocketChannel;\r
+import java.util.Iterator;\r
+\r
+import org.apache.catalina.tribes.ChannelReceiver;\r
+import org.apache.catalina.tribes.io.ListenCallback;\r
+import org.apache.catalina.tribes.io.ObjectReader;\r
+import org.apache.catalina.tribes.transport.Constants;\r
+import org.apache.catalina.tribes.transport.ReceiverBase;\r
+import org.apache.catalina.tribes.transport.ThreadPool;\r
+import org.apache.catalina.tribes.transport.WorkerThread;\r
+import org.apache.catalina.tribes.util.StringManager;\r
+import java.util.LinkedList;\r
+import java.util.Set;\r
+import java.nio.channels.CancelledKeyException;\r
+\r
+/**\r
+ * @author Filip Hanik\r
+ * @version $Revision: 379904 $ $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $\r
+ */\r
+public class NioReceiver extends ReceiverBase implements Runnable, ChannelReceiver, ListenCallback {\r
+\r
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(NioReceiver.class);\r
+\r
+    /**\r
+     * The string manager for this package.\r
+     */\r
+    protected StringManager sm = StringManager.getManager(Constants.Package);\r
+\r
+    /**\r
+     * The descriptive information about this implementation.\r
+     */\r
+    private static final String info = "NioReceiver/1.0";\r
+\r
+    private Selector selector = null;\r
+    private ServerSocketChannel serverChannel = null;\r
+\r
+    protected LinkedList events = new LinkedList();\r
+//    private Object interestOpsMutex = new Object();\r
+\r
+    public NioReceiver() {\r
+    }\r
+\r
+    /**\r
+     * Return descriptive information about this implementation and the\r
+     * corresponding version number, in the format\r
+     * <code>&lt;description&gt;/&lt;version&gt;</code>.\r
+     */\r
+    public String getInfo() {\r
+        return (info);\r
+    }\r
+\r
+//    public Object getInterestOpsMutex() {\r
+//        return interestOpsMutex;\r
+//    }\r
+\r
+    public void stop() {\r
+        this.stopListening();\r
+    }\r
+\r
+    /**\r
+     * start cluster receiver\r
+     * @throws Exception\r
+     * @see org.apache.catalina.tribes.ClusterReceiver#start()\r
+     */\r
+    public void start() throws IOException {\r
+        try {\r
+//            setPool(new ThreadPool(interestOpsMutex, getMaxThreads(),getMinThreads(),this));\r
+            setPool(new ThreadPool(getMaxThreads(),getMinThreads(),this));\r
+        } catch (Exception x) {\r
+            log.fatal("ThreadPool can initilzed. Listener not started", x);\r
+            if ( x instanceof IOException ) throw (IOException)x;\r
+            else throw new IOException(x.getMessage());\r
+        }\r
+        try {\r
+            getBind();\r
+            bind();\r
+            Thread t = new Thread(this, "NioReceiver");\r
+            t.setDaemon(true);\r
+            t.start();\r
+        } catch (Exception x) {\r
+            log.fatal("Unable to start cluster receiver", x);\r
+            if ( x instanceof IOException ) throw (IOException)x;\r
+            else throw new IOException(x.getMessage());\r
+        }\r
+    }\r
+    \r
+    public WorkerThread getWorkerThread() {\r
+        NioReplicationThread thread = new NioReplicationThread(this,this);\r
+        thread.setUseBufferPool(this.getUseBufferPool());\r
+        thread.setRxBufSize(getRxBufSize());\r
+        thread.setOptions(getWorkerThreadOptions());\r
+        return thread;\r
+    }\r
+    \r
+    \r
+    \r
+    protected void bind() throws IOException {\r
+        // allocate an unbound server socket channel\r
+        serverChannel = ServerSocketChannel.open();\r
+        // Get the associated ServerSocket to bind it with\r
+        ServerSocket serverSocket = serverChannel.socket();\r
+        // create a new Selector for use below\r
+        selector = Selector.open();\r
+        // set the port the server channel will listen to\r
+        //serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort()));\r
+        bind(serverSocket,getTcpListenPort(),getAutoBind());\r
+        // set non-blocking mode for the listening socket\r
+        serverChannel.configureBlocking(false);\r
+        // register the ServerSocketChannel with the Selector\r
+        serverChannel.register(selector, SelectionKey.OP_ACCEPT);\r
+        \r
+    }\r
+    \r
+    public void addEvent(Runnable event) {\r
+        if ( selector != null ) {\r
+            synchronized (events) {\r
+                events.add(event);\r
+            }\r
+            if ( log.isTraceEnabled() ) log.trace("Adding event to selector:"+event);\r
+            selector.wakeup();\r
+        }\r
+    }\r
+\r
+    public void events() {\r
+        if ( events.size() == 0 ) return;\r
+        synchronized (events) {\r
+            Runnable r = null;\r
+            while ( (events.size() > 0) && (r = (Runnable)events.removeFirst()) != null ) {\r
+                try {\r
+                    if ( log.isTraceEnabled() ) log.trace("Processing event in selector:"+r);\r
+                    r.run();\r
+                } catch ( Exception x ) {\r
+                    log.error("",x);\r
+                }\r
+            }\r
+            events.clear();\r
+        }\r
+    }\r
+    \r
+    public static void cancelledKey(SelectionKey key) {\r
+        ObjectReader reader = (ObjectReader)key.attachment();\r
+        if ( reader != null ) {\r
+            reader.setCancelled(true);\r
+            reader.finish();\r
+        }\r
+        key.cancel(); \r
+        key.attach(null);\r
+        try { ((SocketChannel)key.channel()).socket().close(); } catch (IOException e) { if (log.isDebugEnabled()) log.debug("", e); }\r
+        try { key.channel().close(); } catch (IOException e) { if (log.isDebugEnabled()) log.debug("", e); }\r
+        \r
+    }\r
+    \r
+    protected void socketTimeouts() {\r
+        //timeout\r
+        Set keys = selector.keys();\r
+        long now = System.currentTimeMillis();\r
+        for (Iterator iter = keys.iterator(); iter.hasNext(); ) {\r
+            SelectionKey key = (SelectionKey) iter.next();\r
+            try {\r
+//                if (key.interestOps() == SelectionKey.OP_READ) {\r
+//                    //only timeout sockets that we are waiting for a read from\r
+//                    ObjectReader ka = (ObjectReader) key.attachment();\r
+//                    long delta = now - ka.getLastAccess();\r
+//                    if (delta > (long) getTimeout()) {\r
+//                        cancelledKey(key);\r
+//                    }\r
+//                }\r
+//                else\r
+                if ( key.interestOps() == 0 ) {\r
+                    //check for keys that didn't make it in.\r
+                    ObjectReader ka = (ObjectReader) key.attachment();\r
+                    if ( ka != null ) {\r
+                        long delta = now - ka.getLastAccess();\r
+                        if (delta > (long) getTimeout() && (!ka.isAccessed())) {\r
+                            log.warn("Channel key is registered, but has had no interest ops for the last "+getTimeout()+" ms. (cancelled:"+ka.isCancelled()+"):"+key+" last access:"+new java.sql.Timestamp(ka.getLastAccess()));\r
+//                            System.out.println("Interest:"+key.interestOps());\r
+//                            System.out.println("Ready Ops:"+key.readyOps());\r
+//                            System.out.println("Valid:"+key.isValid());\r
+                            ka.setLastAccess(now);\r
+                            //key.interestOps(SelectionKey.OP_READ);\r
+                        }//end if\r
+                    } else {\r
+                        cancelledKey(key);\r
+                    }//end if\r
+                }//end if\r
+            }catch ( CancelledKeyException ckx ) {\r
+                cancelledKey(key);\r
+            }\r
+        }\r
+    }\r
+\r
+\r
+    /**\r
+     * get data from channel and store in byte array\r
+     * send it to cluster\r
+     * @throws IOException\r
+     * @throws java.nio.channels.ClosedChannelException\r
+     */\r
+    protected void listen() throws Exception {\r
+        if (doListen()) {\r
+            log.warn("ServerSocketChannel already started");\r
+            return;\r
+        }\r
+        \r
+        setListen(true);\r
+\r
+        while (doListen() && selector != null) {\r
+            // this may block for a long time, upon return the\r
+            // selected set contains keys of the ready channels\r
+            try {\r
+                events();\r
+                socketTimeouts();\r
+                int n = selector.select(getTcpSelectorTimeout());\r
+                if (n == 0) {\r
+                    //there is a good chance that we got here\r
+                    //because the TcpReplicationThread called\r
+                    //selector wakeup().\r
+                    //if that happens, we must ensure that that\r
+                    //thread has enough time to call interestOps\r
+//                    synchronized (interestOpsMutex) {\r
+                        //if we got the lock, means there are no\r
+                        //keys trying to register for the\r
+                        //interestOps method\r
+//                    }\r
+                    continue; // nothing to do\r
+                }\r
+                // get an iterator over the set of selected keys\r
+                Iterator it = selector.selectedKeys().iterator();\r
+                // look at each key in the selected set\r
+                while (it.hasNext()) {\r
+                    SelectionKey key = (SelectionKey) it.next();\r
+                    // Is a new connection coming in?\r
+                    if (key.isAcceptable()) {\r
+                        ServerSocketChannel server = (ServerSocketChannel) key.channel();\r
+                        SocketChannel channel = server.accept();\r
+                        channel.socket().setReceiveBufferSize(getRxBufSize());\r
+                        channel.socket().setSendBufferSize(getTxBufSize());\r
+                        channel.socket().setTcpNoDelay(getTcpNoDelay());\r
+                        channel.socket().setKeepAlive(getSoKeepAlive());\r
+                        channel.socket().setOOBInline(getOoBInline());\r
+                        channel.socket().setReuseAddress(getSoReuseAddress());\r
+                        channel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime());\r
+                        channel.socket().setTrafficClass(getSoTrafficClass());\r
+                        channel.socket().setSoTimeout(getTimeout());\r
+                        Object attach = new ObjectReader(channel);\r
+                        registerChannel(selector,\r
+                                        channel,\r
+                                        SelectionKey.OP_READ,\r
+                                        attach);\r
+                    }\r
+                    // is there data to read on this channel?\r
+                    if (key.isReadable()) {\r
+                        readDataFromSocket(key);\r
+                    } else {\r
+                        key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE));\r
+                    }\r
+\r
+                    // remove key from selected set, it's been handled\r
+                    it.remove();\r
+                }\r
+            } catch (java.nio.channels.ClosedSelectorException cse) {\r
+                // ignore is normal at shutdown or stop listen socket\r
+            } catch (java.nio.channels.CancelledKeyException nx) {\r
+                log.warn("Replication client disconnected, error when polling key. Ignoring client.");\r
+            } catch (Throwable x) {\r
+                try {\r
+                    log.error("Unable to process request in NioReceiver", x);\r
+                }catch ( Throwable tx ) {\r
+                    //in case an out of memory error, will affect the logging framework as well\r
+                    tx.printStackTrace();\r
+                }\r
+            }\r
+\r
+        }\r
+        serverChannel.close();\r
+        if (selector != null)\r
+            selector.close();\r
+    }\r
+\r
+    \r
+\r
+    /**\r
+     * Close Selector.\r
+     *\r
+     * @see org.apache.catalina.tribes.transport.ClusterReceiverBase#stopListening()\r
+     */\r
+    protected void stopListening() {\r
+        // Bugzilla 37529: http://issues.apache.org/bugzilla/show_bug.cgi?id=37529\r
+        setListen(false);\r
+        if (selector != null) {\r
+            try {\r
+                for (int i = 0; i < getMaxThreads(); i++) {\r
+                    selector.wakeup();\r
+                }\r
+                selector.close();\r
+            } catch (Exception x) {\r
+                log.error("Unable to close cluster receiver selector.", x);\r
+            } finally {\r
+                selector = null;\r
+            }\r
+        }\r
+    }\r
+\r
+    // ----------------------------------------------------------\r
+\r
+    /**\r
+     * Register the given channel with the given selector for\r
+     * the given operations of interest\r
+     */\r
+    protected void registerChannel(Selector selector,\r
+                                   SelectableChannel channel,\r
+                                   int ops,\r
+                                   Object attach) throws Exception {\r
+        if (channel == null)return; // could happen\r
+        // set the new channel non-blocking\r
+        channel.configureBlocking(false);\r
+        // register it with the selector\r
+        channel.register(selector, ops, attach);\r
+    }\r
+\r
+    /**\r
+     * Start thread and listen\r
+     */\r
+    public void run() {\r
+        try {\r
+            listen();\r
+        } catch (Exception x) {\r
+            log.error("Unable to run replication listener.", x);\r
+        }\r
+    }\r
+\r
+    // ----------------------------------------------------------\r
+\r
+    /**\r
+     * Sample data handler method for a channel with data ready to read.\r
+     * @param key A SelectionKey object associated with a channel\r
+     *  determined by the selector to be ready for reading.  If the\r
+     *  channel returns an EOF condition, it is closed here, which\r
+     *  automatically invalidates the associated key.  The selector\r
+     *  will then de-register the channel on the next select call.\r
+     */\r
+    protected void readDataFromSocket(SelectionKey key) throws Exception {\r
+        NioReplicationThread worker = (NioReplicationThread) getPool().getWorker();\r
+        if (worker == null) {\r
+            // No threads available, do nothing, the selection\r
+            // loop will keep calling this method until a\r
+            // thread becomes available, the thread pool itself has a waiting mechanism\r
+            // so we will not wait here.\r
+            if (log.isDebugEnabled())\r
+                log.debug("No TcpReplicationThread available");\r
+        } else {\r
+            // invoking this wakes up the worker thread then returns\r
+            worker.serviceChannel(key);\r
+        }\r
+    }\r
+\r
+\r
+}\r
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java b/java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java
new file mode 100644 (file)
index 0000000..d19f131
--- /dev/null
@@ -0,0 +1,310 @@
+/*\r
+ * Copyright 1999,2004 The Apache Software Foundation.\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ * \r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.apache.catalina.tribes.transport.nio;\r
+import java.io.IOException;\r
+import java.nio.ByteBuffer;\r
+import java.nio.channels.SelectionKey;\r
+import java.nio.channels.SocketChannel;\r
+\r
+import org.apache.catalina.tribes.io.ObjectReader;\r
+import org.apache.catalina.tribes.transport.Constants;\r
+import org.apache.catalina.tribes.transport.WorkerThread;\r
+import org.apache.catalina.tribes.ChannelMessage;\r
+import org.apache.catalina.tribes.io.ListenCallback;\r
+import org.apache.catalina.tribes.io.ChannelData;\r
+import org.apache.catalina.tribes.io.BufferPool;\r
+import java.nio.channels.CancelledKeyException;\r
+import org.apache.catalina.tribes.UniqueId;\r
+import org.apache.catalina.tribes.RemoteProcessException;\r
+import org.apache.catalina.tribes.util.Logs;\r
+\r
+/**\r
+ * A worker thread class which can drain channels and echo-back the input. Each\r
+ * instance is constructed with a reference to the owning thread pool object.\r
+ * When started, the thread loops forever waiting to be awakened to service the\r
+ * channel associated with a SelectionKey object. The worker is tasked by\r
+ * calling its serviceChannel() method with a SelectionKey object. The\r
+ * serviceChannel() method stores the key reference in the thread object then\r
+ * calls notify() to wake it up. When the channel has been drained, the worker\r
+ * thread returns itself to its parent pool.\r
+ * \r
+ * @author Filip Hanik\r
+ * \r
+ * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $\r
+ */\r
+public class NioReplicationThread extends WorkerThread {\r
+    \r
+    private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( NioReplicationThread.class );\r
+    \r
+    private ByteBuffer buffer = null;\r
+    private SelectionKey key;\r
+    private int rxBufSize;\r
+    private NioReceiver receiver;\r
+    public NioReplicationThread (ListenCallback callback, NioReceiver receiver)\r
+    {\r
+        super(callback);\r
+        this.receiver = receiver;\r
+    }\r
+\r
+    // loop forever waiting for work to do\r
+    public synchronized void run() { \r
+        this.notify();\r
+        if ( (getOptions() & OPTION_DIRECT_BUFFER) == OPTION_DIRECT_BUFFER ) {\r
+            buffer = ByteBuffer.allocateDirect(getRxBufSize());\r
+        }else {\r
+            buffer = ByteBuffer.allocate (getRxBufSize());\r
+        }\r
+        while (isDoRun()) {\r
+            try {\r
+                // sleep and release object lock\r
+                this.wait();\r
+            } catch (InterruptedException e) {\r
+                if(log.isInfoEnabled()) log.info("TCP worker thread interrupted in cluster",e);\r
+                // clear interrupt status\r
+                Thread.interrupted();\r
+            }\r
+            if (key == null) {\r
+                continue;      // just in case\r
+            }\r
+            if ( log.isTraceEnabled() ) \r
+                log.trace("Servicing key:"+key);\r
+\r
+            try {\r
+                ObjectReader reader = (ObjectReader)key.attachment();\r
+                if ( reader == null ) {\r
+                    if ( log.isTraceEnabled() ) \r
+                        log.trace("No object reader, cancelling:"+key);\r
+                    cancelKey(key);\r
+                } else {\r
+                    if ( log.isTraceEnabled() ) \r
+                        log.trace("Draining channel:"+key);\r
+\r
+                    drainChannel(key, reader);\r
+                }\r
+            } catch (Exception e) {\r
+                //this is common, since the sockets on the other\r
+                //end expire after a certain time.\r
+                if ( e instanceof CancelledKeyException ) {\r
+                    //do nothing\r
+                } else if ( e instanceof IOException ) {\r
+                    //dont spew out stack traces for IO exceptions unless debug is enabled.\r
+                    if (log.isDebugEnabled()) log.debug ("IOException in replication worker, unable to drain channel. Probable cause: Keep alive socket closed["+e.getMessage()+"].", e);\r
+                    else log.warn ("IOException in replication worker, unable to drain channel. Probable cause: Keep alive socket closed["+e.getMessage()+"].");\r
+                } else if ( log.isErrorEnabled() ) {\r
+                    //this is a real error, log it.\r
+                    log.error("Exception caught in TcpReplicationThread.drainChannel.",e);\r
+                } \r
+                cancelKey(key);\r
+            } finally {\r
+                \r
+            }\r
+            key = null;\r
+            // done, ready for more, return to pool\r
+            getPool().returnWorker (this);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Called to initiate a unit of work by this worker thread\r
+     * on the provided SelectionKey object.  This method is\r
+     * synchronized, as is the run() method, so only one key\r
+     * can be serviced at a given time.\r
+     * Before waking the worker thread, and before returning\r
+     * to the main selection loop, this key's interest set is\r
+     * updated to remove OP_READ.  This will cause the selector\r
+     * to ignore read-readiness for this channel while the\r
+     * worker thread is servicing it.\r
+     */\r
+    public synchronized void serviceChannel (SelectionKey key) {\r
+        if ( log.isTraceEnabled() ) \r
+            log.trace("About to service key:"+key);\r
+        ObjectReader reader = (ObjectReader)key.attachment();\r
+        if ( reader != null ) reader.setLastAccess(System.currentTimeMillis());\r
+        this.key = key;\r
+        key.interestOps (key.interestOps() & (~SelectionKey.OP_READ));\r
+        key.interestOps (key.interestOps() & (~SelectionKey.OP_WRITE));\r
+        this.notify();         // awaken the thread\r
+    }\r
+\r
+    /**\r
+     * The actual code which drains the channel associated with\r
+     * the given key.  This method assumes the key has been\r
+     * modified prior to invocation to turn off selection\r
+     * interest in OP_READ.  When this method completes it\r
+     * re-enables OP_READ and calls wakeup() on the selector\r
+     * so the selector will resume watching this channel.\r
+     */\r
+    protected void drainChannel (final SelectionKey key, ObjectReader reader) throws Exception {\r
+        reader.setLastAccess(System.currentTimeMillis());\r
+        reader.access();\r
+        SocketChannel channel = (SocketChannel) key.channel();\r
+        int count;\r
+        buffer.clear();                        // make buffer empty\r
+\r
+        // loop while data available, channel is non-blocking\r
+        while ((count = channel.read (buffer)) > 0) {\r
+            buffer.flip();             // make buffer readable\r
+            if ( buffer.hasArray() ) \r
+                reader.append(buffer.array(),0,count,false);\r
+            else \r
+                reader.append(buffer,count,false);\r
+            buffer.clear();            // make buffer empty\r
+            //do we have at least one package?\r
+            if ( reader.hasPackage() ) break;\r
+        }\r
+\r
+        int pkgcnt = reader.count();\r
+        \r
+        if (count < 0 && pkgcnt == 0 ) {\r
+            //end of stream, and no more packages to process\r
+            remoteEof(key);\r
+            return;\r
+        }\r
+        \r
+        ChannelMessage[] msgs = pkgcnt == 0? ChannelData.EMPTY_DATA_ARRAY : reader.execute();\r
+        \r
+        registerForRead(key,reader);//register to read new data, before we send it off to avoid dead locks\r
+        \r
+        for ( int i=0; i<msgs.length; i++ ) {\r
+            /**\r
+             * Use send ack here if you want to ack the request to the remote \r
+             * server before completing the request\r
+             * This is considered an asynchronized request\r
+             */\r
+            if (ChannelData.sendAckAsync(msgs[i].getOptions())) sendAck(key,channel,Constants.ACK_COMMAND);\r
+            try {\r
+                if ( Logs.MESSAGES.isTraceEnabled() ) {\r
+                    try {\r
+                        Logs.MESSAGES.trace("NioReplicationThread - Received msg:" + new UniqueId(msgs[i].getUniqueId()) + " at " + new java.sql.Timestamp(System.currentTimeMillis()));\r
+                    }catch ( Throwable t ) {}\r
+                }\r
+                //process the message\r
+                getCallback().messageDataReceived(msgs[i]);\r
+                /**\r
+                 * Use send ack here if you want the request to complete on this \r
+                 * server before sending the ack to the remote server\r
+                 * This is considered a synchronized request\r
+                 */\r
+                if (ChannelData.sendAckSync(msgs[i].getOptions())) sendAck(key,channel,Constants.ACK_COMMAND);\r
+            }catch ( RemoteProcessException e ) {\r
+                if ( log.isDebugEnabled() ) log.error("Processing of cluster message failed.",e);\r
+                if (ChannelData.sendAckSync(msgs[i].getOptions())) sendAck(key,channel,Constants.FAIL_ACK_COMMAND);\r
+            }catch ( Exception e ) {\r
+                log.error("Processing of cluster message failed.",e);\r
+                if (ChannelData.sendAckSync(msgs[i].getOptions())) sendAck(key,channel,Constants.FAIL_ACK_COMMAND);\r
+            }\r
+            if ( getUseBufferPool() ) {\r
+                BufferPool.getBufferPool().returnBuffer(msgs[i].getMessage());\r
+                msgs[i].setMessage(null);\r
+            }\r
+        }                        \r
+        \r
+        if (count < 0) {\r
+            remoteEof(key);\r
+            return;\r
+        }\r
+    }\r
+\r
+    private void remoteEof(SelectionKey key) {\r
+        // close channel on EOF, invalidates the key\r
+        if ( log.isDebugEnabled() ) log.debug("Channel closed on the remote end, disconnecting");\r
+        cancelKey(key);\r
+    }\r
+\r
+    protected void registerForRead(final SelectionKey key, ObjectReader reader) {\r
+        if ( log.isTraceEnabled() ) \r
+            log.trace("Adding key for read event:"+key);\r
+        reader.finish();\r
+        //register our OP_READ interest\r
+        Runnable r = new Runnable() {\r
+            public void run() {\r
+                try {\r
+                    if (key.isValid()) {\r
+                        // cycle the selector so this key is active again\r
+                        key.selector().wakeup();\r
+                        // resume interest in OP_READ, OP_WRITE\r
+                        int resumeOps = key.interestOps() | SelectionKey.OP_READ;\r
+                        key.interestOps(resumeOps);\r
+                        if ( log.isTraceEnabled() ) \r
+                            log.trace("Registering key for read:"+key);\r
+                    }\r
+                } catch (CancelledKeyException ckx ) {\r
+                    NioReceiver.cancelledKey(key);\r
+                    if ( log.isTraceEnabled() ) \r
+                        log.trace("CKX Cancelling key:"+key);\r
+\r
+                } catch (Exception x) {\r
+                    log.error("Error registering key for read:"+key,x);\r
+                }\r
+            }\r
+        };\r
+        receiver.addEvent(r);\r
+    }\r
+\r
+    private void cancelKey(final SelectionKey key) {\r
+        if ( log.isTraceEnabled() ) \r
+            log.trace("Adding key for cancel event:"+key);\r
+\r
+        ObjectReader reader = (ObjectReader)key.attachment();\r
+        if ( reader != null ) {\r
+            reader.setCancelled(true);\r
+            reader.finish();\r
+        }\r
+        Runnable cx = new Runnable() {\r
+            public void run() {\r
+                if ( log.isTraceEnabled() ) \r
+                    log.trace("Cancelling key:"+key);\r
+\r
+                NioReceiver.cancelledKey(key);\r
+            }\r
+        };\r
+        receiver.addEvent(cx);\r
+    }\r
+    \r
+    \r
+\r
+\r
+\r
+    /**\r
+     * send a reply-acknowledgement (6,2,3)\r
+     * @param key\r
+     * @param channel\r
+     */\r
+    protected void sendAck(SelectionKey key, SocketChannel channel, byte[] command) {\r
+        \r
+        try {\r
+            ByteBuffer buf = ByteBuffer.wrap(command);\r
+            int total = 0;\r
+            while ( total < command.length ) {\r
+                total += channel.write(buf);\r
+            }\r
+            if (log.isTraceEnabled()) {\r
+                log.trace("ACK sent to " + channel.socket().getPort());\r
+            }\r
+        } catch ( java.io.IOException x ) {\r
+            log.warn("Unable to send ACK back through channel, channel disconnected?: "+x.getMessage());\r
+        }\r
+    }\r
+\r
+    public void setRxBufSize(int rxBufSize) {\r
+        this.rxBufSize = rxBufSize;\r
+    }\r
+\r
+    public int getRxBufSize() {\r
+        return rxBufSize;\r
+    }\r
+}\r
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioSender.java b/java/org/apache/catalina/tribes/transport/nio/NioSender.java
new file mode 100644 (file)
index 0000000..906b8a2
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.tribes.transport.nio;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.Arrays;
+
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.transport.AbstractSender;
+import org.apache.catalina.tribes.transport.DataSender;
+import org.apache.catalina.tribes.RemoteProcessException;
+
+/**
+ * This class is NOT thread safe and should never be used with more than one thread at a time
+ * 
+ * This is a state machine, handled by the process method
+ * States are:
+ * - NOT_CONNECTED -> connect() -> CONNECTED
+ * - CONNECTED -> setMessage() -> READY TO WRITE
+ * - READY_TO_WRITE -> write() -> READY TO WRITE | READY TO READ
+ * - READY_TO_READ -> read() -> READY_TO_READ | TRANSFER_COMPLETE
+ * - TRANSFER_COMPLETE -> CONNECTED
+ * 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class NioSender extends AbstractSender implements DataSender{
+
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(NioSender.class);
+
+    
+    
+    protected Selector selector;    
+    protected SocketChannel socketChannel;
+
+    /*
+     * STATE VARIABLES *
+     */
+    protected ByteBuffer readbuf = null;
+    protected ByteBuffer writebuf = null;
+    protected byte[] current = null;
+    protected XByteBuffer ackbuf = new XByteBuffer(128,true);
+    protected int remaining = 0;
+    protected boolean complete;
+    
+    protected boolean connecting = false;
+    
+    public NioSender() {
+        super();
+        
+    }
+    
+    /**
+     * State machine to send data
+     * @param key SelectionKey
+     * @return boolean
+     * @throws IOException
+     */
+    public boolean process(SelectionKey key, boolean waitForAck) throws IOException {
+        int ops = key.readyOps();
+        key.interestOps(key.interestOps() & ~ops);
+        //in case disconnect has been called
+        if ((!isConnected()) && (!connecting)) throw new IOException("Sender has been disconnected, can't selection key.");
+        if ( !key.isValid() ) throw new IOException("Key is not valid, it must have been cancelled.");
+        if ( key.isConnectable() ) {
+            if ( socketChannel.finishConnect() ) {
+                //we connected, register ourselves for writing
+                setConnected(true);
+                connecting = false;
+                setRequestCount(0);
+                setConnectTime(System.currentTimeMillis());
+                socketChannel.socket().setSendBufferSize(getTxBufSize());
+                socketChannel.socket().setReceiveBufferSize(getRxBufSize());
+                socketChannel.socket().setSoTimeout((int)getTimeout());
+                socketChannel.socket().setSoLinger(false,0);
+                socketChannel.socket().setTcpNoDelay(getTcpNoDelay());
+                socketChannel.socket().setKeepAlive(getSoKeepAlive());
+                socketChannel.socket().setReuseAddress(getSoReuseAddress());
+                socketChannel.socket().setOOBInline(getOoBInline());
+                socketChannel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime());
+                socketChannel.socket().setTrafficClass(getSoTrafficClass());
+                if ( current != null ) key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
+                return false;
+            } else  { 
+                //wait for the connection to finish
+                key.interestOps(key.interestOps() | SelectionKey.OP_CONNECT);
+                return false;
+            }//end if
+        } else if ( key.isWritable() ) {
+            boolean writecomplete = write(key);
+            if ( writecomplete ) {
+                //we are completed, should we read an ack?
+                if ( waitForAck ) {
+                    //register to read the ack
+                    key.interestOps(key.interestOps() | SelectionKey.OP_READ);
+                } else {
+                    //if not, we are ready, setMessage will reregister us for another write interest
+                    //do a health check, we have no way of verify a disconnected
+                    //socket since we don't register for OP_READ on waitForAck=false
+                    read(key);//this causes overhead
+                    setRequestCount(getRequestCount()+1);
+                    return true;
+                }
+            } else {
+                //we are not complete, lets write some more
+                key.interestOps(key.interestOps()|SelectionKey.OP_WRITE);
+            }//end if
+        } else if ( key.isReadable() ) {
+            boolean readcomplete = read(key);
+            if ( readcomplete ) {
+                setRequestCount(getRequestCount()+1);
+                return true;
+            } else {
+                key.interestOps(key.interestOps() | SelectionKey.OP_READ);
+            }//end if
+        } else {
+            //unknown state, should never happen
+            log.warn("Data is in unknown state. readyOps="+ops);
+            throw new IOException("Data is in unknown state. readyOps="+ops);
+        }//end if
+        return false;
+    }
+    
+    
+
+    protected boolean read(SelectionKey key) throws IOException {
+        //if there is no message here, we are done
+        if ( current == null ) return true;
+        int read = socketChannel.read(readbuf);
+        //end of stream
+        if ( read == -1 ) throw new IOException("Unable to receive an ack message. EOF on socket channel has been reached.");
+        //no data read
+        else if ( read == 0 ) return false;
+        readbuf.flip();
+        ackbuf.append(readbuf,read);
+        readbuf.clear();
+        if (ackbuf.doesPackageExist() ) {
+            byte[] ackcmd = ackbuf.extractDataPackage(true).getBytes();
+            boolean ack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.ACK_DATA);
+            boolean fack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA);
+            if ( fack && getThrowOnFailedAck() ) throw new RemoteProcessException("Received a failed ack:org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA");
+            return ack || fack;
+        } else {
+            return false;
+        }
+    }
+
+    
+    protected boolean write(SelectionKey key) throws IOException {
+        if ( (!isConnected()) || (this.socketChannel==null)) {
+            throw new IOException("NioSender is not connected, this should not occur.");
+        }
+        if ( current != null ) {
+            if ( remaining > 0 ) {
+                //weve written everything, or we are starting a new package
+                //protect against buffer overwrite
+                int byteswritten = socketChannel.write(writebuf);
+                remaining -= byteswritten;
+                //if the entire message was written from the buffer
+                //reset the position counter
+                if ( remaining < 0 ) {
+                    remaining = 0;
+                }
+            }
+            return (remaining==0);
+        }
+        //no message to send, we can consider that complete
+        return true;
+    }
+
+    /**
+     * connect - blocking in this operation
+     *
+     * @throws IOException
+     * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method
+     */
+    public synchronized void connect() throws IOException {
+        if ( connecting ) return;
+        connecting = true;
+        if ( isConnected() ) throw new IOException("NioSender is already in connected state.");
+        if ( readbuf == null ) {
+            readbuf = getReadBuffer();
+        } else {
+            readbuf.clear();
+        }
+        if ( writebuf == null ) {
+            writebuf = getWriteBuffer();
+        } else {
+            writebuf.clear();
+        }
+        
+        InetSocketAddress addr = new InetSocketAddress(getAddress(),getPort());
+        if ( socketChannel != null ) throw new IOException("Socket channel has already been established. Connection might be in progress.");
+        socketChannel = SocketChannel.open();
+        socketChannel.configureBlocking(false);
+        socketChannel.connect(addr);
+        socketChannel.register(getSelector(),SelectionKey.OP_CONNECT,this);
+    }
+    
+
+    /**
+     * disconnect
+     *
+     * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method
+     */
+    public void disconnect() {
+        try {
+            connecting = false;
+            setConnected(false);
+            if ( socketChannel != null ) {
+                try {
+                    try {socketChannel.socket().close();}catch ( Exception x){}
+                    //error free close, all the way
+                    //try {socket.shutdownOutput();}catch ( Exception x){}
+                    //try {socket.shutdownInput();}catch ( Exception x){}
+                    //try {socket.close();}catch ( Exception x){}
+                    try {socketChannel.close();}catch ( Exception x){}
+                }finally {
+                    socketChannel = null;
+                }
+            }
+        } catch ( Exception x ) {
+            log.error("Unable to disconnect NioSender. msg="+x.getMessage());
+            if ( log.isDebugEnabled() ) log.debug("Unable to disconnect NioSender. msg="+x.getMessage(),x);
+        } finally {
+        }
+
+    }
+    
+    public void reset() {
+        if ( isConnected() && readbuf == null) {
+            readbuf = getReadBuffer();
+        }
+        if ( readbuf != null ) readbuf.clear();
+        if ( writebuf != null ) writebuf.clear();
+        current = null;
+        ackbuf.clear();
+        remaining = 0;
+        complete = false;
+        setAttempt(0);
+        setRequestCount(0);
+        setConnectTime(-1);
+    }
+
+    private ByteBuffer getReadBuffer() { 
+        return getBuffer(getRxBufSize());
+    }
+    
+    private ByteBuffer getWriteBuffer() {
+        return getBuffer(getTxBufSize());
+    }
+
+    private ByteBuffer getBuffer(int size) {
+        return (getDirectBuffer()?ByteBuffer.allocateDirect(size):ByteBuffer.allocate(size));
+    }
+    
+    /**
+    * sendMessage
+    *
+    * @param data ChannelMessage
+    * @throws IOException
+    * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method
+    */
+   public synchronized void setMessage(byte[] data) throws IOException {
+       setMessage(data,0,data.length);
+   }
+
+   public synchronized void setMessage(byte[] data,int offset, int length) throws IOException {
+       if ( data != null ) {
+           current = data;
+           remaining = length;
+           ackbuf.clear();
+           if ( writebuf != null ) writebuf.clear();
+           else writebuf = getBuffer(length);
+           if ( writebuf.capacity() < length ) writebuf = getBuffer(length);
+           writebuf.put(data,offset,length);
+           //writebuf.rewind();
+           //set the limit so that we don't write non wanted data
+           //writebuf.limit(length);
+           writebuf.flip();
+           if (isConnected()) {
+               socketChannel.register(getSelector(), SelectionKey.OP_WRITE, this);
+           }
+       } 
+   }
+   
+   public byte[] getMessage() {
+       return current;
+   }
+
+
+
+    public boolean isComplete() {
+        return complete;
+    }
+
+    public Selector getSelector() {
+        return selector;
+    }
+
+    public void setSelector(Selector selector) {
+        this.selector = selector;
+    }
+
+
+    public void setComplete(boolean complete) {
+        this.complete = complete;
+    }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java b/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java
new file mode 100644 (file)
index 0000000..5e92c5e
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.transport.nio;
+
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.transport.MultiPointSender;
+import org.apache.catalina.tribes.transport.SenderState;
+import org.apache.catalina.tribes.transport.AbstractSender;
+import java.net.UnknownHostException;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.group.RpcChannel;
+import org.apache.catalina.tribes.util.Logs;
+import org.apache.catalina.tribes.UniqueId;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class ParallelNioSender extends AbstractSender implements MultiPointSender {
+    
+    protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ParallelNioSender.class);
+    protected long selectTimeout = 5000; //default 5 seconds, same as send timeout
+    protected Selector selector;
+    protected HashMap nioSenders = new HashMap();
+
+    public ParallelNioSender() throws IOException {
+        selector = Selector.open();
+        setConnected(true);
+    }
+    
+    
+    public synchronized void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException {
+        long start = System.currentTimeMillis();
+        byte[] data = XByteBuffer.createDataPackage((ChannelData)msg);
+        NioSender[] senders = setupForSend(destination);
+        connect(senders);
+        setData(senders,data);
+        
+        int remaining = senders.length;
+        ChannelException cx = null;
+        try {
+            //loop until complete, an error happens, or we timeout
+            long delta = System.currentTimeMillis() - start;
+            boolean waitForAck = (Channel.SEND_OPTIONS_USE_ACK & msg.getOptions()) == Channel.SEND_OPTIONS_USE_ACK;
+            while ( (remaining>0) && (delta<getTimeout()) ) {
+                try {
+                    remaining -= doLoop(selectTimeout, getMaxRetryAttempts(),waitForAck,msg);
+                } catch (Exception x ) {
+                    if ( cx == null ) {
+                        if ( x instanceof ChannelException ) cx = (ChannelException)x;
+                        else cx = new ChannelException("Parallel NIO send failed.", x);
+                    } else {
+                        if (x instanceof ChannelException) cx.addFaultyMember( ( (ChannelException) x).getFaultyMembers());
+                    }
+                }
+                //bail out if all remaining senders are failing
+                if ( cx != null && cx.getFaultyMembers().length == remaining ) throw cx;
+                delta = System.currentTimeMillis() - start;
+            }
+            if ( remaining > 0 ) {
+                //timeout has occured
+                cx = new ChannelException("Operation has timed out("+getTimeout()+" ms.).");
+                for (int i=0; i<senders.length; i++ ) {
+                    if (!senders[i].isComplete() ) cx.addFaultyMember(senders[i].getDestination(),null);
+                }
+                throw cx;
+            }
+        } catch (Exception x ) {
+            try { this.disconnect(); } catch (Exception ignore) {}
+            if ( x instanceof ChannelException ) throw (ChannelException)x;
+            else throw new ChannelException(x);
+        }
+        
+    }
+    
+    private int doLoop(long selectTimeOut, int maxAttempts, boolean waitForAck, ChannelMessage msg) throws IOException, ChannelException {
+        int completed = 0;
+        int selectedKeys = selector.select(selectTimeOut);
+        
+        if (selectedKeys == 0) {
+            return 0;
+        }
+        
+        Iterator it = selector.selectedKeys().iterator();
+        while (it.hasNext()) {
+            SelectionKey sk = (SelectionKey) it.next();
+            it.remove();
+            int readyOps = sk.readyOps();
+            sk.interestOps(sk.interestOps() & ~readyOps);
+            NioSender sender = (NioSender) sk.attachment();
+            try {
+                if (sender.process(sk,waitForAck)) {
+                    completed++;
+                    sender.setComplete(true);
+                    if ( Logs.MESSAGES.isTraceEnabled() ) {
+                        Logs.MESSAGES.trace("ParallelNioSender - Sent msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " to "+sender.getDestination().getName());
+                    }
+                    SenderState.getSenderState(sender.getDestination()).setReady();
+                }//end if
+            } catch (Exception x) {
+                SenderState state = SenderState.getSenderState(sender.getDestination());
+                int attempt = sender.getAttempt()+1;
+                boolean retry = (sender.getAttempt() <= maxAttempts && maxAttempts>0);
+                synchronized (state) {
+                
+                    //sk.cancel();
+                    if (state.isSuspect()) state.setFailing();
+                    if (state.isReady()) {
+                        state.setSuspect();
+                        if ( retry ) 
+                            log.warn("Member send is failing for:" + sender.getDestination().getName() +" ; Setting to suspect and retrying.");
+                        else 
+                            log.warn("Member send is failing for:" + sender.getDestination().getName() +" ; Setting to suspect.", x);
+                    }                    
+                }
+                if ( !isConnected() ) {
+                    log.warn("Not retrying send for:" + sender.getDestination().getName() + "; Sender is disconnected.");
+                    ChannelException cx = new ChannelException("Send failed, and sender is disconnected. Not retrying.",x);
+                    cx.addFaultyMember(sender.getDestination(),x);
+                    throw cx;
+                }
+                
+                byte[] data = sender.getMessage();
+                if ( retry ) {
+                    try { 
+                        sender.disconnect(); 
+                        sender.connect();
+                        sender.setAttempt(attempt);
+                        sender.setMessage(data);
+                    }catch ( Exception ignore){
+                        state.setFailing();
+                    }
+                } else {
+                    ChannelException cx = new ChannelException("Send failed, attempt:"+sender.getAttempt()+" max:"+maxAttempts,x);
+                    cx.addFaultyMember(sender.getDestination(),x);
+                    throw cx;
+                }//end if
+            }
+        }
+        return completed;
+
+    }
+    
+    private void connect(NioSender[] senders) throws ChannelException {
+        ChannelException x = null;
+        for (int i=0; i<senders.length; i++ ) {
+            try {
+                if (!senders[i].isConnected()) senders[i].connect();
+            }catch ( IOException io ) {
+                if ( x==null ) x = new ChannelException(io);
+                x.addFaultyMember(senders[i].getDestination(),io);
+            }
+        }
+        if ( x != null ) throw x;
+    }
+    
+    private void setData(NioSender[] senders, byte[] data) throws ChannelException {
+        ChannelException x = null;
+        for (int i=0; i<senders.length; i++ ) {
+            try {
+                senders[i].setMessage(data);
+            }catch ( IOException io ) {
+                if ( x==null ) x = new ChannelException(io);
+                x.addFaultyMember(senders[i].getDestination(),io);
+            }
+        }
+        if ( x != null ) throw x;
+    }
+    
+    
+    private NioSender[] setupForSend(Member[] destination) throws ChannelException {
+        ChannelException cx = null;
+        NioSender[] result = new NioSender[destination.length];
+        for ( int i=0; i<destination.length; i++ ) {
+            NioSender sender = (NioSender)nioSenders.get(destination[i]);
+            try {
+
+                if (sender == null) {
+                    sender = new NioSender();
+                    sender.transferProperties(this, sender);
+                    nioSenders.put(destination[i], sender);
+                }
+                if (sender != null) {
+                    sender.reset();
+                    sender.setDestination(destination[i]);
+                    sender.setSelector(selector);
+                    result[i] = sender;
+                }
+            }catch ( UnknownHostException x ) {
+                if (cx == null) cx = new ChannelException("Unable to setup NioSender.", x);
+                cx.addFaultyMember(destination[i], x);
+            }
+        }
+        if ( cx != null ) throw cx;
+        else return result;
+    }
+    
+    public void connect() {
+        //do nothing, we connect on demand
+        setConnected(true);
+    }
+    
+    
+    private synchronized void close() throws ChannelException  {
+        ChannelException x = null;
+        Object[] members = nioSenders.keySet().toArray();
+        for (int i=0; i<members.length; i++ ) {
+            Member mbr = (Member)members[i];
+            try {
+                NioSender sender = (NioSender)nioSenders.get(mbr);
+                sender.disconnect();
+            }catch ( Exception e ) {
+                if ( x == null ) x = new ChannelException(e);
+                x.addFaultyMember(mbr,e);
+            }
+            nioSenders.remove(mbr);
+        }
+        if ( x != null ) throw x;
+    }
+    
+    public void memberAdded(Member member) {
+        
+    }
+    
+    public void memberDisappeared(Member member) {
+        //disconnect senders
+        NioSender sender = (NioSender)nioSenders.remove(member);
+        if ( sender != null ) sender.disconnect();
+    }
+
+    
+    public synchronized void disconnect() {
+        setConnected(false);
+        try {close(); }catch (Exception x){}
+        
+    }
+    
+    public void finalize() {
+        try {disconnect(); }catch ( Exception ignore){}
+    }
+
+    public boolean keepalive() {
+        //throw new UnsupportedOperationException("Method ParallelNioSender.checkKeepAlive() not implemented");
+        boolean result = false;
+        for ( Iterator i = nioSenders.entrySet().iterator(); i.hasNext();  ) {
+            Map.Entry entry = (Map.Entry)i.next();
+            NioSender sender = (NioSender)entry.getValue();
+            if ( sender.keepalive() ) {
+                nioSenders.remove(entry.getKey());
+            }
+        }
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java b/java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java
new file mode 100644 (file)
index 0000000..c802194
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.transport.nio;
+
+import java.io.IOException;
+
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.transport.DataSender;
+import org.apache.catalina.tribes.transport.MultiPointSender;
+import org.apache.catalina.tribes.transport.PooledSender;
+
+/**
+ * <p>Title: </p>
+ *
+ * <p>Description: </p>
+ *
+ * <p>Copyright: Copyright (c) 2005</p>
+ *
+ * <p>Company: </p>
+ *
+ * @author not attributable
+ * @version 1.0
+ */
+public class PooledParallelSender extends PooledSender implements MultiPointSender {
+    protected boolean connected = true;
+    public PooledParallelSender() {
+        super();
+    }
+    
+    public void sendMessage(Member[] destination, ChannelMessage message) throws ChannelException {
+        if ( !connected ) throw new ChannelException("Sender not connected.");
+        ParallelNioSender sender = (ParallelNioSender)getSender();
+        try {
+            sender.sendMessage(destination, message);
+            sender.keepalive();
+        }finally {
+            if ( !connected ) disconnect();
+            returnSender(sender);
+        }
+    }
+
+    public DataSender getNewDataSender() {
+        try {
+            ParallelNioSender sender = new ParallelNioSender();
+            sender.transferProperties(this,sender);
+            return sender;
+        } catch ( IOException x ) {
+            throw new RuntimeException("Unable to open NIO selector.",x);
+        }
+    }
+    
+    public synchronized void disconnect() {
+        this.connected = false;
+        super.disconnect();
+    }
+
+    public synchronized void connect() throws IOException {
+        this.connected = true;
+        super.connect();
+    }
+
+    public void memberAdded(Member member) {
+    
+    }
+    
+    public void memberDisappeared(Member member) {
+        //disconnect senders
+    }    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/util/Arrays.java b/java/org/apache/catalina/tribes/util/Arrays.java
new file mode 100644 (file)
index 0000000..ef075a8
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.group.AbsoluteOrder;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.membership.Membership;
+
+/**
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class Arrays {
+
+    public static boolean contains(byte[] source, int srcoffset, byte[] key, int keyoffset, int length) {
+        if ( srcoffset < 0 || srcoffset >= source.length) throw new ArrayIndexOutOfBoundsException("srcoffset is out of bounds.");
+        if ( keyoffset < 0 || keyoffset >= key.length) throw new ArrayIndexOutOfBoundsException("keyoffset is out of bounds.");
+        if ( length > (key.length-keyoffset) ) throw new ArrayIndexOutOfBoundsException("not enough data elements in the key, length is out of bounds.");
+        //we don't have enough data to validate it
+        if ( length > (source.length-srcoffset) ) return false;
+        boolean match = true;
+        int pos = keyoffset;
+        for ( int i=srcoffset; match && i<length; i++ ) {
+            match = (source[i] == key[pos++]);
+        }
+        return match;
+    }
+    
+    public static String toString(byte[] data) {
+        return toString(data,0,data!=null?data.length:0);
+    }
+
+    public static String toString(byte[] data, int offset, int length) {
+        StringBuffer buf = new StringBuffer("{");
+        if ( data != null && length > 0 ) {
+            buf.append(data[offset++]);
+            for (int i = offset; i < length; i++) {
+                buf.append(", ").append(data[i]);
+            }
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    public static String toString(Object[] data) {
+        return toString(data,0,data!=null?data.length:0);
+    }
+    
+    public static String toString(Object[] data, int offset, int length) {
+        StringBuffer buf = new StringBuffer("{");
+        if ( data != null && length > 0 ) {
+            buf.append(data[offset++]);
+            for (int i = offset; i < length; i++) {
+                buf.append(", ").append(data[i]);
+            }
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    public static String toNameString(Member[] data) {
+        return toNameString(data,0,data!=null?data.length:0);
+    }
+    
+    public static String toNameString(Member[] data, int offset, int length) {
+        StringBuffer buf = new StringBuffer("{");
+        if ( data != null && length > 0 ) {
+            buf.append(data[offset++].getName());
+            for (int i = offset; i < length; i++) {
+                buf.append(", ").append(data[i].getName());
+            }
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+
+    public static int add(int[] data) {
+        int result = 0;
+        for (int i=0;i<data.length; i++ ) result += data[i];
+        return result;
+    }
+    
+    public static UniqueId getUniqudId(ChannelMessage msg) {
+        return new UniqueId(msg.getUniqueId());
+    }
+
+    public static UniqueId getUniqudId(byte[] data) {
+        return new UniqueId(data);
+    }
+    
+    public static boolean equals(byte[] o1, byte[] o2) {
+        return java.util.Arrays.equals(o1,o2);
+    }
+
+    public static boolean equals(Object[] o1, Object[] o2) {
+        boolean result = o1.length == o2.length;
+        if ( result ) for (int i=0; i<o1.length && result; i++ ) result = o1[i].equals(o2[i]);
+        return result;
+    }
+    
+    public static boolean sameMembers(Member[] m1, Member[] m2) {
+        AbsoluteOrder.absoluteOrder(m1);
+        AbsoluteOrder.absoluteOrder(m2);
+        return equals(m1,m2);
+    }
+    
+    public static Member[] merge(Member[] m1, Member[] m2) {
+        AbsoluteOrder.absoluteOrder(m1);
+        AbsoluteOrder.absoluteOrder(m2);
+        ArrayList list = new ArrayList(java.util.Arrays.asList(m1));
+        for (int i=0; i<m2.length; i++) if ( !list.contains(m2[i]) ) list.add(m2[i]);
+        Member[] result = new Member[list.size()];
+        list.toArray(result);
+        AbsoluteOrder.absoluteOrder(result);
+        return result;
+    }
+    
+    public static void fill(Membership mbrship, Member[] m) {
+        for (int i=0; i<m.length; i++ ) mbrship.addMember((MemberImpl)m[i]);
+    }
+    
+    public static Member[] diff(Membership complete, Membership local, MemberImpl ignore) {
+        ArrayList result = new ArrayList();
+        MemberImpl[] comp = complete.getMembers();
+        for ( int i=0; i<comp.length; i++ ) {
+            if ( ignore!=null && ignore.equals(comp[i]) ) continue;
+            if ( local.getMember(comp[i]) == null ) result.add(comp[i]);
+        }
+        return (MemberImpl[])result.toArray(new MemberImpl[result.size()]);
+    }
+    
+    public static Member[] remove(Member[] all, Member remove) {
+        return extract(all,new Member[] {remove});
+    }
+    
+    public static Member[] extract(Member[] all, Member[] remove) {
+        List alist = java.util.Arrays.asList(all);
+        ArrayList list = new ArrayList(alist);
+        for (int i=0; i<remove.length; i++ ) list.remove(remove[i]);
+        return (Member[])list.toArray(new Member[list.size()]);
+    }
+    
+    public static int indexOf(Member member, Member[] members) {
+        int result = -1;
+        for (int i=0; (result==-1) && (i<members.length); i++ ) 
+            if ( member.equals(members[i]) ) result = i;
+        return result;
+    }
+    
+    public static int nextIndex(Member member, Member[] members) {
+        int idx = indexOf(member,members)+1;
+        if (idx >= members.length ) idx = ((members.length>0)?0:-1);
+        
+//System.out.println("Next index:"+idx);
+//System.out.println("Member:"+member.getName());
+//System.out.println("Members:"+toNameString(members));
+        return idx;
+    }
+    
+    public static int hashCode(byte a[]) {
+        if (a == null)
+            return 0;
+
+        int result = 1;
+        for (int i=0; i<a.length; i++) {
+            byte element = a[i];
+            result = 31 * result + element;
+        }
+        return result;
+    }
+
+
+    
+    
+    
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/util/Logs.java b/java/org/apache/catalina/tribes/util/Logs.java
new file mode 100644 (file)
index 0000000..2485894
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.util;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+/**
+ * 
+ * Simple class that holds references to global loggers
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class Logs {
+    public static Log MESSAGES = LogFactory.getLog( "org.apache.catalina.tribes.MESSAGES" );
+}
diff --git a/java/org/apache/catalina/tribes/util/StringManager.java b/java/org/apache/catalina/tribes/util/StringManager.java
new file mode 100644 (file)
index 0000000..77f1122
--- /dev/null
@@ -0,0 +1,252 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.catalina.tribes.util;
+
+import java.text.MessageFormat;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.net.URLClassLoader;
+
+/**
+ * An internationalization / localization helper class which reduces
+ * the bother of handling ResourceBundles and takes care of the
+ * common cases of message formating which otherwise require the
+ * creation of Object arrays and such.
+ *
+ * <p>The StringManager operates on a package basis. One StringManager
+ * per package can be created and accessed via the getManager method
+ * call.
+ *
+ * <p>The StringManager will look for a ResourceBundle named by
+ * the package name given plus the suffix of "LocalStrings". In
+ * practice, this means that the localized information will be contained
+ * in a LocalStrings.properties file located in the package
+ * directory of the classpath.
+ *
+ * <p>Please see the documentation for java.util.ResourceBundle for
+ * more information.
+ *
+ * @author James Duncan Davidson [duncan@eng.sun.com]
+ * @author James Todd [gonzo@eng.sun.com]
+ */
+
+public class StringManager {
+
+    /**
+     * The ResourceBundle for this StringManager.
+     */
+
+    private ResourceBundle bundle;
+
+    private static org.apache.commons.logging.Log log=
+        org.apache.commons.logging.LogFactory.getLog( StringManager.class );
+
+    /**
+     * Creates a new StringManager for a given package. This is a
+     * private method and all access to it is arbitrated by the
+     * static getManager method call so that only one StringManager
+     * per package will be created.
+     *
+     * @param packageName Name of package to create StringManager for.
+     */
+
+    private StringManager(String packageName) {
+        String bundleName = packageName + ".LocalStrings";
+        try {
+            bundle = ResourceBundle.getBundle(bundleName);
+            return;
+        } catch( MissingResourceException ex ) {
+            // Try from the current loader ( that's the case for trusted apps )
+            ClassLoader cl=Thread.currentThread().getContextClassLoader();
+            if( cl != null ) {
+                try {
+                    bundle=ResourceBundle.getBundle(bundleName, Locale.getDefault(), cl);
+                    return;
+                } catch(MissingResourceException ex2) {
+                }
+            }
+            if( cl==null )
+                cl=this.getClass().getClassLoader();
+
+            if (log.isDebugEnabled())
+                log.debug("Can't find resource " + bundleName +
+                    " " + cl);
+            if( cl instanceof URLClassLoader ) {
+                if (log.isDebugEnabled()) 
+                    log.debug( ((URLClassLoader)cl).getURLs());
+            }
+        }
+    }
+
+    /**
+     * Get a string from the underlying resource bundle.
+     *
+     * @param key The resource name
+     */
+    public String getString(String key) {
+        return MessageFormat.format(getStringInternal(key), (Object [])null);
+    }
+
+
+    protected String getStringInternal(String key) {
+        if (key == null) {
+            String msg = "key is null";
+
+            throw new NullPointerException(msg);
+        }
+
+        String str = null;
+
+        if( bundle==null )
+            return key;
+        try {
+            str = bundle.getString(key);
+        } catch (MissingResourceException mre) {
+            str = "Cannot find message associated with key '" + key + "'";
+        }
+
+        return str;
+    }
+
+    /**
+     * Get a string from the underlying resource bundle and format
+     * it with the given set of arguments.
+     *
+     * @param key The resource name
+     * @param args Formatting directives
+     */
+
+    public String getString(String key, Object[] args) {
+        String iString = null;
+        String value = getStringInternal(key);
+
+        // this check for the runtime exception is some pre 1.1.6
+        // VM's don't do an automatic toString() on the passed in
+        // objects and barf out
+
+        try {
+            // ensure the arguments are not null so pre 1.2 VM's don't barf
+            Object nonNullArgs[] = args;
+            for (int i=0; i<args.length; i++) {
+                if (args[i] == null) {
+                    if (nonNullArgs==args) nonNullArgs=(Object[])args.clone();
+                    nonNullArgs[i] = "null";
+                }
+            }
+
+            iString = MessageFormat.format(value, nonNullArgs);
+        } catch (IllegalArgumentException iae) {
+            StringBuffer buf = new StringBuffer();
+            buf.append(value);
+            for (int i = 0; i < args.length; i++) {
+                buf.append(" arg[" + i + "]=" + args[i]);
+            }
+            iString = buf.toString();
+        }
+        return iString;
+    }
+
+    /**
+     * Get a string from the underlying resource bundle and format it
+     * with the given object argument. This argument can of course be
+     * a String object.
+     *
+     * @param key The resource name
+     * @param arg Formatting directive
+     */
+
+    public String getString(String key, Object arg) {
+        Object[] args = new Object[] {arg};
+        return getString(key, args);
+    }
+
+    /**
+     * Get a string from the underlying resource bundle and format it
+     * with the given object arguments. These arguments can of course
+     * be String objects.
+     *
+     * @param key The resource name
+     * @param arg1 Formatting directive
+     * @param arg2 Formatting directive
+     */
+
+    public String getString(String key, Object arg1, Object arg2) {
+        Object[] args = new Object[] {arg1, arg2};
+        return getString(key, args);
+    }
+
+    /**
+     * Get a string from the underlying resource bundle and format it
+     * with the given object arguments. These arguments can of course
+     * be String objects.
+     *
+     * @param key The resource name
+     * @param arg1 Formatting directive
+     * @param arg2 Formatting directive
+     * @param arg3 Formatting directive
+     */
+
+    public String getString(String key, Object arg1, Object arg2,
+                            Object arg3) {
+        Object[] args = new Object[] {arg1, arg2, arg3};
+        return getString(key, args);
+    }
+
+    /**
+     * Get a string from the underlying resource bundle and format it
+     * with the given object arguments. These arguments can of course
+     * be String objects.
+     *
+     * @param key The resource name
+     * @param arg1 Formatting directive
+     * @param arg2 Formatting directive
+     * @param arg3 Formatting directive
+     * @param arg4 Formatting directive
+     */
+
+    public String getString(String key, Object arg1, Object arg2,
+                            Object arg3, Object arg4) {
+        Object[] args = new Object[] {arg1, arg2, arg3, arg4};
+        return getString(key, args);
+    }
+    // --------------------------------------------------------------
+    // STATIC SUPPORT METHODS
+    // --------------------------------------------------------------
+
+    private static Hashtable managers = new Hashtable();
+
+    /**
+     * Get the StringManager for a particular package. If a manager for
+     * a package already exists, it will be reused, else a new
+     * StringManager will be created and returned.
+     *
+     * @param packageName The package name
+     */
+
+    public synchronized static StringManager getManager(String packageName) {
+        StringManager mgr = (StringManager)managers.get(packageName);
+
+        if (mgr == null) {
+            mgr = new StringManager(packageName);
+            managers.put(packageName, mgr);
+        }
+        return mgr;
+    }
+}
diff --git a/java/org/apache/catalina/tribes/util/UUIDGenerator.java b/java/org/apache/catalina/tribes/util/UUIDGenerator.java
new file mode 100644 (file)
index 0000000..aa56370
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.tribes.util;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * simple generation of a UUID 
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class UUIDGenerator {
+    public static final int UUID_LENGTH = 16;
+    public static final int UUID_VERSION = 4;
+    public static final int BYTES_PER_INT = 4;
+    public static final int BITS_PER_BYTE = 8;
+    
+    protected static SecureRandom secrand = null;
+    protected static Random rand = new Random(System.currentTimeMillis());
+    static {
+        secrand = new SecureRandom();
+        secrand.setSeed(rand.nextLong());
+    }
+    
+    public static byte[] randomUUID(boolean secure) {
+        byte[] result = new byte[UUID_LENGTH];
+        return randomUUID(secure,result,0);
+    }
+
+    public static byte[] randomUUID(boolean secure, byte[] into, int offset) {
+        if ( (offset+UUID_LENGTH)>into.length )
+            throw new ArrayIndexOutOfBoundsException("Unable to fit "+UUID_LENGTH+" bytes into the array. length:"+into.length+" required length:"+(offset+UUID_LENGTH));
+        Random r = (secure&&(secrand!=null))?secrand:rand;
+        nextBytes(into,offset,UUID_LENGTH,r);
+        into[6+offset] &= 0x0F;
+        into[6+offset] |= (UUID_VERSION << 4);
+        into[8+offset] &= 0x3F; //0011 1111
+        into[8+offset] |= 0x80; //1000 0000
+        return into;
+    }
+    
+    /**
+     * Same as java.util.Random.nextBytes except this one we dont have to allocate a new byte array
+     * @param into byte[]
+     * @param offset int
+     * @param length int
+     * @param r Random
+     */
+    public static void nextBytes(byte[] into, int offset, int length, Random r) {
+        int numRequested = length;
+        int numGot = 0, rnd = 0;
+        while (true) {
+            for (int i = 0; i < BYTES_PER_INT; i++) {
+                if (numGot == numRequested) return;
+                rnd = (i == 0 ? r.nextInt() : rnd >> BITS_PER_BYTE);
+                into[offset+numGot] = (byte) rnd;
+                numGot++;
+            }
+        }
+    }
+
+}
\ No newline at end of file