Skip to content

Commit

Permalink
Implement 'clear room history' event
Browse files Browse the repository at this point in the history
Openfire 5.0.0 introduces a 'clear room history' event. In this commit, the monitoring plugin implements that event, by:
- deleting associated database content
- removing deleted content from the Lucene indices.

Fixes #369
Fixes #370
  • Loading branch information
guusdk committed Jan 2, 2025
1 parent e9add72 commit e7c1870
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 6 deletions.
2 changes: 2 additions & 0 deletions changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ <h1>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/401'>Issue #401</a>] - Fixes: Update Jersey from 2.35 to 2.45</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/398'>Issue #398</a>] - Fixes: Missing translation for system property</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/392'>Issue #392</a>] - Fixes: Compatibility issue with Openfire 5.0.0</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/370'>Issue #370</a>] - Add option to delete history on room deletion</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/369'>Issue #369</a>] - Add option to clear history for a given MUC</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/363'>Issue #363</a>] - Fixes SQL Server error: An expression of non-boolean type specified in a context where a condition is expected, near 'RowNum'</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/357'>Issue #357</a>] - Fixes: Error on admin console after user session expired.</li>
<li>[<a href='https://github.com/igniterealtime/openfire-monitoring-plugin/issues/354'>Issue #354</a>] - Allow full-text search / indexing to be disabled</li>
Expand Down
36 changes: 34 additions & 2 deletions src/java/com/reucon/openfire/plugin/archive/impl/MucIndexer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008 Jive Software, 2024 Ignite Realtime Foundation. All rights reserved.
* Copyright (C) 2008 Jive Software, 2024-2025 Ignite Realtime Foundation. 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.
Expand All @@ -17,6 +17,7 @@

import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.archive.ConversationManager;
import org.jivesoftware.openfire.archive.MonitoringConstants;
Expand All @@ -36,6 +37,8 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
* Creates and maintains a Lucene index for messages exchanged in multi-user chat.
Expand All @@ -55,12 +58,27 @@ public class MucIndexer extends LuceneIndexer

private ConversationManager conversationManager;

/**
* A collection of rooms that are to be removed from the index during the next update or rebuild operation.
*/
private final Set<Long> roomsPendingDeletion = new HashSet<>();

public MucIndexer( final TaskEngine taskEngine, final ConversationManager conversationManager )
{
super(taskEngine, JiveGlobals.getHomePath().resolve(Path.of(MonitoringConstants.NAME, "mucsearch")), "MUCSEARCH", SCHEMA_VERSION);
this.conversationManager = conversationManager;
}

/**
* Schedules documents that relate to the provided room for deletion during the next update cycle.
*
* @param roomID Room for which documents are to be removed from the index.
*/
public void scheduleForDeletion(final Long roomID)
{
roomsPendingDeletion.add(roomID);
}

@Override
protected Instant doUpdateIndex( final IndexWriter writer, final Instant lastModified ) throws IOException
{
Expand Down Expand Up @@ -148,6 +166,11 @@ private Instant indexMUCMessages( IndexWriter writer, Instant since )
continue;
}

// Skip rooms that are going to be deleted anyway.
if (roomsPendingDeletion.contains(roomID)) {
continue;
}

// Index message.
final Document document = createDocument(roomID, messageID, sender, logTime, body );
writer.addDocument(document);
Expand All @@ -164,6 +187,15 @@ private Instant indexMUCMessages( IndexWriter writer, Instant since )
}
}
Log.debug( "... finished the entire result set. Processed {} messages in total.", progress );

if (!since.equals( Instant.EPOCH ) && !roomsPendingDeletion.isEmpty()) {
// In case this is an update instead of a rebuild, older documents may still refer to rooms that are deleted. Remove those.
Log.debug( "... removing documents for {} rooms that are pending deletion.", roomsPendingDeletion.size());
for (long roomID : roomsPendingDeletion) {
writer.deleteDocuments(new Term("roomID", Long.toString(roomID)));
}
}
roomsPendingDeletion.clear();
}
catch (SQLException sqle) {
Log.error("An exception occurred while trying to fetch all MUC messages from the database to rebuild the Lucene index.", sqle);
Expand All @@ -179,7 +211,7 @@ private Instant indexMUCMessages( IndexWriter writer, Instant since )
}

/**
* Creates a index document for one particular chat message.
* Creates an index document for one particular chat message.
*
* @param roomID ID of the MUC room in which the message was exchanged.
* @param messageID ID of the message that was exchanged.
Expand Down
28 changes: 26 additions & 2 deletions src/java/org/jivesoftware/openfire/archive/ArchiveIndexer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008 Jive Software, Ignite Realtime Foundation 2024. All rights reserved.
* Copyright (C) 2008 Jive Software, Ignite Realtime Foundation 2024-2025. 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.
Expand Down Expand Up @@ -61,6 +61,11 @@ public class ArchiveIndexer extends org.jivesoftware.openfire.index.LuceneIndexe
*/
public static final int SCHEMA_VERSION = 1;

