From d47cbb28e3613a0b1c59e408e0fa15d74c8053fb Mon Sep 17 00:00:00 2001 From: Damian Minkov Date: Tue, 18 Nov 2014 11:11:20 +0200 Subject: [PATCH] Adds advanced msg history service, to allow plugins to insert messages into the history, and tests for the implementation. --- .../impl/history/HistoryWriterImpl.java | 203 +++++++++++++----- .../msghistory/MessageHistoryServiceImpl.java | 53 +++++ .../service/history/HistoryWriter.java | 15 ++ .../MessageHistoryAdvancedService.java | 39 ++++ .../msghistory/TestMsgHistoryService.java | 22 ++ 5 files changed, 284 insertions(+), 48 deletions(-) create mode 100644 src/net/java/sip/communicator/service/msghistory/MessageHistoryAdvancedService.java diff --git a/src/net/java/sip/communicator/impl/history/HistoryWriterImpl.java b/src/net/java/sip/communicator/impl/history/HistoryWriterImpl.java index 882b435df..2b6c045f3 100644 --- a/src/net/java/sip/communicator/impl/history/HistoryWriterImpl.java +++ b/src/net/java/sip/communicator/impl/history/HistoryWriterImpl.java @@ -145,54 +145,8 @@ private void addRecord(String[] propertyNames, removeFirstRecord(root); } - Element elem = this.currentDoc.createElement("record"); - SimpleDateFormat sdf - = new SimpleDateFormat(DATE_FORMAT); - elem.setAttribute("timestamp", sdf.format(date)); - - for (int i = 0; i < propertyNames.length; i++) - { - String propertyName = propertyNames[i]; - - if(propertyName.endsWith(CDATA_SUFFIX)) - { - if (propertyValues[i] != null) - { - propertyName = - propertyName.replaceFirst(CDATA_SUFFIX, ""); - - Element propertyElement = this.currentDoc - .createElement(propertyName); - - Text value = this.currentDoc - .createCDATASection( - XmlEscapers.xmlContentEscaper().escape( - propertyValues[i].replaceAll("\0", " ") - )); - propertyElement.appendChild(value); - - elem.appendChild(propertyElement); - } - } - else - { - if (propertyValues[i] != null) - { - Element propertyElement = this.currentDoc - .createElement(propertyName); - - Text value = this.currentDoc - .createTextNode( - XmlEscapers.xmlContentEscaper().escape( - propertyValues[i].replaceAll("\0", " ") - )); - propertyElement.appendChild(value); - - elem.appendChild(propertyElement); - } - } - } - + Element elem = createRecord( + this.currentDoc, propertyNames, propertyValues, date); root.appendChild(elem); this.currentDocElements++; } @@ -208,6 +162,66 @@ private void addRecord(String[] propertyNames, } } + /** + * Creates an element. + * @param doc the parent of the element. + * @param propertyNames property names for the element + * @param propertyValues values for the properties + * @param date the of creation of the record + * @return the newly created element. + */ + private Element createRecord(Document doc, + String[] propertyNames, + String[] propertyValues, + Date date) + { + Element elem = doc.createElement("record"); + SimpleDateFormat sdf + = new SimpleDateFormat(DATE_FORMAT); + elem.setAttribute("timestamp", sdf.format(date)); + + for (int i = 0; i < propertyNames.length; i++) + { + String propertyName = propertyNames[i]; + + if(propertyName.endsWith(CDATA_SUFFIX)) + { + if (propertyValues[i] != null) + { + propertyName = + propertyName.replaceFirst(CDATA_SUFFIX, ""); + + Element propertyElement = doc.createElement(propertyName); + + Text value = doc.createCDATASection( + XmlEscapers.xmlContentEscaper().escape( + propertyValues[i].replaceAll("\0", " ") + )); + propertyElement.appendChild(value); + + elem.appendChild(propertyElement); + } + } + else + { + if (propertyValues[i] != null) + { + Element propertyElement = doc.createElement(propertyName); + + Text value = doc.createTextNode( + XmlEscapers.xmlContentEscaper().escape( + propertyValues[i].replaceAll("\0", " ") + )); + propertyElement.appendChild(value); + + elem.appendChild(propertyElement); + } + } + } + + return elem; + } + /** * Finds the oldest node by timestamp in current root and deletes it. * @param root where to search for records @@ -252,6 +266,99 @@ private void removeFirstRecord(Node root) root.removeChild(oldestNode); } + /** + * Stores the passed propertyValues complying with the + * historyRecordStructure. + * + * @param propertyValues + * The values of the record. + * @param timestamp + * The timestamp of the record. + * + * @throws IOException + */ + public void insertRecord( + String[] propertyValues, Date timestamp, String timestampProperty) + throws IOException + { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + Iterator fileIterator + = HistoryReaderImpl.filterFilesByDate( + this.historyImpl.getFileList(), timestamp, null) + .iterator(); + String filename = null; + while (fileIterator.hasNext()) + { + filename = fileIterator.next(); + + Document doc = this.historyImpl.getDocumentForFile(filename); + + if(doc == null) + continue; + + NodeList nodes = doc.getElementsByTagName("record"); + + boolean changed = false; + + Node node; + for (int i = 0; i < nodes.getLength(); i++) + { + node = nodes.item(i); + + Element idNode = XMLUtils.findChild( + (Element)node, timestampProperty); + if(idNode == null) + continue; + + Node nestedNode = idNode.getFirstChild(); + if(nestedNode == null) + continue; + + // Get nested TEXT node's value + String nodeValue = nestedNode.getNodeValue(); + + Date nodeTimeStamp; + try + { + nodeTimeStamp = sdf.parse(nodeValue); + } + catch (ParseException e) + { + nodeTimeStamp = new Date(Long.parseLong(nodeValue)); + } + + if(nodeTimeStamp.before(timestamp)) + continue; + + Element newElem = createRecord( + doc, structPropertyNames, propertyValues, timestamp); + + doc.getFirstChild().insertBefore(newElem, node); + + changed = true; + break; + } + + if(changed) + { + // write changes + synchronized (this.docWriteLock) + { + this.historyImpl.writeFile(filename, doc); + } + + // this prevents that the current writer, which holds + // instance for the last document he is editing will not + // override our last changes to the document + if(filename.equals(this.currentFile)) + { + this.currentDoc = doc; + } + + break; + } + } + } /** * If no file is currently loaded loads the last opened file. If it does not diff --git a/src/net/java/sip/communicator/impl/msghistory/MessageHistoryServiceImpl.java b/src/net/java/sip/communicator/impl/msghistory/MessageHistoryServiceImpl.java index 5ff97c385..cd3872ff3 100644 --- a/src/net/java/sip/communicator/impl/msghistory/MessageHistoryServiceImpl.java +++ b/src/net/java/sip/communicator/impl/msghistory/MessageHistoryServiceImpl.java @@ -43,6 +43,7 @@ */ public class MessageHistoryServiceImpl implements MessageHistoryService, + MessageHistoryAdvancedService, MessageListener, ChatRoomMessageListener, AdHocChatRoomMessageListener, @@ -1485,6 +1486,58 @@ private void writeMessage(History history, String direction, } } + /** + * Inserts message to the history. Allows to update the laready saved + * history. + * @param direction String direction of the message in or out. + * @param source The source Contact + * @param destination The destination Contact + * @param message Message message to be written + * @param messageTimestamp Date this is the timestamp when was message + * received that came from the protocol provider + * @param isSmsSubtype whether message to write is an sms + */ + public void insertMessage( + String direction, + Contact source, + Contact destination, + Message message, + Date messageTimestamp, + boolean isSmsSubtype) + { + try + { + MetaContact metaContact = MessageHistoryActivator + .getContactListService().findMetaContactByContact(destination); + if(metaContact != null + && !isHistoryLoggingEnabled( + metaContact.getMetaUID())) + { + // logging is switched off for this particular contact + return; + } + + History history = this.getHistory(source, destination); + + HistoryWriter historyWriter = history.getWriter(); + SimpleDateFormat sdf + = new SimpleDateFormat(HistoryService.DATE_FORMAT); + historyWriter.insertRecord(new String[]{direction, + message.getContent(), message.getContentType(), + message.getEncoding(), message.getMessageUID(), + message.getSubject(), sdf.format(messageTimestamp), + isSmsSubtype ? MSG_SUBTYPE_SMS : null}, + messageTimestamp, + STRUCTURE_NAMES[6]); + // this date is when the history record to be written + // as we are inserting + + } catch (IOException e) + { + logger.error("Could not add message to history", e); + } + } + // ////////////////////////////////////////////////////////////////////////// /** diff --git a/src/net/java/sip/communicator/service/history/HistoryWriter.java b/src/net/java/sip/communicator/service/history/HistoryWriter.java index 3d952b303..413b300a6 100644 --- a/src/net/java/sip/communicator/service/history/HistoryWriter.java +++ b/src/net/java/sip/communicator/service/history/HistoryWriter.java @@ -69,6 +69,21 @@ public void addRecord(String[] propertyValues, public void addRecord(String[] propertyValues, Date timestamp) throws IOException; + /** + * Stores the passed propertyValues complying with the + * historyRecordStructure. + * + * @param propertyValues + * The values of the record. + * @param timestamp + * The timestamp of the record. + * + * @throws IOException + */ + public void insertRecord( + String[] propertyValues, Date timestamp, String timestampProperty) + throws IOException; + /** * Updates a record by searching for record with idProperty which have idValue * and updating/creating the property with newValue. diff --git a/src/net/java/sip/communicator/service/msghistory/MessageHistoryAdvancedService.java b/src/net/java/sip/communicator/service/msghistory/MessageHistoryAdvancedService.java new file mode 100644 index 000000000..53786cbcf --- /dev/null +++ b/src/net/java/sip/communicator/service/msghistory/MessageHistoryAdvancedService.java @@ -0,0 +1,39 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.service.msghistory; + +import net.java.sip.communicator.service.protocol.*; + +import java.util.*; + +/** + * Adds advanced operation to the message service like inserting/editing + * messages. Can be used to insert messages when synchronizing history with + * external source. + * @author Damian Minkov + */ +public interface MessageHistoryAdvancedService +{ + /** + * Inserts message to the history. Allows to update the already saved + * history. + * @param direction String direction of the message in or out. + * @param source The source Contact + * @param destination The destination Contact + * @param message Message message to be written + * @param messageTimestamp Date this is the timestamp when was message + * received that came from the protocol provider + * @param isSmsSubtype whether message to write is an sms + */ + public void insertMessage( + String direction, + Contact source, + Contact destination, + Message message, + Date messageTimestamp, + boolean isSmsSubtype); +} diff --git a/test/net/java/sip/communicator/slick/msghistory/TestMsgHistoryService.java b/test/net/java/sip/communicator/slick/msghistory/TestMsgHistoryService.java index b81d1e0a8..d78a9121e 100644 --- a/test/net/java/sip/communicator/slick/msghistory/TestMsgHistoryService.java +++ b/test/net/java/sip/communicator/slick/msghistory/TestMsgHistoryService.java @@ -93,6 +93,8 @@ public static Test suite() new TestMsgHistoryService("readRecords")); suite.addTest( new TestMsgHistoryService("specialChars")); + suite.addTest( + new TestMsgHistoryService("insertRecords")); suite.addTest( new TestMsgHistoryService("writeRecordsToMultiChat")); suite.addTest( @@ -436,6 +438,26 @@ public void specialChars() msgs.contains(messagesToSend[6].getContent())); } + /** + * Inserts a message between the control dates and queries to check + * of the expected number of messages. + */ + public void insertRecords() + { + if(!(msgHistoryService instanceof MessageHistoryAdvancedService)) + return; + + ((MessageHistoryAdvancedService)msgHistoryService).insertMessage( + "out", null, testContact, messagesToSend[1], + new Date(controlDate1.getTime() + 50), false); + + Collection rs + = msgHistoryService.findByPeriod( + testMetaContact, controlDate1, controlDate2); + List msgs = getMessages(rs); + assertEquals("Messages must be 3", 3, msgs.size()); + } + private static void waitWrite(long timeout) { synchronized (lock)