diff --git a/pom.xml b/pom.xml
index 11a23df..8fe840f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
com.reucon.openfire.plugins
archive
openfire-plugin
- 1.0.6-SNAPSHOT
+ 1.1.0-SNAPSHOT
Open Archive
http://maven.reucon.com/projects/public/archive
XEP-0136 compliant server side message archive for Openfire.
diff --git a/src/main/database/archive_hsqldb.sql b/src/main/database/archive_hsqldb.sql
index 74faaef..bf7bb29 100644
--- a/src/main/database/archive_hsqldb.sql
+++ b/src/main/database/archive_hsqldb.sql
@@ -2,6 +2,7 @@ CREATE TABLE archiveConversations (
conversationId BIGINT NOT NULL PRIMARY KEY,
startTime BIGINT NOT NULL,
endTime BIGINT NOT NULL,
+ version BIGINT NOT NULL,
ownerJid VARCHAR(255) NOT NULL,
ownerResource VARCHAR(255),
withJid VARCHAR(255) NOT NULL,
@@ -37,4 +38,4 @@ CREATE TABLE archiveMessages (
CREATE INDEX idx_archiveMessages_conversationId ON archiveMessages (conversationId);
CREATE INDEX idx_archiveMessages_time ON archiveMessages (time);
-INSERT INTO ofVersion (name, version) VALUES ('archive', 2);
+INSERT INTO ofVersion (name, version) VALUES ('archive', 3);
diff --git a/src/main/database/archive_mysql.sql b/src/main/database/archive_mysql.sql
index 2501e8d..597d6b2 100644
--- a/src/main/database/archive_mysql.sql
+++ b/src/main/database/archive_mysql.sql
@@ -2,6 +2,7 @@ CREATE TABLE archiveConversations (
conversationId BIGINT NOT NULL,
startTime BIGINT NOT NULL,
endTime BIGINT NOT NULL,
+ version BIGINT NOT NULL,
ownerJid VARCHAR(255) NOT NULL,
ownerResource VARCHAR(255),
withJid VARCHAR(255) NOT NULL,
@@ -56,4 +57,4 @@ CREATE TABLE archivePrefMethods (
PRIMARY KEY (username,methodType)
);
-INSERT INTO ofVersion (name, version) VALUES ('archive', 2);
+INSERT INTO ofVersion (name, version) VALUES ('archive', 3);
diff --git a/src/main/database/archive_postgresql.sql b/src/main/database/archive_postgresql.sql
index 309a4c2..1e4bcde 100644
--- a/src/main/database/archive_postgresql.sql
+++ b/src/main/database/archive_postgresql.sql
@@ -2,6 +2,7 @@ CREATE TABLE archiveConversations (
conversationId BIGINT NOT NULL,
startTime BIGINT NOT NULL,
endTime BIGINT NOT NULL,
+ version BIGINT NOT NULL,
ownerJid VARCHAR(255) NOT NULL,
ownerResource VARCHAR(255),
withJid VARCHAR(255) NOT NULL,
@@ -57,4 +58,4 @@ CREATE TABLE archivePrefMethods (
CONSTRAINT archivePrefMethods_pk PRIMARY KEY (username,methodType)
);
-INSERT INTO ofVersion (name, version) VALUES ('archive', 2);
+INSERT INTO ofVersion (name, version) VALUES ('archive', 3);
diff --git a/src/main/database/archive_sqlserver.sql b/src/main/database/archive_sqlserver.sql
index 87ab807..ddf7fef 100644
--- a/src/main/database/archive_sqlserver.sql
+++ b/src/main/database/archive_sqlserver.sql
@@ -2,6 +2,7 @@ CREATE TABLE archiveConversations (
conversationId BIGINT NOT NULL,
startTime BIGINT NOT NULL,
endTime BIGINT NOT NULL,
+version BIGINT NOT NULL,
ownerJid VARCHAR(255) NOT NULL,
ownerResource VARCHAR(255),
withJid VARCHAR(255) NOT NULL,
@@ -56,4 +57,4 @@ methodUsage INTEGER,
PRIMARY KEY (username,methodType)
);
-INSERT INTO ofVersion (name, version) VALUES ('archive', 2);
+INSERT INTO ofVersion (name, version) VALUES ('archive', 3);
diff --git a/src/main/database/upgrade/3/archive_hsqldb.sql b/src/main/database/upgrade/3/archive_hsqldb.sql
new file mode 100644
index 0000000..a116e4e
--- /dev/null
+++ b/src/main/database/upgrade/3/archive_hsqldb.sql
@@ -0,0 +1,3 @@
+ALTER TABLE archiveConversations add column version BIGINT DEFAULT 0 NOT NULL;
+
+UPDATE ofVersion SET version=3 WHERE name='archive';
diff --git a/src/main/database/upgrade/3/archive_mysql.sql b/src/main/database/upgrade/3/archive_mysql.sql
new file mode 100644
index 0000000..7047571
--- /dev/null
+++ b/src/main/database/upgrade/3/archive_mysql.sql
@@ -0,0 +1,3 @@
+ALTER TABLE archiveConversations add column version BIGINT NOT NULL default 0;
+
+UPDATE ofVersion SET version=3 WHERE name='archive';
diff --git a/src/main/database/upgrade/3/archive_postgresql.sql b/src/main/database/upgrade/3/archive_postgresql.sql
new file mode 100644
index 0000000..7047571
--- /dev/null
+++ b/src/main/database/upgrade/3/archive_postgresql.sql
@@ -0,0 +1,3 @@
+ALTER TABLE archiveConversations add column version BIGINT NOT NULL default 0;
+
+UPDATE ofVersion SET version=3 WHERE name='archive';
diff --git a/src/main/database/upgrade/3/archive_sqlserver.sql b/src/main/database/upgrade/3/archive_sqlserver.sql
new file mode 100644
index 0000000..7047571
--- /dev/null
+++ b/src/main/database/upgrade/3/archive_sqlserver.sql
@@ -0,0 +1,3 @@
+ALTER TABLE archiveConversations add column version BIGINT NOT NULL default 0;
+
+UPDATE ofVersion SET version=3 WHERE name='archive';
diff --git a/src/main/java/com/reucon/openfire/plugin/archive/impl/ArchiveManagerImpl.java b/src/main/java/com/reucon/openfire/plugin/archive/impl/ArchiveManagerImpl.java
index 1eac9a9..5ab6693 100644
--- a/src/main/java/com/reucon/openfire/plugin/archive/impl/ArchiveManagerImpl.java
+++ b/src/main/java/com/reucon/openfire/plugin/archive/impl/ArchiveManagerImpl.java
@@ -137,6 +137,7 @@ private Conversation determineConversation(JID ownerJid, JID withJid, String sub
else
{
conversation.setEnd(archivedMessage.getTime());
+ conversation.setVersion(conversation.getVersion() + 1);
persistenceManager.updateConversationEnd(conversation);
}
}
diff --git a/src/main/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManager.java b/src/main/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManager.java
index 3552e48..11bc98d 100644
--- a/src/main/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManager.java
+++ b/src/main/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManager.java
@@ -1,3 +1,23 @@
+/**
+ *
+ * Copyright (C) 20xx Stefan Reuter + others
+ * Copyright (C) 2012 Taylor Raack
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
package com.reucon.openfire.plugin.archive.impl;
import com.reucon.openfire.plugin.archive.ArchivedMessageConsumer;
@@ -11,10 +31,12 @@
import org.jivesoftware.util.Log;
import java.sql.*;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
+import java.util.TimeZone;
/**
* Manages database persistence.
@@ -29,7 +51,7 @@ public class JdbcPersistenceManager implements PersistenceManager
public static final String SELECT_ALL_MESSAGES =
"SELECT m.messageId,m.time,m.direction,m.type,m.subject,m.body," +
- " c.conversationId,c.startTime,c.endTime," +
+ " c.conversationId,c.startTime,c.endTime,c.version," +
" c.ownerJid,c.ownerResource,c.withJid,c.withResource,c.subject,c.thread " +
"FROM archiveMessages AS m, archiveConversations AS c " +
"WHERE m.conversationId = c.conversationId " +
@@ -41,14 +63,14 @@ public class JdbcPersistenceManager implements PersistenceManager
public static final String CREATE_CONVERSATION =
"INSERT INTO archiveConversations (conversationId,startTime,endTime," +
- " ownerJid,ownerResource,withJid,withResource,subject,thread) " +
- "VALUES (?,?,?,?,?,?,?,?,?)";
+ " ownerJid,ownerResource,withJid,withResource,subject,thread,version) " +
+ "VALUES (?,?,?,?,?,?,?,?,?,0)";
public static final String UPDATE_CONVERSATION_END =
- "UPDATE archiveConversations SET endTime = ? WHERE conversationId = ?";
+ "UPDATE archiveConversations SET endTime = ?, version = ? WHERE conversationId = ?";
public static final String SELECT_CONVERSATIONS =
- "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource,c.withJid,c.withResource," +
+ "SELECT c.conversationId,c.startTime,c.endTime,c.version,c.ownerJid,c.ownerResource,c.withJid,c.withResource," +
" c.subject,c.thread " +
"FROM archiveConversations AS c";
public static final String COUNT_CONVERSATIONS =
@@ -61,7 +83,7 @@ public class JdbcPersistenceManager implements PersistenceManager
public static final String CONVERSATION_WITH_JID_RESOURCE = "c.withResource";
public static final String SELECT_ACTIVE_CONVERSATIONS =
- "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource,withJid,c.withResource," +
+ "SELECT c.conversationId,c.startTime,c.endTime,c.version,c.ownerJid,c.ownerResource,withJid,c.withResource," +
" c.subject,c.thread " +
"FROM archiveConversations AS c WHERE c.endTime > ?";
@@ -129,9 +151,9 @@ public int processAllMessages(ArchivedMessageConsumer callback)
if (conversation == null || !conversation.getId().equals(conversationId))
{
conversation = new Conversation(
- millisToDate(rs.getLong(8)), millisToDate(rs.getLong(9)),
- rs.getString(10), rs.getString(11), rs.getString(12), rs.getString(13),
- rs.getString(14), rs.getString(15));
+ millisToDate(rs.getLong(8)), millisToDate(rs.getLong(9)), rs.getLong(10),
+ rs.getString(11), rs.getString(12), rs.getString(13), rs.getString(14),
+ rs.getString(15), rs.getString(16));
conversation.setId(conversationId);
}
message.setConversation(conversation);
@@ -206,7 +228,8 @@ public boolean updateConversationEnd(Conversation conversation)
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_CONVERSATION_END);
pstmt.setLong(1, dateToMillis(conversation.getEnd()));
- pstmt.setLong(2, conversation.getId());
+ pstmt.setLong(2, conversation.getVersion());
+ pstmt.setLong(3, conversation.getId());
pstmt.executeUpdate();
return true;
@@ -449,6 +472,101 @@ else if (xmppResultSet.getBefore() != null)
}
return conversations;
}
+
+ public List findModifiedConversationsSince(Date date, String ownerJid, String withJid, XmppResultSet xmppResultSet) {
+ final List conversations;
+ final StringBuilder querySB;
+ final StringBuilder whereSB;
+ final StringBuilder limitSB;
+
+ conversations = new ArrayList();
+
+ querySB = new StringBuilder(SELECT_CONVERSATIONS);
+ whereSB = new StringBuilder();
+ limitSB = new StringBuilder();
+
+ String bareWithJid = null;
+ String withJidResource = null;
+
+ if(date != null)
+ {
+ appendWhere(whereSB, CONVERSATION_END_TIME, " >= ?");
+ }
+ if (ownerJid != null)
+ {
+ appendWhere(whereSB, CONVERSATION_OWNER_JID, " = ?");
+ }
+ if (withJid != null)
+ {
+
+
+ // look for resource on with JID
+ String[] parts = withJid.split("\\/");
+ bareWithJid = parts[0];
+
+ appendWhere(whereSB, CONVERSATION_WITH_JID, " = ?");
+
+ if(parts.length > 1) {
+ withJidResource = parts[1];
+ appendWhere(whereSB, CONVERSATION_WITH_JID_RESOURCE, " = ?");
+ }
+ }
+
+ if (xmppResultSet != null)
+ {
+ // TODO - need the "last" identified logic here
+ Integer firstIndex = null;
+ int max = xmppResultSet.getMax() != null ? xmppResultSet.getMax() : DEFAULT_MAX;
+
+ // set as the total count, not just this fragment of collections
+ xmppResultSet.setCount(countModifiedConversationsSince(date, ownerJid, bareWithJid, withJidResource, whereSB.toString()));
+ limitSB.append(" LIMIT ").append(max);
+ }
+
+ if (whereSB.length() != 0)
+ {
+ querySB.append(" WHERE ").append(whereSB);
+ }
+
+ // The server MUST return the changed collections in the chronological order that they were changed (most recent last).
+ querySB.append(" ORDER BY ").append(CONVERSATION_END_TIME);
+ querySB.append(limitSB);
+
+ Connection con = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+ try
+ {
+ con = DbConnectionManager.getConnection();
+ pstmt = con.prepareStatement(querySB.toString());
+ Date queryDate = date;
+ /*if(xmppResultSet.getAfter() != null) {
+ queryDate = new Date(xmppResultSet.getAfter() + 1);
+ }*/
+ bindModifiedConversationSinceParameters(queryDate, ownerJid, bareWithJid, withJidResource, pstmt);
+ rs = pstmt.executeQuery();
+ while (rs.next())
+ {
+ conversations.add(extractConversation(rs));
+ }
+ }
+ catch (SQLException sqle)
+ {
+ Log.error("Error selecting conversations", sqle);
+ }
+ finally
+ {
+ DbConnectionManager.closeConnection(rs, pstmt, con);
+ }
+
+ if (xmppResultSet != null && conversations.size() > 0)
+ {
+ // set to millis since epoch
+ Date endDate = conversations.get(conversations.size() - 1).getEnd();
+ xmppResultSet.setLast(endDate.getTime());
+ }
+ return conversations;
+ }
private void appendWhere(StringBuilder sb, String... fragments)
{
@@ -501,6 +619,45 @@ private int countConversations(Date startDate, Date endDate, String ownerJid, St
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
+
+ private int countModifiedConversationsSince(Date date, String ownerJid, String withBareJid, String withJidResource, String whereClause)
+ {
+ StringBuilder querySB;
+
+ querySB = new StringBuilder(COUNT_CONVERSATIONS);
+ if (whereClause != null && whereClause.length() != 0)
+ {
+ querySB.append(" WHERE ").append(whereClause);
+ }
+
+ Connection con = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+ try
+ {
+ con = DbConnectionManager.getConnection();
+ pstmt = con.prepareStatement(querySB.toString());
+ bindModifiedConversationSinceParameters(date, ownerJid, withBareJid, withJidResource, pstmt);
+ rs = pstmt.executeQuery();
+ if (rs.next())
+ {
+ return rs.getInt(1);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ catch (SQLException sqle)
+ {
+ Log.error("Error counting conversations", sqle);
+ return 0;
+ }
+ finally
+ {
+ DbConnectionManager.closeConnection(rs, pstmt, con);
+ }
+ }
private int countConversationsBefore(Date startDate, Date endDate, String ownerJid, String withJid, Long before, String whereClause)
{
@@ -568,6 +725,29 @@ private int bindConversationParameters(Date startDate, Date endDate, String owne
}
return parameterIndex;
}
+
+ private int bindModifiedConversationSinceParameters(Date date, String ownerJid, String withBareJid, String withJidResource, PreparedStatement pstmt) throws SQLException
+ {
+ int parameterIndex = 1;
+
+ if (date != null)
+ {
+ pstmt.setLong(parameterIndex++, dateToMillis(date));
+ }
+ if (ownerJid != null)
+ {
+ pstmt.setString(parameterIndex++, ownerJid);
+ }
+ if (withBareJid != null)
+ {
+ pstmt.setString(parameterIndex++, withBareJid);
+ }
+ if (withJidResource != null)
+ {
+ pstmt.setString(parameterIndex++, withJidResource);
+ }
+ return parameterIndex;
+ }
public Collection getActiveConversations(int conversationTimeout)
{
@@ -801,8 +981,8 @@ private Conversation extractConversation(ResultSet rs)
id = rs.getLong(1);
conversation = new Conversation(millisToDate(rs.getLong(2)), millisToDate(rs.getLong(3)),
- rs.getString(4), rs.getString(5), rs.getString(6), rs.getString(7),
- rs.getString(8), rs.getString(9));
+ rs.getLong(4), rs.getString(5), rs.getString(6), rs.getString(7), rs.getString(8),
+ rs.getString(9), rs.getString(10));
conversation.setId(id);
return conversation;
}
@@ -844,4 +1024,6 @@ private Date millisToDate(Long millis)
{
return millis == null ? null : new Date(millis);
}
+
+
}
diff --git a/src/main/java/com/reucon/openfire/plugin/archive/model/Conversation.java b/src/main/java/com/reucon/openfire/plugin/archive/model/Conversation.java
index 27a82b8..05d6c8a 100644
--- a/src/main/java/com/reucon/openfire/plugin/archive/model/Conversation.java
+++ b/src/main/java/com/reucon/openfire/plugin/archive/model/Conversation.java
@@ -1,3 +1,24 @@
+/**
+ *
+ * Copyright (C) 20xx Stefan Reuter + others
+ * Copyright (C) 2012 Taylor Raack
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+
package com.reucon.openfire.plugin.archive.model;
import org.jivesoftware.database.JiveID;
@@ -13,6 +34,7 @@ public class Conversation
private Long id;
private final Date start;
private Date end;
+ private Long version;
private final String ownerJid;
private final String ownerResource;
private final String withJid;
@@ -25,14 +47,15 @@ public class Conversation
public Conversation(Date start, String ownerJid, String ownerResource, String withJid, String withResource,
String subject, String thread)
{
- this(start, start, ownerJid, ownerResource, withJid, withResource, subject, thread);
+ this(start, start, 0L, ownerJid, ownerResource, withJid, withResource, subject, thread);
}
- public Conversation(Date start, Date end, String ownerJid, String ownerResource, String withJid, String withResource,
+ public Conversation(Date start, Date end, Long version, String ownerJid, String ownerResource, String withJid, String withResource,
String subject, String thread)
{
this.start = start;
this.end = end;
+ this.version = version;
this.ownerJid = ownerJid;
this.ownerResource = ownerResource;
this.withJid = withJid;
@@ -67,8 +90,16 @@ public void setEnd(Date end)
{
this.end = end;
}
+
+ public Long getVersion() {
+ return version;
+ }
+
+ public void setVersion(Long version) {
+ this.version = version;
+ }
- public String getOwnerJid()
+ public String getOwnerJid()
{
return ownerJid;
}
diff --git a/src/main/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandler.java b/src/main/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandler.java
new file mode 100644
index 0000000..719114b
--- /dev/null
+++ b/src/main/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandler.java
@@ -0,0 +1,97 @@
+/**
+ *
+ * Copyright (C) 2012 Taylor Raack
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package com.reucon.openfire.plugin.archive.xep0136;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.dom4j.Element;
+import org.jivesoftware.openfire.auth.UnauthorizedException;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.JID;
+
+import com.reucon.openfire.plugin.archive.model.Conversation;
+import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
+import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
+
+public class IQModifiedHandler extends AbstractIQHandler {
+
+ public IQModifiedHandler()
+ {
+ super("Message Archiving Modified Handler", "modified");
+ }
+
+ @Override
+ public IQ handleIQ(IQ packet) throws UnauthorizedException {
+ IQ reply = IQ.createResultIQ(packet);
+ ListRequest listRequest = new ListRequest(packet.getChildElement());
+ JID from = packet.getFrom();
+
+ Element modifiedElement = reply.setChildElement("modified", NAMESPACE);
+
+
+ List conversations = modified(from, listRequest);
+ XmppResultSet resultSet = listRequest.getResultSet();
+
+ for (Conversation conversation : conversations)
+ {
+ addConversationElement(modifiedElement, conversation);
+ }
+
+ if (resultSet != null)
+ {
+ modifiedElement.add(resultSet.createResultElement());
+ }
+
+ return reply;
+ }
+
+ private List modified(JID from, ListRequest request)
+ {
+ // need to query the persistence manager for conversations whose since the start date provided (paginated)
+
+ Date start = request.getStart();
+ if(request.getResultSet().getAfter() != null) {
+ start = new Date(request.getResultSet().getAfter() + 1);
+ }
+ return getPersistenceManager().findModifiedConversationsSince(start,
+ from.toBareJID(), request.getWith(), request.getResultSet());
+ }
+
+ private Element addConversationElement(Element modifiedElement, Conversation conversation)
+ {
+
+ // TODO - if removal ever is implemented, then we need to mark as either
+ // changed OR removed
+ // for now, only changed elements are supported
+ Element conversationElement = modifiedElement.addElement("changed");
+
+ StringBuilder builder = new StringBuilder(conversation.getWithJid());
+ if(StringUtils.isNotEmpty(conversation.getWithResource())) {
+ builder.append("/").append(conversation.getWithResource());
+ }
+ conversationElement.addAttribute("with", builder.toString());
+ conversationElement.addAttribute("start", XmppDateUtil.formatDate(conversation.getStart()));
+ conversationElement.addAttribute("version", conversation.getVersion() + "");
+
+ return conversationElement;
+ }
+}
diff --git a/src/main/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136Support.java b/src/main/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136Support.java
index b8c71b5..54f4fdc 100644
--- a/src/main/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136Support.java
+++ b/src/main/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136Support.java
@@ -19,6 +19,7 @@
*/
public class Xep0136Support
{
+ private static final String NAMESPACE_BASE = "urn:xmpp:archive";
private static final String NAMESPACE_AUTO = "urn:xmpp:archive:auto";
final XMPPServer server;
@@ -58,11 +59,16 @@ public IQ handleIQ(IQ packet) throws UnauthorizedException
// support for #ns-manage
iqHandlers.add(new IQListHandler());
iqHandlers.add(new IQRetrieveHandler());
+ iqHandlers.add(new IQModifiedHandler());
+ // TODO -if the remove handler is ever implemented, the IQModifiedHandler must be adjusted to
+ // send back any removed collections
//iqHandlers.add(new IQRemoveHandler());
}
public void start()
{
+ server.getIQDiscoInfoHandler().addServerFeature(NAMESPACE_BASE);
+
for (IQHandler iqHandler : iqHandlers)
{
try
diff --git a/src/main/openfire/plugin.xml b/src/main/openfire/plugin.xml
index ff34359..a7b04d1 100644
--- a/src/main/openfire/plugin.xml
+++ b/src/main/openfire/plugin.xml
@@ -12,7 +12,7 @@
${openfire-plugin.build.date}
3.4.0
archive
- 2
+ 3
gpl
diff --git a/src/test/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManagerTest.java b/src/test/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManagerTest.java
index c7169ab..214f116 100644
--- a/src/test/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManagerTest.java
+++ b/src/test/java/com/reucon/openfire/plugin/archive/impl/JdbcPersistenceManagerTest.java
@@ -38,7 +38,7 @@ public void retrievingCollectionWithBareWIthJidWorks() throws SQLException {
mockStatic(DbConnectionManager.class);
// conversation query mocking
- String expectedCollectionQuery = "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource," +
+ String expectedCollectionQuery = "SELECT c.conversationId,c.startTime,c.endTime,c.version,c.ownerJid,c.ownerResource," +
"c.withJid,c.withResource, c.subject,c.thread FROM archiveConversations AS c WHERE c.ownerJid = ? " +
"AND c.withJid = ? AND c.startTime = ? ";
@@ -107,7 +107,7 @@ public void retrievingCollectionSplitsExactJIDForWithBeforeQuery() throws SQLExc
mockStatic(DbConnectionManager.class);
// conversation query mocking
- String expectedCollectionQuery = "SELECT c.conversationId,c.startTime,c.endTime,c.ownerJid,c.ownerResource," +
+ String expectedCollectionQuery = "SELECT c.conversationId,c.startTime,c.endTime,c.version,c.ownerJid,c.ownerResource," +
"c.withJid,c.withResource, c.subject,c.thread FROM archiveConversations AS c WHERE c.ownerJid = ? " +
"AND c.withJid = ? AND c.withResource = ? AND c.startTime = ? ";
diff --git a/src/test/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandlerTest.java b/src/test/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandlerTest.java
new file mode 100644
index 0000000..97c13f1
--- /dev/null
+++ b/src/test/java/com/reucon/openfire/plugin/archive/xep0136/IQModifiedHandlerTest.java
@@ -0,0 +1,291 @@
+/**
+ *
+ * Copyright (C) 2012 Taylor Raack
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package com.reucon.openfire.plugin.archive.xep0136;
+
+import static org.junit.Assert.assertEquals;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+import org.dom4j.dom.DOMElement;
+import org.dom4j.tree.DefaultElement;
+import org.jivesoftware.openfire.auth.UnauthorizedException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.xmpp.packet.IQ;
+
+import com.reucon.openfire.plugin.archive.ArchivePlugin;
+import com.reucon.openfire.plugin.archive.PersistenceManager;
+import com.reucon.openfire.plugin.archive.model.Conversation;
+import com.reucon.openfire.plugin.archive.util.XmppDateUtil;
+import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest( { ArchivePlugin.class })
+public class IQModifiedHandlerTest {
+
+ private IQModifiedHandler handler;
+
+ private PersistenceManager persistenceManager;
+
+ @Before
+ public void setup() {
+ handler = new IQModifiedHandler();
+
+ ArchivePlugin plugin = mock(ArchivePlugin.class);
+ persistenceManager = mock(PersistenceManager.class);
+ mockStatic(ArchivePlugin.class);
+ when(ArchivePlugin.getInstance()).thenReturn(plugin);
+ when(plugin.getPersistenceManager()).thenReturn(persistenceManager);
+ }
+
+ @Test
+ public void basicEmptyModifiedResults() throws UnauthorizedException {
+
+ // need to capture XMPPResultSet argument
+ int maxResults = 30;
+ String startTime = "1469-07-21T02:56:15Z";
+
+ IQ packet = createModifiedRequest(maxResults, startTime, null);
+
+ final List conversations = new ArrayList();
+
+ mockPersistenceResponse(XmppDateUtil.parseDate(startTime), null, maxResults, conversations);
+
+ // method under test
+ IQ reply = handler.handleIQ(packet);
+
+ // verification
+ assertEquals(IQ.Type.result, reply.getType());
+ assertEquals("thejid", reply.getTo().toString());
+ assertNotNull(reply.getChildElement());
+ Element replyModified = reply.getChildElement();
+ assertEquals("modified", replyModified.getName());
+ assertEquals("urn:xmpp:archive", replyModified.getNamespace().getText());
+
+ // should have one child the set
+ assertEquals(1, replyModified.nodeCount());
+ Element replySetElement = replyModified.element(QName.get("set", XmppResultSet.NAMESPACE));
+ assertNotNull(replySetElement);
+ assertEquals("set", replySetElement.getName());
+ assertEquals("http://jabber.org/protocol/rsm", replySetElement.getNamespace().getText());
+
+ Element countElement = replySetElement.element("count");
+ assertEquals("0", countElement.getText());
+ }
+
+
+
+ @Test
+ public void conversationsReturned() throws UnauthorizedException {
+
+ // need to capture XMPPResultSet argument
+ int maxResults = 30;
+ String startTime = "1469-07-21T02:56:15Z";
+
+ IQ packet = createModifiedRequest(maxResults, startTime, null);
+
+ Conversation conversation1 = new Conversation(date("2012/04/27 14:27:28"), date("2012/04/27 14:27:28"), 2L, "thejid", "theresource", "otherjid", "otherresource", null, null);
+ Conversation conversation2 = new Conversation(date("2012/04/28 12:39:10"), date("2012/04/29 18:34:55"), 4L, "thejid", "theresource", "otherjid2", "otherresource2", null, null);
+ final List conversations = Arrays.asList(new Conversation[] {conversation1, conversation2});
+
+ mockPersistenceResponse(XmppDateUtil.parseDate(startTime), null, maxResults, conversations);
+
+ // method under test
+ IQ reply = handler.handleIQ(packet);
+
+ // verification
+ Element replyElement = reply.getChildElement();
+ assertEquals(IQ.Type.result, reply.getType());
+ assertEquals("thejid", reply.getTo().toString());
+ assertNotNull(reply.getChildElement());
+ Element replyModified = reply.getChildElement();
+ assertEquals("modified", replyModified.getName());
+ assertEquals("urn:xmpp:archive", replyModified.getNamespace().getText());
+
+ // should have one child the set
+ assertEquals(3, replyModified.nodeCount());
+ Element replySetElement = replyModified.element(QName.get("set", XmppResultSet.NAMESPACE));
+ assertNotNull(replySetElement);
+ assertEquals("set", replySetElement.getName());
+ assertEquals("http://jabber.org/protocol/rsm", replySetElement.getNamespace().getText());
+ Element countElement = replySetElement.element("count");
+ assertEquals("2", countElement.getText());
+
+ Element lastElement = replySetElement.element("last");
+ assertEquals(conversation2.getEnd().getTime() + "", lastElement.getText());
+
+ List changed = replyModified.elements("changed");
+ assertEquals(2, changed.size());
+ verifyChanged(conversation1, changed.get(0));
+ verifyChanged(conversation2, changed.get(1));
+ }
+
+ @Test
+ public void conversationsReturnedWithAfterSpecified() throws UnauthorizedException {
+
+ // need to capture XMPPResultSet argument
+ int maxResults = 30;
+ String startTime = "1469-07-21T02:56:15.132Z";
+ String afterTime = "2012-03-01T02:56:15.765Z";
+
+ IQ packet = createModifiedRequest(maxResults, startTime, XmppDateUtil.parseDate(afterTime).getTime() + "");
+
+ Conversation conversation1 = new Conversation(date("2012/04/27 14:27:28"), date("2012/04/27 14:27:28"), 2L, "thejid", "theresource", "otherjid", "otherresource", null, null);
+ Conversation conversation2 = new Conversation(date("2012/04/28 12:39:10"), date("2012/04/29 18:34:55"), 4L, "thejid", "theresource", "otherjid2", "otherresource2", null, null);
+ final List conversations = Arrays.asList(new Conversation[] {conversation1, conversation2});
+
+ // expect the persistence layer to look just after the afterDate
+ Date afterDate = new Date(XmppDateUtil.parseDate(afterTime).getTime() + 1);
+ mockPersistenceResponse(XmppDateUtil.parseDate(startTime), afterDate, maxResults, conversations);
+
+ // method under test
+ IQ reply = handler.handleIQ(packet);
+
+ verify(persistenceManager).findModifiedConversationsSince(eq(afterDate), any(String.class), any(String.class), argThat(hasMaxResults(maxResults)));
+
+
+ // verification
+ Element replyElement = reply.getChildElement();
+ assertEquals(IQ.Type.result, reply.getType());
+ assertEquals("thejid", reply.getTo().toString());
+ assertNotNull(reply.getChildElement());
+ Element replyModified = reply.getChildElement();
+ assertEquals("modified", replyModified.getName());
+ assertEquals("urn:xmpp:archive", replyModified.getNamespace().getText());
+
+ // should have one child the set
+ assertEquals(3, replyModified.nodeCount());
+ Element replySetElement = replyModified.element(QName.get("set", XmppResultSet.NAMESPACE));
+ assertNotNull(replySetElement);
+ assertEquals("set", replySetElement.getName());
+ assertEquals("http://jabber.org/protocol/rsm", replySetElement.getNamespace().getText());
+ Element countElement = replySetElement.element("count");
+ assertEquals("2", countElement.getText());
+
+ Element lastElement = replySetElement.element("last");
+ assertEquals(conversation2.getEnd().getTime() + "", lastElement.getText());
+
+ List changed = replyModified.elements("changed");
+ assertEquals(2, changed.size());
+ verifyChanged(conversation1, changed.get(0));
+ verifyChanged(conversation2, changed.get(1));
+
+ }
+
+ private void verifyChanged(Conversation conversation, Element element) {
+ assertEquals(conversation.getWithJid() + "/" + conversation.getWithResource(), element.attribute("with").getText());
+ assertEquals(dateUTC(conversation.getStart()), element.attribute("start").getText());
+ assertEquals(conversation.getVersion() + "", element.attribute("version").getText());
+ }
+
+ private String dateUTC(Date date) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return sdf.format(date);
+ }
+
+ private Date date(String dateString) {
+ String format = "yyyy/MM/dd HH:mm:ss";
+ try {
+ return new SimpleDateFormat(format, Locale.ENGLISH).parse(dateString);
+ } catch (ParseException e) {
+ throw new RuntimeException("Cannot convert " + dateString + " to " + format, e);
+ }
+ }
+
+ private void mockPersistenceResponse(Date date, Date afterDate, int maxResults, final List conversations) {
+ if(afterDate != null) {
+ date = afterDate;
+ }
+ when(persistenceManager.findModifiedConversationsSince(eq(date), eq("thejid"), isNull(String.class), argThat(hasMaxResults(maxResults))))
+ .thenAnswer(new Answer>() {
+ @Override
+ public List answer(InvocationOnMock invocation) throws Throwable {
+ Object args[] = invocation.getArguments();
+
+ // add empty result set
+ XmppResultSet resultSet = (XmppResultSet)args[3];
+ if(conversations.size() > 0) {
+ resultSet.setLast(conversations.get(conversations.size() - 1).getEnd().getTime());
+ }
+ resultSet.setCount(conversations.size());
+
+ return conversations;
+ }});
+ }
+
+
+ private ArgumentMatcher hasMaxResults(final int maxResults) {
+ return new ArgumentMatcher() {
+ public boolean matches(Object argument) {
+ XmppResultSet resultSet = (XmppResultSet)argument;
+ return resultSet.getMax() == maxResults;
+ }};
+ }
+
+ private IQ createModifiedRequest(int maxResults, String startTime, String afterTime) {
+ Element modifiedElement = new DOMElement("modified", Namespace.get("urn:xmpp:archive"));
+ modifiedElement.addAttribute("start", startTime);
+ Element setElement = new DefaultElement("set", Namespace.get("http://jabber.org/protocol/rsm"));
+
+ Element maxElement = new DefaultElement("max");
+ maxElement.setText(maxResults + "");
+ setElement.add(maxElement);
+ if(afterTime != null) {
+ Element afterElement = new DefaultElement("after");
+ afterElement.setText(afterTime);
+ setElement.add(afterElement);
+ }
+ modifiedElement.add(setElement);
+
+ IQ packet = new IQ(IQ.Type.get, "sync1");
+ packet.setFrom("thejid");
+ packet.setChildElement(modifiedElement);
+ return packet;
+ }
+
+}
diff --git a/src/test/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136SupportTest.java b/src/test/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136SupportTest.java
new file mode 100644
index 0000000..c1a8e14
--- /dev/null
+++ b/src/test/java/com/reucon/openfire/plugin/archive/xep0136/Xep0136SupportTest.java
@@ -0,0 +1,50 @@
+/**
+ *
+ * Copyright (C) 2012 Taylor Raack
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package com.reucon.openfire.plugin.archive.xep0136;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.jivesoftware.openfire.IQRouter;
+import org.jivesoftware.openfire.XMPPServer;
+import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
+import org.junit.Test;
+
+public class Xep0136SupportTest {
+
+ @Test
+ public void featuresSupported() {
+ XMPPServer server = mock(XMPPServer.class);
+ IQDiscoInfoHandler discoInfoHandler = mock(IQDiscoInfoHandler.class);
+ IQRouter router = mock(IQRouter.class);
+
+ when(server.getIQDiscoInfoHandler()).thenReturn(discoInfoHandler);
+ when(server.getIQRouter()).thenReturn(router);
+
+ Xep0136Support support = new Xep0136Support(server);
+
+ support.start();
+
+ verify(discoInfoHandler).addServerFeature("urn:xmpp:archive");
+ verify(discoInfoHandler).addServerFeature("urn:xmpp:archive:manage");
+ verify(discoInfoHandler).addServerFeature("urn:xmpp:archive:auto");
+ }
+}