org.jivesoftware.openfire.muc.HistoryStrategy Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* 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.jivesoftware.openfire.muc;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.jivesoftware.openfire.muc.cluster.UpdateHistoryStrategy;
import org.jivesoftware.openfire.muc.spi.MUCPersistenceManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.Message;
/**
* Multi-User Chat rooms may cache history of the conversations in the room in order to
* play them back to newly arriving members.
*
* This class is an internal component of MUCRoomHistory that describes the strategy that can
* be used, and provides a method of administering the history behavior.
*
* @author Gaston Dombiak
* @author Derek DeMoro
*/
public class HistoryStrategy {
private static final Logger Log = LoggerFactory.getLogger(HistoryStrategy.class);
/**
* The type of strategy being used.
*/
private Type type = Type.number;
/**
* List containing the history of messages.
*/
private ConcurrentLinkedQueue history = new ConcurrentLinkedQueue<>();
/**
* Default max number.
*/
private static final int DEFAULT_MAX_NUMBER = 25;
/**
* The maximum number of chat history messages stored for the room.
*/
private int maxNumber;
/**
* The parent history used for default settings, or null if no parent
* (chat server defaults).
*/
private HistoryStrategy parent;
/**
* Track the latest room subject change or null if none exists yet.
*/
private Message roomSubject = null;
/**
* The string prefix to be used on the context property names
* (do not include trailing dot).
*/
private String contextPrefix = null;
/**
* The subdomain of the service the properties are set on.
*/
private String contextSubdomain = null;
/**
* Create a history strategy with the given parent strategy (for defaults) or null if no
* parent exists.
*
* @param parentStrategy The parent strategy of this strategy or null if none exists.
*/
public HistoryStrategy(HistoryStrategy parentStrategy) {
this.parent = parentStrategy;
if (parent == null) {
maxNumber = DEFAULT_MAX_NUMBER;
}
else {
type = Type.defaulType;
maxNumber = parent.getMaxNumber();
}
}
/**
* Obtain the maximum number of messages for strategies using message number limitations.
*
* @return The maximum number of messages to store in applicable strategies.
*/
public int getMaxNumber() {
return maxNumber;
}
/**
* Set the maximum number of messages for strategies using message number limitations.
*
* @param max the maximum number of messages to store in applicable strategies.
*/
public void setMaxNumber(int max) {
if (maxNumber == max) {
// Do nothing since value has not changed
return;
}
this.maxNumber = max;
if (contextPrefix != null){
MUCPersistenceManager.setProperty(contextSubdomain, contextPrefix + ".maxNumber", Integer.toString(maxNumber));
}
if (parent == null) {
// Update the history strategy of the MUC service
CacheFactory.doClusterTask(new UpdateHistoryStrategy(contextSubdomain, this));
}
}
/**
* Set the type of history strategy being used.
*
* @param newType The new type of chat history to use.
*/
public void setType(Type newType){
if (type == newType) {
// Do nothing since value has not changed
return;
}
if (newType != null){
type = newType;
}
if (contextPrefix != null){
MUCPersistenceManager.setProperty(contextSubdomain, contextPrefix + ".type", type.toString());
}
if (parent == null) {
// Update the history strategy of the MUC service
CacheFactory.doClusterTask(new UpdateHistoryStrategy(contextSubdomain, this));
}
}
/**
* Obtain the type of history strategy being used.
*
* @return The current type of strategy being used.
*/
public Type getType(){
return type;
}
/**
* Add a message to the current chat history. The strategy type will determine what
* actually happens to the message.
*
* @param packet The packet to add to the chatroom's history.
*/
public void addMessage(Message packet){
// get the conditions based on default or not
Type strategyType;
int strategyMaxNumber;
if (type == Type.defaulType && parent != null) {
strategyType = parent.getType();
strategyMaxNumber = parent.getMaxNumber();
}
else {
strategyType = type;
strategyMaxNumber = maxNumber;
}
// Room subject change messages are special
boolean subjectChange = isSubjectChangeRequest(packet);
if (subjectChange) {
roomSubject = packet;
return;
}
// store message according to active strategy
if (strategyType == Type.all) {
history.add(packet);
}
else if (strategyType == Type.number) {
if (history.size() >= strategyMaxNumber) {
// We have to remove messages so the new message won't exceed
// the max history size
// This is complicated somewhat because we must skip over the
// last room subject
// message because we want to preserve the room subject if
// possible.
Iterator historyIter = history.iterator();
while (historyIter.hasNext() && history.size() > strategyMaxNumber) {
if (historyIter.next() != roomSubject) {
historyIter.remove();
}
}
}
history.add(packet);
}
}
boolean isHistoryEnabled() {
Type strategyType = type;
if (type == Type.defaulType && parent != null) {
strategyType = parent.getType();
}
return strategyType != HistoryStrategy.Type.none;
}
/**
* Obtain the current history as an iterator of messages to play back to a new room member.
*
* @return An iterator of Message objects to be sent to the new room member.
*/
public Iterator getMessageHistory(){
LinkedList list = new LinkedList<>(history);
// Sort messages. Messages may be out of order when running inside of a cluster
Collections.sort(list, new MessageComparator());
return list.iterator();
}
/**
* Obtain the current history to be iterated in reverse mode. This means that the returned list
* iterator will be positioned at the end of the history so senders of this message must
* traverse the list in reverse mode.
*
* @return A list iterator of Message objects positioned at the end of the list.
*/
public ListIterator getReverseMessageHistory(){
LinkedList list = new LinkedList<>(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());
}
/**
* Strategy type.
*/
public enum Type {
defaulType, none, all, number;
}
/**
* Obtain the strategy type from string name. See the Type enumeration name
* strings for the names strings supported. If nothing matches
* and parent is not null, then the default strategy is used. Otherwise the number
* strategy is used.
*
* @param typeName the text name of the strategy type.
*/
public void setTypeFromString(String typeName) {
try {
type = Type.valueOf(typeName);
}
catch (Exception e) {
if (parent != null) {
type = Type.defaulType;
}
else {
type = Type.number;
}
}
}
/**
* Sets the prefix to use for retrieving and saving settings (and also
* triggers an immediate loading of properties).
*
* @param subdomain the subdomain of the muc service to pull properties for.
* @param prefix the prefix to use (without trailing dot) on property names.
*/
public void setContext(String subdomain, String prefix) {
this.contextSubdomain = subdomain;
this.contextPrefix = prefix;
setTypeFromString(MUCPersistenceManager.getProperty(subdomain, prefix + ".type"));
String maxNumberString = MUCPersistenceManager.getProperty(subdomain, prefix + ".maxNumber");
if (maxNumberString != null && maxNumberString.trim().length() > 0){
try {
this.maxNumber = Integer.parseInt(maxNumberString);
}
catch (Exception e){
Log.info("Jive property " + prefix + ".maxNumber not a valid number.");
}
}
}
/**
* Returns true if there is a message within the history of the room that has changed the
* room's subject.
*
* @return true if there is a message within the history of the room that has changed the
* room's subject.
*/
public boolean hasChangedSubject() {
return roomSubject != null;
}
/**
* Returns the message within the history of the room that has changed the
* room's subject.
*
* @return the latest room subject change or null if none exists yet.
*/
public Message getChangedSubject() {
return roomSubject;
}
/**
* Returns true if the given message qualifies as a subject change request for
* the target MUC room, per XEP-0045. Note that this does not validate whether
* the sender has permission to make the change, because subject change requests
* may be loaded from history or processed "live" during a user's session.
*
* Refer to http://xmpp.org/extensions/xep-0045.html#subject-mod for details.
*
* @return true if the given packet is a subject change request
*/
public boolean isSubjectChangeRequest(Message message) {
// The subject is changed by sending a message of type "groupchat" to the ,
// where the MUST contain a element that specifies the new subject
// but MUST NOT contain a element (or a element).
// Unfortunately, many clients do not follow these strict guidelines from the specs, so we
// allow a lenient policy for detecting non-conforming subject change requests. This can be
// configured by setting the "xmpp.muc.subject.change.strict" property to false (true by default).
// An empty value means that the room subject should be removed.
return Message.Type.groupchat == message.getType() &&
message.getSubject() != null &&
(!isSubjectChangeStrict() ||
(message.getBody() == null &&
message.getThread() == null));
}
private boolean isSubjectChangeStrict() {
return JiveGlobals.getBooleanProperty("xmpp.muc.subject.change.strict", true);
}
private static class MessageComparator implements Comparator {
@Override
public int compare(Message o1, Message o2) {
String stamp1 = o1.getChildElement("delay", "urn:xmpp:delay").attributeValue("stamp");
String stamp2 = o2.getChildElement("delay", "urn:xmpp:delay").attributeValue("stamp");
return stamp1.compareTo(stamp2);
}
}
}