/**
* A collection of conversations that are to be removed from the index during the next update or rebuild operation.
*/
private final Set<Long> conversationsPendingDeletion = new HashSet<>();

/**
* Constructs a new archive indexer.
*
Expand All @@ -79,6 +84,16 @@ public void stop()
conversationManager = null;
}

/**
* Schedules documents that relate to the provided conversations for deletion during the next update cycle.
*
* @param conversations Conversations for which documents are to be removed from the index.
*/
public void scheduleForDeletion(final Set<Long> conversations)
{
conversationsPendingDeletion.addAll(conversations);
}

/**
* Updates the index with all new conversation data since the last index update.
*
Expand All @@ -104,6 +119,10 @@ protected Instant doUpdateIndex( final IndexWriter writer, Instant lastModified)
// Load meta-data for each conversation that needs updating.
final SortedMap<Long, Boolean> externalMetaData = extractMetaData(conversationIDs);

// Add those conversations that are scheduled to be deleted.
conversationIDs.addAll(conversationsPendingDeletion);
conversationsPendingDeletion.clear();

// Delete any conversations found -- they may have already been indexed, but updated since then.
Log.debug("... deleting all to-be-updated conversations from the index.");
for (long conversationID : conversationIDs) {
Expand Down Expand Up @@ -137,6 +156,11 @@ public synchronized Instant doRebuildIndex( final IndexWriter writer ) throws IO
}

final SortedMap<Long, Boolean> conversationMetadata = findAllConversations();

// Correct for conversations that are scheduled to be removed.
conversationsPendingDeletion.forEach(conversationMetadata::remove);
conversationsPendingDeletion.clear();

Log.debug("... identified {} conversations.", conversationMetadata.size());
if (conversationMetadata.isEmpty()) {
return Instant.EPOCH;
Expand Down Expand Up @@ -183,7 +207,7 @@ private SortedMap<Long, Boolean> findAllConversations()
}

/**
* Finds converstations that are modified after the specified date.
* Finds conversations that are modified after the specified date.
*
* @param lastModified The date that marks the beginning of the period for which to return conversations. Cannot be null.
* @return A list of conversation identifiers (never null, possibly empty).
Expand Down
103 changes: 103 additions & 0 deletions src/java/org/jivesoftware/openfire/archive/ConversationDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public class ConversationDAO {
private static final String LOAD_MESSAGES = "SELECT fromJID, fromJIDResource, toJID, toJIDResource, sentDate, body, stanza, isPMforJID FROM ofMessageArchive WHERE conversationID=? "
+ "ORDER BY sentDate";

private static final String DELETE_ROOM_MESSAGES = "DELETE FROM ofMessageArchive WHERE conversationID IN (SELECT conversationID FROM ofConversation WHERE roomID=?)";
private static final String DELETE_ROOM_PARTICIPANTS = "DELETE FROM ofConParticipant WHERE conversationID IN (SELECT conversationID FROM ofConversation WHERE roomID=?)";
private static final String DELETE_ROOM_CONVERSATIONS = "DELETE FROM ofConversation WHERE roomID=?";

private static final String CONVERSATIONS_FOR_ROOM = "SELECT DISTINCT conversationID FROM ofConversation WHERE roomID=?";

private static final Logger Log = LoggerFactory.getLogger(ConversationDAO.class);

/**
Expand Down Expand Up @@ -239,6 +245,103 @@ public static List<ArchivedMessage> getMessages(@Nonnull final Conversation conv
return messages;
}

/**
* Removes all recorded chat history (messages) that is stored in the database.
*
* @param roomID the numeric ID for the room
*/
public static void deleteRoomMessages(final long roomID)
{
Log.debug("Removing messages for room {}", roomID);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ROOM_MESSAGES);
pstmt.setLong(1, roomID);
pstmt.executeUpdate();
} catch (SQLException e) {
Log.error("A database error occurred while removing messages for room {}", roomID, e);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}

/**
* Removes all recorded participants (users) that are stored in the database.
*
* @param roomID the numeric ID for the room
*/
public static void deleteRoomParticipants(final long roomID)
{
Log.debug("Removing participants for room {}", roomID);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ROOM_PARTICIPANTS);
pstmt.setLong(1, roomID);
pstmt.executeUpdate();
} catch (SQLException e) {
Log.error("A database error occurred while removing participants for room {}", roomID, e);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}

/**
* Removes all recorded conversations that are stored in the database.
*
* @param roomID the numeric ID for the room
* @return IDs of the removed conversations.
*/
public static Set<Long> deleteRoomConversations(final long roomID)
{
Log.debug("Removing conversations for room {}", roomID);

// JDBC does not support returning data from deleted rows. Instead, this first queries for the data, then deletes it.
final Set<Long> result = getConversationIDsForRoom(roomID);

Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ROOM_CONVERSATIONS);
pstmt.setLong(1, roomID);
pstmt.executeUpdate();
} catch (SQLException e) {
Log.error("A database error occurred while removing conversations for room {}", roomID, e);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return result;
}

public static Set<Long> getConversationIDsForRoom(final long roomID)
{
Log.debug("Getting conversation IDs for room {}", roomID);

final Set<Long> result = new HashSet<>();

Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CONVERSATIONS_FOR_ROOM);
pstmt.setLong(1, roomID);
rs = pstmt.executeQuery();
while (rs.next()) {
result.add(rs.getLong(1));
}
} catch (SQLException e) {
Log.error("A database error occurred while loading conversation IDs for room {}", roomID, e);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return result;
}

private static Conversation loadFromDb(final long conversationID) throws NotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
Expand Down
19 changes: 17 additions & 2 deletions src/java/org/jivesoftware/openfire/archive/ConversationEvent.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008 Jive Software. All rights reserved.
* Copyright (C) 2008 Jive Software, 2025 Ignite Realtime Foundation. 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.
Expand Down Expand Up @@ -76,12 +76,15 @@ public void run(ConversationManager conversationManager) {
else if (Type.roomDestroyed == type) {
conversationManager.roomConversationEnded(roomJID, date);
}
else if (Type.roomClearChatHistory == type) {
conversationManager.clearChatHistory(roomID);
}
else if (Type.occupantJoined == type) {
conversationManager.joinedGroupConversation(roomJID, user, nickname, date);
}
else if (Type.occupantLeft == type) {
conversationManager.leftGroupConversation(roomJID, user, date);
// If there are no more occupants then consider the group conversation over
// If there are no more occupants than consider the group conversation over
MUCRoom mucRoom = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(roomJID).getChatRoom(roomJID.getNode());
if (mucRoom != null && mucRoom.getOccupantsCount() == 0) {
conversationManager.roomConversationEnded(roomJID, date);
Expand Down Expand Up @@ -116,6 +119,14 @@ public static ConversationEvent roomDestroyed(long roomID, JID roomJID, Date dat
return event;
}

public static ConversationEvent roomClearChatHistory(long roomID, JID roomJID) {
ConversationEvent event = new ConversationEvent();
event.type = Type.roomClearChatHistory;
event.roomID = roomID;
event.roomJID = roomJID;
return event;
}

public static ConversationEvent occupantJoined(JID roomJID, JID user, String nickname, Date date) {
ConversationEvent event = new ConversationEvent();
event.type = Type.occupantJoined;
Expand Down Expand Up @@ -164,6 +175,10 @@ private enum Type {
* Event triggered when a room was destroyed.
*/
roomDestroyed,
/**
* Event triggered when historic data for a room is removed.
*/
roomClearChatHistory,
/**
* Event triggered when a new occupant joins a room.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,26 @@ private void removeConversation(String key, Conversation conversation, Date date
}
}

/**
* Removes all recorded data for a particular chat room, including messages, participants and conversations from the
* database, and associated data from the Lucene indices.
*
* @param roomID the numeric ID for the room
*/
public void clearChatHistory(final long roomID) {
ConversationDAO.deleteRoomMessages(roomID);
ConversationDAO.deleteRoomParticipants(roomID);
final Set<Long> deletedConversations = ConversationDAO.deleteRoomConversations(roomID);

final MonitoringPlugin plugin = MonitoringPlugin.getInstance();
if (!deletedConversations.isEmpty()) {
plugin.getArchiveIndexer().scheduleForDeletion(deletedConversations);
plugin.getArchiveIndexer().updateIndex();
}

plugin.getMucIndexer().scheduleForDeletion(roomID);
}

/**
* Returns the group conversation taking place in the specified room or <tt>null</tt> if none.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ public void roomDestroyed(final long roomID, @Nonnull final JID roomJID) {
}
}

@Override
public void roomClearChatHistory(final long roomID, @Nonnull final JID roomJID)
{
// Process this event in the senior cluster member or local JVM when not in a cluster
if (ClusterManager.isSeniorClusterMember()) {
conversationManager.clearChatHistory(roomID);
}
else {
ConversationEventsQueue eventsQueue = conversationManager.getConversationEventsQueue();
eventsQueue.addGroupChatEvent(conversationManager.getRoomConversationKey(roomJID),
ConversationEvent.roomClearChatHistory(roomID, roomJID));
}
}

@Override
public void occupantJoined(JID roomJID, JID user, String nickname) {
// Process this event in the senior cluster member or local JVM when not in a cluster
Expand Down

0 comments on commit e7c1870

Please sign in to comment.