Implement message stanza <thread/> and <subject/> as ExtensionElement

Fixes SMACK-852.
This commit is contained in:
Florian Schmaus 2020-04-05 17:51:06 +02:00
parent f9fb4d7627
commit 14c351397d
8 changed files with 172 additions and 38 deletions

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2003-2007 Jive Software, 2014-2016 Florian Schmaus
* Copyright 2003-2007 Jive Software, 2014-2020 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,9 +36,12 @@ import org.jivesoftware.smack.compression.zlib.ZlibXmppCompressionFactory;
import org.jivesoftware.smack.initializer.SmackInitializer;
import org.jivesoftware.smack.isr.InstantStreamResumptionModuleDescriptor;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Body;
import org.jivesoftware.smack.provider.BindIQProvider;
import org.jivesoftware.smack.provider.BodyElementProvider;
import org.jivesoftware.smack.provider.MessageSubjectElementProvider;
import org.jivesoftware.smack.provider.MessageThreadElementProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.provider.SaslChallengeProvider;
import org.jivesoftware.smack.provider.SaslFailureProvider;
@ -130,6 +133,8 @@ public final class SmackInitialization {
ProviderManager.addIQProvider(Bind.ELEMENT, Bind.NAMESPACE, new BindIQProvider());
ProviderManager.addExtensionProvider(Body.ELEMENT, Body.NAMESPACE, new BodyElementProvider());
ProviderManager.addExtensionProvider(Message.Thread.ELEMENT, Message.Thread.NAMESPACE, new MessageThreadElementProvider());
ProviderManager.addExtensionProvider(Message.Subject.ELEMENT, Message.Subject.NAMESPACE, new MessageSubjectElementProvider());
ProviderManager.addNonzaProvider(SaslChallengeProvider.INSTANCE);
ProviderManager.addNonzaProvider(SaslSuccessProvider.INSTANCE);

View File

@ -26,9 +26,11 @@ import java.util.Set;
import javax.xml.namespace.QName;
import org.jivesoftware.smack.packet.Message.Thread;
import org.jivesoftware.smack.util.EqualsUtil;
import org.jivesoftware.smack.util.HashCode;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.TypedCloneable;
import org.jivesoftware.smack.util.XmlStringBuilder;
@ -67,7 +69,6 @@ public final class Message extends MessageOrPresence<MessageBuilder>
public static final String BODY = "body";
private Type type;
private String thread = null;
/**
* Creates a new, "normal" message.
@ -150,7 +151,6 @@ public final class Message extends MessageOrPresence<MessageBuilder>
Message(MessageBuilder messageBuilder) {
super(messageBuilder);
type = messageBuilder.type;
thread = messageBuilder.thread;
}
/**
@ -165,7 +165,6 @@ public final class Message extends MessageOrPresence<MessageBuilder>
public Message(Message other) {
super(other);
this.type = other.type;
this.thread = other.thread;
}
@Override
@ -502,7 +501,11 @@ public final class Message extends MessageOrPresence<MessageBuilder>
* @return the thread id of the message, or <code>null</code> if it doesn't exist.
*/
public String getThread() {
return thread;
Message.Thread thread = getExtension(Message.Thread.class);
if (thread == null) {
return null;
}
return thread.getThread();
}
/**
@ -515,7 +518,7 @@ public final class Message extends MessageOrPresence<MessageBuilder>
@Deprecated
// TODO: Remove when stanza builder is ready.
public void setThread(String thread) {
this.thread = thread;
addExtension(new Message.Thread(thread));
}
private String determineLanguage(String language) {
@ -559,7 +562,6 @@ public final class Message extends MessageOrPresence<MessageBuilder>
buf.optAttribute("type", type);
buf.rightAngleBracket();
buf.optElement("thread", thread);
// Append the error subpacket if the message type is an error.
if (type == Type.error) {
appendErrorIfExists(buf);
@ -754,6 +756,60 @@ public final class Message extends MessageOrPresence<MessageBuilder>
}
@SuppressWarnings("JavaLangClash")
public static class Thread implements ExtensionElement {
public static final String ELEMENT = "thread";
public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE;
public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
public static final String PARENT_ATTRIBUTE_NAME = "parent";
private final String thread;
private final String parent;
public Thread(String thread) {
this(thread, null);
}
public Thread(String thread, String parent) {
this.thread = StringUtils.requireNotNullNorEmpty(thread, "thread must not be null nor empty");
this.parent = StringUtils.requireNullOrNotEmpty(parent, "parent must be null or not empty");
}
public String getThread() {
return thread;
}
public String getParent() {
return parent;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public QName getQName() {
return QNAME;
}
@Override
public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
xml.optAttribute(PARENT_ATTRIBUTE_NAME, parent);
xml.rightAngleBracket();
xml.escape(thread);
xml.closeElement(this);
return xml;
}
}
/**
* Represents the type of a message.
*/

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2019 Florian Schmaus
* Copyright 2019-2020 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,8 +29,6 @@ public final class MessageBuilder extends MessageOrPresenceBuilder<Message, Mess
Message.Type type;
String thread;
MessageBuilder(Message message, String stanzaId) {
super(message, stanzaId);
copyFromMessage(message);
@ -51,13 +49,11 @@ public final class MessageBuilder extends MessageOrPresenceBuilder<Message, Mess
private void copyFromMessage(Message message) {
type = message.getType();
thread = message.getThread();
}
@Override
protected void addStanzaSpecificAttributes(ToStringUtil.Builder builder) {
builder.addValue("type", type)
.addValue("thread", thread)
;
}
@ -67,7 +63,11 @@ public final class MessageBuilder extends MessageOrPresenceBuilder<Message, Mess
}
public MessageBuilder setThread(String thread) {
this.thread = thread;
return setThread(thread, null);
}
public MessageBuilder setThread(String thread, String parent) {
addExtension(new Message.Thread(thread, parent));
return getThis();
}

View File

@ -0,0 +1,39 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.smack.provider;
import java.io.IOException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
public class MessageSubjectElementProvider extends ExtensionElementProvider<Message.Subject> {
@Override
public Message.Subject parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
throws XmlPullParserException, IOException, SmackParsingException {
String xmlLangSubject = ParserUtils.getXmlLang(parser);
String subject = parser.nextText();
return new Message.Subject(xmlLangSubject, subject);
}
}

View File

@ -0,0 +1,38 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.smack.provider;
import java.io.IOException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
public class MessageThreadElementProvider extends ExtensionElementProvider<Message.Thread> {
@Override
public Message.Thread parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
throws XmlPullParserException, IOException, SmackParsingException {
String parent = parser.getAttributeValue(Message.Thread.PARENT_ATTRIBUTE_NAME);
String thread = parser.nextText();
return new Message.Thread(thread, parent);
}
}

View File

@ -179,7 +179,6 @@ public class PacketParserUtils {
// Parse sub-elements. We include extra logic to make sure the values
// are only read once. This is because it's possible for the names to appear
// in arbitrary sub-elements.
String thread = null;
outerloop: while (true) {
XmlPullParser.Event eventType = parser.next();
switch (eventType) {
@ -187,18 +186,6 @@ public class PacketParserUtils {
String elementName = parser.getName();
String namespace = parser.getNamespace();
switch (elementName) {
case "subject":
String xmlLangSubject = ParserUtils.getXmlLang(parser);
String subject = parseElementText(parser);
Message.Subject subjectExtensionElement = new Message.Subject(xmlLangSubject, subject);
message.addExtension(subjectExtensionElement);
break;
case "thread":
if (thread == null) {
thread = parser.nextText();
}
break;
case "error":
message.setError(parseError(parser, messageXmlEnvironment));
break;
@ -217,8 +204,6 @@ public class PacketParserUtils {
}
}
message.setThread(thread);
// TODO check for duplicate body elements. This means we need to check for duplicate xml:lang pairs and for
// situations where we have a body element with an explicit xml lang set and once where the value is inherited
// and both values are equal.

View File

@ -223,6 +223,7 @@ public class PacketParserUtilsTest {
// message has default language, subject has no language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -239,10 +240,11 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().isEmpty());
assertEquals(defaultLanguage, message.getSubject(defaultLanguage));
assertNull(message.getSubject(otherLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has non-default language, subject has no language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -258,10 +260,11 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().isEmpty());
assertEquals(otherLanguage, message.getSubject(otherLanguage));
assertNull(message.getSubject(defaultLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has no language, subject has no language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -276,10 +279,11 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().isEmpty());
assertEquals(defaultLanguage, message.getSubject(null));
assertNull(message.getSubject(otherLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has no language, subject has default language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -295,10 +299,11 @@ public class PacketParserUtilsTest {
assertEquals(defaultLanguage, message.getSubject(defaultLanguage));
assertNull(message.getSubject(otherLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has no language, subject has non-default language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -315,10 +320,11 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().contains(otherLanguage));
assertEquals(otherLanguage, message.getSubject(otherLanguage));
assertNull(message.getSubject(defaultLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has default language, subject has non-default language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -336,10 +342,11 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().contains(otherLanguage));
assertEquals(otherLanguage, message.getSubject(otherLanguage));
assertNull(message.getSubject(defaultLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has non-default language, subject has default language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -357,7 +364,7 @@ public class PacketParserUtilsTest {
assertTrue(message.getSubjectLanguages().contains(defaultLanguage));
assertEquals(defaultLanguage, message.getSubject(defaultLanguage));
assertNull(message.getSubject(otherLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
}
@ -523,6 +530,7 @@ public class PacketParserUtilsTest {
// message has default language, first subject no language, second subject other language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -544,10 +552,11 @@ public class PacketParserUtilsTest {
assertEquals(2, message.getSubjects().size());
assertEquals(1, message.getSubjectLanguages().size());
assertTrue(message.getSubjectLanguages().contains(otherLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
// message has non-default language, first subject no language, second subject default language
control = XMLBuilder.create("message")
.ns(StreamOpen.CLIENT_NAMESPACE)
.a("from", "romeo@montague.lit/orchard")
.a("to", "juliet@capulet.lit/balcony")
.a("id", "zid615d9")
@ -569,7 +578,7 @@ public class PacketParserUtilsTest {
assertEquals(2, message.getSubjects().size());
assertEquals(1, message.getSubjectLanguages().size());
assertTrue(message.getSubjectLanguages().contains(defaultLanguage));
assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString());
assertXmlSimilar(control, message.toXML());
/*
// message has no language, first subject no language, second subject default language

View File

@ -336,7 +336,9 @@ public class ChatConnectionTest {
jid = JidTestUtil.DUMMY_AT_EXAMPLE_ORG;
}
chatMsg.from(jid);
chatMsg.setThread(threadId);
if (threadId != null) {
chatMsg.setThread(threadId);
}
return chatMsg;
}