Commit 52a517c6 authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

More clustering work (MUC support).

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@8787 b35dd754-fafc-0310-a699-88a17e54d16e
parent a3ac7356
...@@ -99,7 +99,7 @@ public class XMPPServer { ...@@ -99,7 +99,7 @@ public class XMPPServer {
private Date startDate; private Date startDate;
private boolean initialized = false; private boolean initialized = false;
private NodeID nodeID; private NodeID nodeID;
private static final NodeID DEFAULT_NODE_ID = new NodeID(new byte[0]); private static final NodeID DEFAULT_NODE_ID = NodeID.getInstance(new byte[0]);
/** /**
* All modules loaded by this server * All modules loaded by this server
...@@ -313,7 +313,7 @@ public class XMPPServer { ...@@ -313,7 +313,7 @@ public class XMPPServer {
name = JiveGlobals.getProperty("xmpp.domain", "127.0.0.1").toLowerCase(); name = JiveGlobals.getProperty("xmpp.domain", "127.0.0.1").toLowerCase();
version = new Version(3, 4, 0, Version.ReleaseStatus.Alpha, 1); version = new Version(3, 4, 0, Version.ReleaseStatus.Alpha, 2);
if ("true".equals(JiveGlobals.getXMLProperty("setup"))) { if ("true".equals(JiveGlobals.getXMLProperty("setup"))) {
setupMode = false; setupMode = false;
} }
......
...@@ -334,6 +334,20 @@ public class ClusterManager { ...@@ -334,6 +334,20 @@ public class ClusterManager {
return CacheFactory.isSeniorClusterMember(); return CacheFactory.isSeniorClusterMember();
} }
/**
* Returns the id of the node that is the senior cluster member. When not in a cluster the returned
* node id will be the {@link XMPPServer#getNodeID()}.
*
* @return the id of the node that is the senior cluster member.
*/
public static NodeID getSeniorClusterMember() {
byte[] clusterMemberID = CacheFactory.getSeniorClusterMemberID();
if (clusterMemberID == null) {
return XMPPServer.getInstance().getNodeID();
}
return NodeID.getInstance(clusterMemberID);
}
private static class Event { private static class Event {
private EventType type; private EventType type;
private byte[] nodeID; private byte[] nodeID;
......
...@@ -17,7 +17,9 @@ import java.io.Externalizable; ...@@ -17,7 +17,9 @@ import java.io.Externalizable;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInput; import java.io.ObjectInput;
import java.io.ObjectOutput; import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
/** /**
* Class which wraps the byte[] we use to identify cluster members. The main reason * Class which wraps the byte[] we use to identify cluster members. The main reason
...@@ -29,12 +31,38 @@ import java.util.Arrays; ...@@ -29,12 +31,38 @@ import java.util.Arrays;
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class NodeID implements Externalizable { public class NodeID implements Externalizable {
private static List<NodeID> instances = new ArrayList<NodeID>();
private byte[] nodeID; private byte[] nodeID;
public static synchronized NodeID getInstance(byte[] nodeIdBytes) {
for (NodeID nodeID : instances) {
if (nodeID.equals(nodeIdBytes)) {
return nodeID;
}
}
NodeID answer = new NodeID(nodeIdBytes);
instances.add(answer);
return answer;
}
public static synchronized void deleteInstance(byte[] nodeIdBytes) {
NodeID toDelete = null;
for (NodeID nodeID : instances) {
if (nodeID.equals(nodeIdBytes)) {
toDelete = nodeID;
break;
}
}
if (toDelete != null) {
instances.remove(toDelete);
}
}
public NodeID() { public NodeID() {
} }
public NodeID(byte[] nodeIdBytes) { private NodeID(byte[] nodeIdBytes) {
this.nodeID = nodeIdBytes; this.nodeID = nodeIdBytes;
} }
......
...@@ -306,7 +306,7 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene ...@@ -306,7 +306,7 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene
public void leftCluster(byte[] nodeID) { public void leftCluster(byte[] nodeID) {
if (ClusterManager.isSeniorClusterMember()) { if (ClusterManager.isSeniorClusterMember()) {
NodeID leftNode = new NodeID(nodeID); NodeID leftNode = NodeID.getInstance(nodeID);
// Remove server features added by node that is gone // Remove server features added by node that is gone
for (Map.Entry<String, Set<NodeID>> entry : serverFeatures.entrySet()) { for (Map.Entry<String, Set<NodeID>> entry : serverFeatures.entrySet()) {
String namespace = entry.getKey(); String namespace = entry.getKey();
......
...@@ -365,7 +365,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -365,7 +365,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
public void leftCluster(byte[] nodeID) { public void leftCluster(byte[] nodeID) {
if (ClusterManager.isSeniorClusterMember()) { if (ClusterManager.isSeniorClusterMember()) {
NodeID leftNode = new NodeID(nodeID); NodeID leftNode = NodeID.getInstance(nodeID);
for (Map.Entry<String, ClusteredServerItem> entry : serverItems.entrySet()) { for (Map.Entry<String, ClusteredServerItem> entry : serverItems.entrySet()) {
String jid = entry.getKey(); String jid = entry.getKey();
Lock lock = LockManager.getLock(jid + "item"); Lock lock = LockManager.getLock(jid + "item");
......
...@@ -11,21 +11,16 @@ ...@@ -11,21 +11,16 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import org.dom4j.Element;
import org.jivesoftware.openfire.muc.spi.LocalMUCRole;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.Log;
import org.xmpp.packet.Message;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.*;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.TimeZone;
import org.jivesoftware.openfire.muc.spi.MUCRoleImpl;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.FastDateFormat;
import org.dom4j.Element;
import org.xmpp.packet.Message;
/** /**
* Represents the amount of history requested by an occupant while joining a room. There are * Represents the amount of history requested by an occupant while joining a room. There are
...@@ -136,7 +131,7 @@ public class HistoryRequest { ...@@ -136,7 +131,7 @@ public class HistoryRequest {
* @param joinRole the user that will receive the history. * @param joinRole the user that will receive the history.
* @param roomHistory the history of the room. * @param roomHistory the history of the room.
*/ */
public void sendHistory(MUCRoleImpl joinRole, MUCRoomHistory roomHistory) { public void sendHistory(LocalMUCRole joinRole, MUCRoomHistory roomHistory) {
if (!isConfigured()) { if (!isConfigured()) {
Iterator history = roomHistory.getMessageHistory(); Iterator history = roomHistory.getMessageHistory();
while (history.hasNext()) { while (history.hasNext()) {
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import org.jivesoftware.openfire.muc.cluster.UpdateHistoryStrategy;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log; import org.jivesoftware.util.Log;
import org.jivesoftware.util.cache.CacheFactory;
import org.xmpp.packet.Message; import org.xmpp.packet.Message;
import java.util.Iterator; import java.util.*;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
...@@ -96,10 +96,18 @@ public class HistoryStrategy { ...@@ -96,10 +96,18 @@ public class HistoryStrategy {
* @param max the maximum number of messages to store in applicable strategies. * @param max the maximum number of messages to store in applicable strategies.
*/ */
public void setMaxNumber(int max) { public void setMaxNumber(int max) {
if (maxNumber == max) {
// Do nothing since value has not changed
return;
}
this.maxNumber = max; this.maxNumber = max;
if (contextPrefix != null){ if (contextPrefix != null){
JiveGlobals.setProperty(contextPrefix + ".maxNumber", Integer.toString(maxNumber)); JiveGlobals.setProperty(contextPrefix + ".maxNumber", Integer.toString(maxNumber));
} }
if (parent == null) {
// Update the history strategy of the MUC service
CacheFactory.doClusterTask(new UpdateHistoryStrategy(this));
}
} }
/** /**
...@@ -108,12 +116,20 @@ public class HistoryStrategy { ...@@ -108,12 +116,20 @@ public class HistoryStrategy {
* @param newType The new type of chat history to use. * @param newType The new type of chat history to use.
*/ */
public void setType(Type newType){ public void setType(Type newType){
if (type == newType) {
// Do nothing since value has not changed
return;
}
if (newType != null){ if (newType != null){
type = newType; type = newType;
} }
if (contextPrefix != null){ if (contextPrefix != null){
JiveGlobals.setProperty(contextPrefix + ".type", type.toString()); JiveGlobals.setProperty(contextPrefix + ".type", type.toString());
} }
if (parent == null) {
// Update the history strategy of the MUC service
CacheFactory.doClusterTask(new UpdateHistoryStrategy(this));
}
} }
/** /**
...@@ -195,6 +211,8 @@ public class HistoryStrategy { ...@@ -195,6 +211,8 @@ public class HistoryStrategy {
*/ */
public Iterator<Message> getMessageHistory(){ public Iterator<Message> getMessageHistory(){
LinkedList<Message> list = new LinkedList<Message>(history); LinkedList<Message> list = new LinkedList<Message>(history);
// Sort messages. Messages may be out of order when running inside of a cluster
Collections.sort(list, new MessageComparator());
return list.iterator(); return list.iterator();
} }
...@@ -207,6 +225,8 @@ public class HistoryStrategy { ...@@ -207,6 +225,8 @@ public class HistoryStrategy {
*/ */
public ListIterator<Message> getReverseMessageHistory(){ public ListIterator<Message> getReverseMessageHistory(){
LinkedList<Message> list = new LinkedList<Message>(history); LinkedList<Message> list = new LinkedList<Message>(history);
// Sort messages. Messages may be out of order when running inside of a cluster
Collections.sort(list, new MessageComparator());
return list.listIterator(list.size()); return list.listIterator(list.size());
} }
...@@ -227,14 +247,14 @@ public class HistoryStrategy { ...@@ -227,14 +247,14 @@ public class HistoryStrategy {
*/ */
public void setTypeFromString(String typeName) { public void setTypeFromString(String typeName) {
try { try {
setType(Type.valueOf(typeName)); type = Type.valueOf(typeName);
} }
catch (Exception e) { catch (Exception e) {
if (parent != null) { if (parent != null) {
setType(Type.defaulType); type = Type.defaulType;
} }
else { else {
setType(Type.number); type = Type.number;
} }
} }
} }
...@@ -251,7 +271,7 @@ public class HistoryStrategy { ...@@ -251,7 +271,7 @@ public class HistoryStrategy {
String maxNumberString = JiveGlobals.getProperty(prefix + ".maxNumber"); String maxNumberString = JiveGlobals.getProperty(prefix + ".maxNumber");
if (maxNumberString != null && maxNumberString.trim().length() > 0){ if (maxNumberString != null && maxNumberString.trim().length() > 0){
try { try {
setMaxNumber(Integer.parseInt(maxNumberString)); this.maxNumber = Integer.parseInt(maxNumberString);
} }
catch (Exception e){ catch (Exception e){
Log.info("Jive property " + prefix + ".maxNumber not a valid number."); Log.info("Jive property " + prefix + ".maxNumber not a valid number.");
...@@ -269,4 +289,12 @@ public class HistoryStrategy { ...@@ -269,4 +289,12 @@ public class HistoryStrategy {
public boolean hasChangedSubject() { public boolean hasChangedSubject() {
return roomSubject != null; return roomSubject != null;
} }
private static class MessageComparator implements Comparator<Message> {
public int compare(Message o1, Message o2) {
String stamp1 = o1.getChildElement("x", "jabber:x:delay").attributeValue("stamp");
String stamp2 = o2.getChildElement("x", "jabber:x:delay").attributeValue("stamp");
return stamp1.compareTo(stamp2);
}
}
} }
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import org.dom4j.Element; import org.jivesoftware.openfire.cluster.NodeID;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.Packet; import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence; import org.xmpp.packet.Presence;
...@@ -27,6 +27,139 @@ import org.xmpp.packet.Presence; ...@@ -27,6 +27,139 @@ import org.xmpp.packet.Presence;
*/ */
public interface MUCRole { public interface MUCRole {
/**
* Obtain the current presence status of a user in a chatroom.
*
* @return The presence of the user in the room.
*/
public Presence getPresence();
/**
* Set the current presence status of a user in a chatroom.
*
* @param presence The presence of the user in the room.
*/
public void setPresence(Presence presence);
/**
* Call this method to promote or demote a user's role in a chatroom.
* It is common for the chatroom or other chat room members to change
* the role of users (a moderator promoting another user to moderator
* status for example).<p>
* <p/>
* Owning ChatUsers should have their membership roles updated.
*
* @param newRole The new role that the user will play.
* @throws NotAllowedException Thrown if trying to change the moderator role to an owner or
* administrator.
*/
public void setRole(Role newRole) throws NotAllowedException;
/**
* Obtain the role state of the user.
*
* @return The role status of this user.
*/
public Role getRole();
/**
* Call this method to promote or demote a user's affiliation in a chatroom.
*
* @param newAffiliation the new affiliation that the user will play.
* @throws NotAllowedException thrown if trying to ban an owner or an administrator.
*/
public void setAffiliation(Affiliation newAffiliation) throws NotAllowedException;
/**
* Obtain the affiliation state of the user.
*
* @return The affiliation status of this user.
*/
public Affiliation getAffiliation();
/**
* Changes the nickname of the occupant within the room to the new nickname.
*
* @param nickname the new nickname of the occupant in the room.
*/
void changeNickname(String nickname);
/**
* Obtain the nickname for the user in the chatroom.
*
* @return The user's nickname in the room or null if invisible.
*/
public String getNickname();
/**
* Destroys this role after the occupant left the room. This role will be
* removed from MUCUser.
*/
public void destroy();
/**
* Returns true if the room occupant does not want to get messages broadcasted to all
* room occupants. This type of users are called "deaf" occupants. Deaf occupants will still
* be able to get private messages, presences, IQ packets or room history.<p>
*
* To be a deaf occupant the initial presence sent to the room while joining the room has
* to include the following child element:
* <pre>
* &lt;x xmlns='http://jivesoftware.org/protocol/muc'&gt;
* &lt;deaf-occupant/&gt;
* &lt;/x&gt;
* </pre>
*
* Note that this is a custom extension to the MUC specification.
*
* @return true if the room occupant does not want to get messages broadcasted to all
* room occupants.
*/
boolean isVoiceOnly();
/**
* Obtain the chat room that hosts this user's role.
*
* @return The chatroom hosting this role.
*/
public MUCRoom getChatRoom();
/**
* Obtain the XMPPAddress representing this role in a room: room@server/nickname
*
* @return The Jabber ID that represents this role in the room.
*/
public JID getRoleAddress();
/**
* Obtain the XMPPAddress of the user that joined the room. A <tt>null</tt> null value
* represents the room's role.
*
* @return The address of the user that joined the room or null if this role belongs to the room itself.
*/
public JID getUserAddress();
/**
* Returns true if this room occupant is hosted by this JVM.
*
* @return true if this room occupant is hosted by this JVM
*/
public boolean isLocal();
/**
* Returns the id of the node that is hosting the room occupant.
*
* @return the id of the node that is hosting the room occupant.
*/
public NodeID getNodeID();
/**
* Sends a packet to the user.
*
* @param packet The packet to send
*/
public void send(Packet packet);
public enum Role { public enum Role {
/** /**
...@@ -141,125 +274,4 @@ public interface MUCRole { ...@@ -141,125 +274,4 @@ public interface MUCRole {
} }
} }
} }
/**
* Obtain the current presence status of a user in a chatroom.
*
* @return The presence of the user in the room.
*/
public Presence getPresence();
/**
* Returns the extended presence information that includes information about roles,
* affiliations, JIDs, etc.
*
* @return the extended presence information that includes information about roles,
* affiliations.
*/
public Element getExtendedPresenceInformation();
/**
* Set the current presence status of a user in a chatroom.
*
* @param presence The presence of the user in the room.
*/
public void setPresence(Presence presence);
/**
* Call this method to promote or demote a user's role in a chatroom.
* It is common for the chatroom or other chat room members to change
* the role of users (a moderator promoting another user to moderator
* status for example).<p>
* <p/>
* Owning ChatUsers should have their membership roles updated.
*
* @param newRole The new role that the user will play.
* @throws NotAllowedException Thrown if trying to change the moderator role to an owner or
* administrator.
*/
public void setRole(Role newRole) throws NotAllowedException;
/**
* Obtain the role state of the user.
*
* @return The role status of this user.
*/
public Role getRole();
/**
* Call this method to promote or demote a user's affiliation in a chatroom.
*
* @param newAffiliation the new affiliation that the user will play.
* @throws NotAllowedException thrown if trying to ban an owner or an administrator.
*/
public void setAffiliation(Affiliation newAffiliation) throws NotAllowedException;
/**
* Obtain the affiliation state of the user.
*
* @return The affiliation status of this user.
*/
public Affiliation getAffiliation();
/**
* Obtain the nickname for the user in the chatroom.
*
* @return The user's nickname in the room or null if invisible.
*/
public String getNickname();
/**
* Changes the nickname of the occupant within the room to the new nickname.
*
* @param nickname the new nickname of the occupant in the room.
*/
public void changeNickname(String nickname);
/**
* Returns true if the room occupant does not want to get messages broadcasted to all
* room occupants. This type of users are called "deaf" occupants. Deaf occupants will still
* be able to get private messages, presences, IQ packets or room history.<p>
*
* To be a deaf occupant the initial presence sent to the room while joining the room has
* to include the following child element:
* <pre>
* &lt;x xmlns='http://jivesoftware.org/protocol/muc'&gt;
* &lt;deaf-occupant/&gt;
* &lt;/x&gt;
* </pre>
*
* Note that this is a custom extension to the MUC specification.
*
* @return true if the room occupant does not want to get messages broadcasted to all
* room occupants.
*/
boolean isVoiceOnly();
/**
* Obtain the chat user that plays this role.
*
* @return The chatuser playing this role.
*/
public MUCUser getChatUser();
/**
* Obtain the chat room that hosts this user's role.
*
* @return The chatroom hosting this role.
*/
public MUCRoom getChatRoom();
/**
* Obtain the XMPPAddress representing this role in a room: room@server/nickname
*
* @return The Jabber ID that represents this role in the room.
*/
public JID getRoleAddress();
/**
* Sends a packet to the user.
*
* @param packet The packet to send
*/
public void send(Packet packet);
} }
\ No newline at end of file
...@@ -11,23 +11,26 @@ ...@@ -11,23 +11,26 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import java.util.List;
import java.util.Date;
import java.util.Collection;
import org.dom4j.Element; import org.dom4j.Element;
import org.jivesoftware.database.JiveID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.muc.spi.IQAdminHandler; import org.jivesoftware.openfire.muc.spi.IQAdminHandler;
import org.jivesoftware.openfire.muc.spi.IQOwnerHandler; import org.jivesoftware.openfire.muc.spi.IQOwnerHandler;
import org.jivesoftware.util.NotFoundException; import org.jivesoftware.openfire.muc.spi.LocalMUCRole;
import org.jivesoftware.util.JiveConstants; import org.jivesoftware.openfire.muc.spi.LocalMUCUser;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.user.UserAlreadyExistsException; import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.database.JiveID; import org.jivesoftware.util.JiveConstants;
import org.xmpp.packet.Presence; import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.Message;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet; import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import java.io.Externalizable;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/** /**
...@@ -37,7 +40,7 @@ import org.xmpp.packet.Packet; ...@@ -37,7 +40,7 @@ import org.xmpp.packet.Packet;
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
@JiveID(JiveConstants.MUC_ROOM) @JiveID(JiveConstants.MUC_ROOM)
public interface MUCRoom { public interface MUCRoom extends Externalizable {
/** /**
* Get the name of this room. * Get the name of this room.
...@@ -139,13 +142,13 @@ public interface MUCRoom { ...@@ -139,13 +142,13 @@ public interface MUCRoom {
List<MUCRole> getOccupantsByBareJID(String jid) throws UserNotFoundException; List<MUCRole> getOccupantsByBareJID(String jid) throws UserNotFoundException;
/** /**
* Obtain the role of a given user in the room by his full JID. * Returns the role of a given user in the room by his full JID or <tt>null</tt>
* if no role was found for the specified user.
* *
* @param jid The full jid of the user you'd like to obtain * @param jid The full jid of the user you'd like to obtain
* @return The user's role in the room * @return The user's role in the room or null if not found.
* @throws UserNotFoundException If there is no user with the given nickname
*/ */
MUCRole getOccupantByFullJID(JID jid) throws UserNotFoundException; MUCRole getOccupantByFullJID(JID jid);
/** /**
* Obtain the roles of all users in the chatroom. * Obtain the roles of all users in the chatroom.
...@@ -209,7 +212,7 @@ public interface MUCRoom { ...@@ -209,7 +212,7 @@ public interface MUCRoom {
* @throws NotAcceptableException If the registered user is trying to join with a * @throws NotAcceptableException If the registered user is trying to join with a
* nickname different than the reserved nickname. * nickname different than the reserved nickname.
*/ */
MUCRole joinRoom(String nickname, String password, HistoryRequest historyRequest, MUCUser user, LocalMUCRole joinRoom(String nickname, String password, HistoryRequest historyRequest, LocalMUCUser user,
Presence presence) throws UnauthorizedException, UserAlreadyExistsException, Presence presence) throws UnauthorizedException, UserAlreadyExistsException,
RoomLockedException, ForbiddenException, RegistrationRequiredException, RoomLockedException, ForbiddenException, RegistrationRequiredException,
ConflictException, ServiceUnavailableException, NotAcceptableException; ConflictException, ServiceUnavailableException, NotAcceptableException;
...@@ -217,10 +220,9 @@ public interface MUCRoom { ...@@ -217,10 +220,9 @@ public interface MUCRoom {
/** /**
* Remove a member from the chat room. * Remove a member from the chat room.
* *
* @param nickname The user to remove * @param leaveRole room occupant that left the room.
* @throws UserNotFoundException If the nickname is not found.
*/ */
void leaveRoom(String nickname) throws UserNotFoundException; void leaveRoom(MUCRole leaveRole);
/** /**
* Destroys the room. Each occupant will be removed and will receive a presence stanza of type * Destroys the room. Each occupant will be removed and will receive a presence stanza of type
...@@ -417,13 +419,23 @@ public interface MUCRoom { ...@@ -417,13 +419,23 @@ public interface MUCRoom {
*/ */
public boolean isManuallyLocked(); public boolean isManuallyLocked();
/**
* An event callback fired whenever an occupant updated his presence in the chatroom.
*
* @param occupantRole occupant that changed his presence in the room.
* @param newPresence presence sent by the occupant.
*/
public void presenceUpdated(MUCRole occupantRole, Presence newPresence);
/** /**
* An event callback fired whenever an occupant changes his nickname within the chatroom. * An event callback fired whenever an occupant changes his nickname within the chatroom.
* *
* @param occupantRole occupant that changed his nickname in the room.
* @param newPresence presence sent by the occupant with the new nickname.
* @param oldNick old nickname within the room. * @param oldNick old nickname within the room.
* @param newNick new nickname within the room. * @param newNick new nickname within the room.
*/ */
public void nicknameChanged(String oldNick, String newNick); public void nicknameChanged(MUCRole occupantRole, Presence newPresence, String oldNick, String newNick);
/** /**
* Changes the room's subject if the occupant has enough permissions. The occupant must be * Changes the room's subject if the occupant has enough permissions. The occupant must be
......
...@@ -83,7 +83,7 @@ public final class MUCRoomHistory { ...@@ -83,7 +83,7 @@ public final class MUCRoomHistory {
// Set the Full JID as the "from" attribute // Set the Full JID as the "from" attribute
try { try {
MUCRole role = room.getOccupant(message.getFrom().getResource()); MUCRole role = room.getOccupant(message.getFrom().getResource());
delayElement.addAttribute("from", role.getChatUser().getAddress().toString()); delayElement.addAttribute("from", role.getUserAddress().toString());
} }
catch (UserNotFoundException e) { catch (UserNotFoundException e) {
// Ignore. // Ignore.
...@@ -105,8 +105,7 @@ public final class MUCRoomHistory { ...@@ -105,8 +105,7 @@ public final class MUCRoomHistory {
// Set the Full JID as the "from" attribute // Set the Full JID as the "from" attribute
try { try {
MUCRole role = room.getOccupant(packet.getFrom().getResource()); MUCRole role = room.getOccupant(packet.getFrom().getResource());
delayInformation.addAttribute("from", role.getChatUser().getAddress() delayInformation.addAttribute("from", role.getUserAddress().toString());
.toString());
} }
catch (UserNotFoundException e) { catch (UserNotFoundException e) {
// Ignore. // Ignore.
......
...@@ -11,12 +11,9 @@ ...@@ -11,12 +11,9 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.openfire.ChannelHandler; import org.jivesoftware.openfire.ChannelHandler;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import java.util.Iterator;
/** /**
* The chat user is a separate user abstraction for interacting with * The chat user is a separate user abstraction for interacting with
* the chat server. Centralizing chat users to the Jabber entity that * the chat server. Centralizing chat users to the Jabber entity that
...@@ -32,13 +29,6 @@ import java.util.Iterator; ...@@ -32,13 +29,6 @@ import java.util.Iterator;
*/ */
public interface MUCUser extends ChannelHandler { public interface MUCUser extends ChannelHandler {
/**
* Obtain a user ID (useful for database indexing).
*
* @return The user's id number if any (-1 indicates the implementation doesn't support ids)
*/
long getID();
/** /**
* Obtain the address of the user. The address is used by services like the core * Obtain the address of the user. The address is used by services like the core
* server packet router to determine if a packet should be sent to the handler. * server packet router to determine if a packet should be sent to the handler.
...@@ -48,51 +38,4 @@ public interface MUCUser extends ChannelHandler { ...@@ -48,51 +38,4 @@ public interface MUCUser extends ChannelHandler {
* @return the address of the packet handler. * @return the address of the packet handler.
*/ */
public JID getAddress(); public JID getAddress();
/**
* Obtain the role of the user in a particular room.
*
* @param roomName The name of the room we're interested in
* @return The role the user plays in that room
* @throws NotFoundException if the user does not have a role in the given room
*/
MUCRole getRole(String roomName) throws NotFoundException;
/**
* Get all roles for this user.
*
* @return Iterator over all roles for this user
*/
Iterator<MUCRole> getRoles();
/**
* Adds the role of the user in a particular room.
*
* @param roomName The name of the room.
* @param role The new role of the user.
*/
void addRole(String roomName, MUCRole role);
/**
* Removes the role of the user in a particular room.<p>
*
* Note: PREREQUISITE: A lock on this object has already been obtained.
*
* @param roomName The name of the room we're being removed
*/
void removeRole(String roomName);
/**
* Returns true if the user is currently present in one or more rooms.
*
* @return true if the user is currently present in one or more rooms.
*/
boolean isJoined();
/**
* Get time (in milliseconds from System currentTimeMillis()) since last packet.
*
* @return The time when the last packet was sent from this user
*/
long getLastPacketTime();
} }
\ No newline at end of file
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
package org.jivesoftware.openfire.muc; package org.jivesoftware.openfire.muc;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.xmpp.component.Component; import org.xmpp.component.Component;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.Message; import org.xmpp.packet.Message;
...@@ -235,30 +234,6 @@ public interface MultiUserChatServer extends Component { ...@@ -235,30 +234,6 @@ public interface MultiUserChatServer extends Component {
*/ */
void removeChatRoom(String roomName); void removeChatRoom(String roomName);
/**
* Removes a user from all chat rooms.
*
* @param jabberID The user's normal jid, not the chat nickname jid.
*/
void removeUser(JID jabberID);
/**
* Obtain a chat user by XMPPAddress.
*
* @param userjid The XMPPAddress of the user.
* @return The chatuser corresponding to that XMPPAddress.
* @throws UserNotFoundException If the user is not found and can't be auto-created.
*/
MUCUser getChatUser(JID userjid) throws UserNotFoundException;
/**
* Broadcast a given message to all members of this chat room. The sender is always set to be
* the chatroom.
*
* @param msg The message to broadcast.
*/
void serverBroadcast(String msg);
/** /**
* Returns the total chat time of all rooms combined. * Returns the total chat time of all rooms combined.
* *
......
...@@ -36,11 +36,11 @@ import java.util.List; ...@@ -36,11 +36,11 @@ import java.util.List;
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class IQAdminHandler { public class IQAdminHandler {
private MUCRoomImpl room; private LocalMUCRoom room;
private PacketRouter router; private PacketRouter router;
public IQAdminHandler(MUCRoomImpl chatroom, PacketRouter packetRouter) { public IQAdminHandler(LocalMUCRoom chatroom, PacketRouter packetRouter) {
this.room = chatroom; this.room = chatroom;
this.router = packetRouter; this.router = packetRouter;
} }
...@@ -170,7 +170,7 @@ public class IQAdminHandler { ...@@ -170,7 +170,7 @@ public class IQAdminHandler {
for (MUCRole role : room.getModerators()) { for (MUCRole role : room.getModerators()) {
metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin");
metaData.addAttribute("role", "moderator"); metaData.addAttribute("role", "moderator");
metaData.addAttribute("jid", role.getChatUser().getAddress().toString()); metaData.addAttribute("jid", role.getUserAddress().toString());
metaData.addAttribute("nick", role.getNickname()); metaData.addAttribute("nick", role.getNickname());
metaData.addAttribute("affiliation", role.getAffiliation().toString()); metaData.addAttribute("affiliation", role.getAffiliation().toString());
} }
...@@ -183,7 +183,7 @@ public class IQAdminHandler { ...@@ -183,7 +183,7 @@ public class IQAdminHandler {
for (MUCRole role : room.getParticipants()) { for (MUCRole role : room.getParticipants()) {
metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin");
metaData.addAttribute("role", "participant"); metaData.addAttribute("role", "participant");
metaData.addAttribute("jid", role.getChatUser().getAddress().toString()); metaData.addAttribute("jid", role.getUserAddress().toString());
metaData.addAttribute("nick", role.getNickname()); metaData.addAttribute("nick", role.getNickname());
metaData.addAttribute("affiliation", role.getAffiliation().toString()); metaData.addAttribute("affiliation", role.getAffiliation().toString());
} }
...@@ -219,7 +219,7 @@ public class IQAdminHandler { ...@@ -219,7 +219,7 @@ public class IQAdminHandler {
else { else {
// Get the JID based on the requested nick // Get the JID based on the requested nick
nick = item.attributeValue("nick"); nick = item.attributeValue("nick");
jid = room.getOccupant(nick).getChatUser().getAddress(); jid = room.getOccupant(nick).getUserAddress();
} }
room.lock.writeLock().lock(); room.lock.writeLock().lock();
...@@ -262,8 +262,7 @@ public class IQAdminHandler { ...@@ -262,8 +262,7 @@ public class IQAdminHandler {
if (MUCRole.Role.moderator != senderRole.getRole()) { if (MUCRole.Role.moderator != senderRole.getRole()) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
presences.add(room.kickOccupant(jid, presences.add(room.kickOccupant(jid, senderRole.getUserAddress(),
senderRole.getChatUser().getAddress(),
item.elementTextTrim("reason"))); item.elementTextTrim("reason")));
} }
} }
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
package org.jivesoftware.openfire.muc.spi; package org.jivesoftware.openfire.muc.spi;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.forms.DataForm; import org.jivesoftware.openfire.forms.DataForm;
import org.jivesoftware.openfire.forms.FormField; import org.jivesoftware.openfire.forms.FormField;
import org.jivesoftware.openfire.forms.spi.XDataFormImpl; import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
...@@ -18,18 +22,16 @@ import org.jivesoftware.openfire.forms.spi.XFormFieldImpl; ...@@ -18,18 +22,16 @@ import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.openfire.muc.ConflictException; import org.jivesoftware.openfire.muc.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException; import org.jivesoftware.openfire.muc.ForbiddenException;
import org.jivesoftware.openfire.muc.MUCRole; import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.openfire.muc.cluster.RoomUpdatedEvent;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
import java.util.*; import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.CacheFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.xmpp.packet.Presence;
import org.xmpp.packet.JID;
import org.xmpp.packet.IQ; import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError; import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;
import java.util.*;
/** /**
* A handler for the IQ packet with namespace http://jabber.org/protocol/muc#owner. This kind of * A handler for the IQ packet with namespace http://jabber.org/protocol/muc#owner. This kind of
...@@ -39,7 +41,7 @@ import org.xmpp.packet.PacketError; ...@@ -39,7 +41,7 @@ import org.xmpp.packet.PacketError;
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class IQOwnerHandler { public class IQOwnerHandler {
private MUCRoomImpl room; private LocalMUCRoom room;
private PacketRouter router; private PacketRouter router;
...@@ -47,7 +49,7 @@ public class IQOwnerHandler { ...@@ -47,7 +49,7 @@ public class IQOwnerHandler {
private Element probeResult; private Element probeResult;
public IQOwnerHandler(MUCRoomImpl chatroom, PacketRouter packetRouter) { public IQOwnerHandler(LocalMUCRoom chatroom, PacketRouter packetRouter) {
this.room = chatroom; this.room = chatroom;
this.router = packetRouter; this.router = packetRouter;
init(); init();
...@@ -209,7 +211,7 @@ public class IQOwnerHandler { ...@@ -209,7 +211,7 @@ public class IQOwnerHandler {
else { else {
// Get the bare JID based on the requested nick // Get the bare JID based on the requested nick
nick = item.attributeValue("nick"); nick = item.attributeValue("nick");
bareJID = room.getOccupant(nick).getChatUser().getAddress().toBareJID(); bareJID = room.getOccupant(nick).getUserAddress().toBareJID();
} }
jids.put(bareJID, affiliation); jids.put(bareJID, affiliation);
} }
...@@ -313,6 +315,10 @@ public class IQOwnerHandler { ...@@ -313,6 +315,10 @@ public class IQOwnerHandler {
if (room.isLocked() && !room.isManuallyLocked()) { if (room.isLocked() && !room.isManuallyLocked()) {
room.unlock(senderRole); room.unlock(senderRole);
} }
if (!room.isDestroyed) {
// Let other cluster nodes that the room has been updated
CacheFactory.doClusterTask(new RoomUpdatedEvent(room));
}
} }
} }
......
...@@ -12,12 +12,12 @@ ...@@ -12,12 +12,12 @@
package org.jivesoftware.openfire.muc.spi; package org.jivesoftware.openfire.muc.spi;
import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.openfire.PacketRouter; import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.muc.MUCRole; import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.MUCRoom; import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.muc.MultiUserChatServer; import org.jivesoftware.openfire.muc.MultiUserChatServer;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
...@@ -144,7 +144,7 @@ public class MUCPersistenceManager { ...@@ -144,7 +144,7 @@ public class MUCPersistenceManager {
* *
* @param room the room to load from the database if persistent * @param room the room to load from the database if persistent
*/ */
public static void loadFromDB(MUCRoomImpl room) { public static void loadFromDB(LocalMUCRoom room) {
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
try { try {
...@@ -294,7 +294,7 @@ public class MUCPersistenceManager { ...@@ -294,7 +294,7 @@ public class MUCPersistenceManager {
* *
* @param room The room to save its configuration. * @param room The room to save its configuration.
*/ */
public static void saveToDB(MUCRoomImpl room) { public static void saveToDB(LocalMUCRoom room) {
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
try { try {
...@@ -415,19 +415,19 @@ public class MUCPersistenceManager { ...@@ -415,19 +415,19 @@ public class MUCPersistenceManager {
* @param packetRouter the PacketRouter that loaded rooms will use to send packets. * @param packetRouter the PacketRouter that loaded rooms will use to send packets.
* @return a collection with all the persistent rooms. * @return a collection with all the persistent rooms.
*/ */
public static Collection<MUCRoom> loadRoomsFromDB(MultiUserChatServer chatserver, public static Collection<LocalMUCRoom> loadRoomsFromDB(MultiUserChatServer chatserver,
Date emptyDate, PacketRouter packetRouter) { Date emptyDate, PacketRouter packetRouter) {
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
Map<Long,MUCRoom> rooms = new HashMap<Long,MUCRoom>(); Map<Long, LocalMUCRoom> rooms = new HashMap<Long, LocalMUCRoom>();
try { try {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_ALL_ROOMS); pstmt = con.prepareStatement(LOAD_ALL_ROOMS);
pstmt.setString(1, StringUtils.dateToMillis(emptyDate)); pstmt.setString(1, StringUtils.dateToMillis(emptyDate));
ResultSet rs = pstmt.executeQuery(); ResultSet rs = pstmt.executeQuery();
MUCRoomImpl room = null; LocalMUCRoom room = null;
while (rs.next()) { while (rs.next()) {
room = new MUCRoomImpl(chatserver, rs.getString(4), packetRouter); room = new LocalMUCRoom(chatserver, rs.getString(4), packetRouter);
room.setID(rs.getLong(1)); room.setID(rs.getLong(1));
room.setCreationDate(new Date(Long.parseLong(rs.getString(2).trim()))); // creation date room.setCreationDate(new Date(Long.parseLong(rs.getString(2).trim()))); // creation date
room.setModificationDate(new Date(Long.parseLong(rs.getString(3).trim()))); // modification date room.setModificationDate(new Date(Long.parseLong(rs.getString(3).trim()))); // modification date
...@@ -478,7 +478,7 @@ public class MUCPersistenceManager { ...@@ -478,7 +478,7 @@ public class MUCPersistenceManager {
// Load the rooms conversations from the last two days // Load the rooms conversations from the last two days
rs = pstmt.executeQuery(); rs = pstmt.executeQuery();
while (rs.next()) { while (rs.next()) {
room = (MUCRoomImpl) rooms.get(rs.getLong(1)); room = (LocalMUCRoom) rooms.get(rs.getLong(1));
// Skip to the next position if the room does not exist // Skip to the next position if the room does not exist
if (room == null) { if (room == null) {
continue; continue;
...@@ -521,7 +521,7 @@ public class MUCPersistenceManager { ...@@ -521,7 +521,7 @@ public class MUCPersistenceManager {
long roomID = rs.getLong(1); long roomID = rs.getLong(1);
String jid = rs.getString(2); String jid = rs.getString(2);
MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(rs.getInt(3)); MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(rs.getInt(3));
room = (MUCRoomImpl) rooms.get(roomID); room = (LocalMUCRoom) rooms.get(roomID);
// Skip to the next position if the room does not exist // Skip to the next position if the room does not exist
if (room == null) { if (room == null) {
continue; continue;
...@@ -552,7 +552,7 @@ public class MUCPersistenceManager { ...@@ -552,7 +552,7 @@ public class MUCPersistenceManager {
pstmt = con.prepareStatement(LOAD_ALL_MEMBERS); pstmt = con.prepareStatement(LOAD_ALL_MEMBERS);
rs = pstmt.executeQuery(); rs = pstmt.executeQuery();
while (rs.next()) { while (rs.next()) {
room = (MUCRoomImpl) rooms.get(rs.getLong(1)); room = (LocalMUCRoom) rooms.get(rs.getLong(1));
// Skip to the next position if the room does not exist // Skip to the next position if the room does not exist
if (room == null) { if (room == null) {
continue; continue;
...@@ -626,7 +626,7 @@ public class MUCPersistenceManager { ...@@ -626,7 +626,7 @@ public class MUCPersistenceManager {
* *
* @param room the room to update its lock status in the database. * @param room the room to update its lock status in the database.
*/ */
public static void updateRoomLock(MUCRoomImpl room) { public static void updateRoomLock(LocalMUCRoom room) {
if (!room.isPersistent() || !room.wasSavedToDB()) { if (!room.isPersistent() || !room.wasSavedToDB()) {
return; return;
} }
......
...@@ -73,8 +73,7 @@ public class ClientRoute implements Cacheable, Externalizable { ...@@ -73,8 +73,7 @@ public class ClientRoute implements Cacheable, Externalizable {
nodeID = XMPPServer.getInstance().getNodeID(); nodeID = XMPPServer.getInstance().getNodeID();
} }
else { else {
// TODO Keep singleton instances in NodeID nodeID = NodeID.getInstance(bytes);
nodeID = new NodeID(bytes);
} }
available = ExternalizableUtil.getInstance().readBoolean(in); available = ExternalizableUtil.getInstance().readBoolean(in);
} }
......
...@@ -134,6 +134,16 @@ public class CacheFactory { ...@@ -134,6 +134,16 @@ public class CacheFactory {
} }
} }
/**
* Returns a byte[] that uniquely identifies this senior cluster member or <tt>null</tt>
* when not in a cluster.
*
* @return a byte[] that uniquely identifies this senior cluster member or null when not in a cluster.
*/
public static byte[] getSeniorClusterMemberID() {
return cacheFactoryStrategy.getSeniorClusterMemberID();
}
/** /**
* Returns true if this member is the senior member in the cluster. If clustering * Returns true if this member is the senior member in the cluster. If clustering
* is not enabled, this method will also return true. This test is useful for * is not enabled, this method will also return true. This test is useful for
......
...@@ -52,6 +52,14 @@ public interface CacheFactoryStrategy { ...@@ -52,6 +52,14 @@ public interface CacheFactoryStrategy {
*/ */
boolean isSeniorClusterMember(); boolean isSeniorClusterMember();
/**
* Returns a byte[] that uniquely identifies this senior cluster member or <tt>null</tt>
* when not in a cluster.
*
* @return a byte[] that uniquely identifies this senior cluster member or null when not in a cluster.
*/
byte[] getSeniorClusterMemberID();
/** /**
* Returns a byte[] that uniquely identifies this member within the cluster or <tt>null</tt> * Returns a byte[] that uniquely identifies this member within the cluster or <tt>null</tt>
* when not in a cluster. * when not in a cluster.
......
...@@ -159,6 +159,10 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy { ...@@ -159,6 +159,10 @@ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy {
return true; return true;
} }
public byte[] getSeniorClusterMemberID() {
return null;
}
public byte[] getClusterMemberID() { public byte[] getClusterMemberID() {
return new byte[0]; return new byte[0];
} }
......
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
- Use is subject to license terms. - Use is subject to license terms.
--%> --%>
<%@ page import="org.jivesoftware.util.ParamUtils, <%@ page import="org.jivesoftware.openfire.muc.MUCRole,
org.jivesoftware.openfire.muc.MUCRole,
org.jivesoftware.openfire.muc.MUCRoom, org.jivesoftware.openfire.muc.MUCRoom,
org.jivesoftware.util.ParamUtils,
java.net.URLEncoder, java.net.URLEncoder,
java.text.DateFormat" java.text.DateFormat"
errorPage="error.jsp" errorPage="error.jsp"
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
<tbody> <tbody>
<% for (MUCRole role : room.getOccupants()) { %> <% for (MUCRole role : room.getOccupants()) { %>
<tr> <tr>
<td><%= role.getChatUser().getAddress() %></td> <td><%= role.getUserAddress() %></td>
<td><%= role.getNickname() %></td> <td><%= role.getNickname() %></td>
<td><%= role.getRole() %></td> <td><%= role.getRole() %></td>
<td><%= role.getAffiliation() %></td> <td><%= role.getAffiliation() %></td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment