diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 6e737430a..d18b72b2e 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -94,6 +94,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. | | Stable and Unique Stanza IDs | [XEP-0359](http://xmpp.org/extensions/xep-0359.html) | This specification describes unique and stable IDs for messages. | | HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. | +| References | [XEP-0372](http://xmpp.org/extensions/xep-0363.html) | Add references like mentions or external data to stanzas. | | [Spoiler Messages](spoiler.md) | [XEP-0382](http://xmpp.org/extensions/xep-0382.html) | Indicate that the body of a message should be treated as a spoiler | | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. | | [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-XXXX](https://conversations.im/omemo/xep-omemo.html) | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). | diff --git a/documentation/extensions/references.md b/documentation/extensions/references.md new file mode 100644 index 000000000..85096d99a --- /dev/null +++ b/documentation/extensions/references.md @@ -0,0 +1,19 @@ +References +========== + +[Back](index.md) + +References are a way to refer to other entities like users, other messages or external data from within a message. + +Typical use-cases are mentioning other users by name, but referencing to their BareJid, or linking to a sent file. + +## Usage + +Mention a user and link to their bare jid. +``` +Message message = new Message("Alice is a realy nice person."); +BareJid alice = JidCreate.bareFrom("alice@capulet.lit"); +ReferenceManager.addMention(message, 0, 5, alice); +``` + +TODO: Add more use cases (for example for MIX, SIMS...) \ No newline at end of file diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java new file mode 100644 index 000000000..a93bf6bad --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java @@ -0,0 +1,109 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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.smackx.reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.reference.element.ReferenceElement; + +import org.jxmpp.jid.BareJid; + +public final class ReferenceManager extends Manager { + + public static final String NAMESPACE = "urn:xmpp:reference:0"; + + private static final Map INSTANCES = new WeakHashMap<>(); + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + private ReferenceManager(XMPPConnection connection) { + super(connection); + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE); + } + + /** + * Return a new instance of the ReferenceManager for the given connection. + * + * @param connection xmpp connection + * @return reference manager instance + */ + public static ReferenceManager getInstanceFor(XMPPConnection connection) { + ReferenceManager manager = INSTANCES.get(connection); + if (manager == null) { + manager = new ReferenceManager(connection); + INSTANCES.put(connection, manager); + } + return manager; + } + + /** + * Add a reference to another users bare jid to a stanza. + * + * @param stanza stanza. + * @param begin start index of the mention in the messages body. + * @param end end index of the mention in the messages body. + * @param jid referenced jid. + */ + public static void addMention(Stanza stanza, int begin, int end, BareJid jid) { + ReferenceElement reference = new ReferenceElement(begin, end, ReferenceElement.Type.mention, null, + "xmpp:" + jid.toString()); + stanza.addExtension(reference); + } + + /** + * Return a list of all reference extensions contained in a stanza. + * If there are no reference elements, return an empty list. + * + * @param stanza stanza + * @return list of all references contained in the stanza + */ + public static List getReferencesFromStanza(Stanza stanza) { + List references = new ArrayList<>(); + List extensions = stanza.getExtensions(ReferenceElement.ELEMENT, NAMESPACE); + for (ExtensionElement e : extensions) { + references.add((ReferenceElement) e); + } + return references; + } + + /** + * Return true, if the stanza contains at least one reference extension. + * + * @param stanza stanza + * @return true if stanza contains references + */ + public static boolean containsReferences(Stanza stanza) { + return getReferencesFromStanza(stanza).size() > 0; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/ReferenceElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/ReferenceElement.java new file mode 100644 index 000000000..e7de2645d --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/ReferenceElement.java @@ -0,0 +1,141 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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.smackx.reference.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.reference.ReferenceManager; + +public class ReferenceElement implements ExtensionElement { + + public static final String ELEMENT = "reference"; + public static final String ATTR_BEGIN = "begin"; + public static final String ATTR_END = "end"; + public static final String ATTR_TYPE = "type"; + public static final String ATTR_ANCHOR = "anchor"; + public static final String ATTR_URI = "uri"; + + public enum Type { + mention, + data + } + + private final Integer begin; + private final Integer end; + private final Type type; + private final String anchor; + private final String uri; + + // Non-XEP-compliant, but needed for SIMS + private final ExtensionElement child; + + /** + * XEP-incompliant (v0.2) constructor. This is needed for SIMS. + * + * @param begin + * @param end + * @param type + * @param anchor + * @param uri + * @param child + */ + public ReferenceElement(Integer begin, Integer end, Type type, String anchor, String uri, ExtensionElement child) { + if (begin != null && begin < 0) { + throw new IllegalArgumentException("Attribute 'begin' MUST NOT be smaller than 0."); + } + if (end != null && end < 0) { + throw new IllegalArgumentException("Attribute 'end' MUST NOT be smaller than 0."); + } + if (begin != null && end != null && begin >= end) { + throw new IllegalArgumentException("Attribute 'begin' MUST be smaller than attribute 'end'."); + } + if (type == null) { + throw new NullPointerException("Attribute 'type' MUST NOT be null."); + } + // TODO: The uri attribute is not mandatory according to SIMS, but it is according to references. + /*if (uri == null) { + throw new NullPointerException("Attribute 'uri' MUST NOT be null."); + }*/ + this.begin = begin; + this.end = end; + this.type = type; + this.anchor = anchor; + this.uri = uri; + this.child = child; + } + + /** + * XEP-Compliant constructor. + * + * @param begin + * @param end + * @param type + * @param anchor + * @param uri + */ + public ReferenceElement(Integer begin, Integer end, Type type, String anchor, String uri) { + this(begin, end, type, anchor, uri, null); + } + + public Integer getBegin() { + return begin; + } + + public Integer getEnd() { + return end; + } + + public Type getType() { + return type; + } + + public String getAnchor() { + return anchor; + } + + public String getUri() { + return uri; + } + + @Override + public String getNamespace() { + return ReferenceManager.NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this) + .optIntAttribute(ATTR_BEGIN, begin != null ? begin : -1) + .optIntAttribute(ATTR_END, end != null ? end : -1) + .attribute(ATTR_TYPE, type.toString()) + .optAttribute(ATTR_ANCHOR, anchor) + .optAttribute(ATTR_URI, uri); + + if (child == null) { + return xml.closeEmptyElement(); + } else { + return xml.rightAngleBracket() + .append(child.toXML()) + .closeElement(this); + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/package-info.java new file mode 100644 index 000000000..d638a8983 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/element/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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. + */ +/** + * Smack's API for XEP-0372: References. + */ +package org.jivesoftware.smackx.reference.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java new file mode 100644 index 000000000..873fbdf73 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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. + */ +/** + * Smack's API for XEP-0372: References. + */ +package org.jivesoftware.smackx.reference; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/ReferenceProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/ReferenceProvider.java new file mode 100644 index 000000000..0b05d0e9b --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/ReferenceProvider.java @@ -0,0 +1,55 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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.smackx.reference.provider; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.reference.element.ReferenceElement; + +import org.xmlpull.v1.XmlPullParser; + +public class ReferenceProvider extends ExtensionElementProvider { + + public static final ReferenceProvider TEST_PROVIDER = new ReferenceProvider(); + + @Override + public ReferenceElement parse(XmlPullParser parser, int initialDepth) throws Exception { + Integer begin = ParserUtils.getIntegerAttribute(parser, ReferenceElement.ATTR_BEGIN); + Integer end = ParserUtils.getIntegerAttribute(parser, ReferenceElement.ATTR_END); + String typeString = parser.getAttributeValue(null, ReferenceElement.ATTR_TYPE); + ReferenceElement.Type type = ReferenceElement.Type.valueOf(typeString); + String anchor = parser.getAttributeValue(null, ReferenceElement.ATTR_ANCHOR); + String uri = parser.getAttributeValue(null, ReferenceElement.ATTR_URI); + ExtensionElement child = null; + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + ExtensionElementProvider provider = ProviderManager.getExtensionProvider(elementName, namespace); + if (provider != null) { + child = provider.parse(parser); + } + } + if (eventType == XmlPullParser.END_TAG) { + return new ReferenceElement(begin, end, type, anchor, uri, child); + } + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/package-info.java new file mode 100644 index 000000000..48adda67c --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/provider/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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. + */ +/** + * Smack's API for XEP-0372: References. + */ +package org.jivesoftware.smackx.reference.provider; diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 686db8ee6..a47159e88 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -289,6 +289,13 @@ org.jivesoftware.smackx.httpfileupload.provider.FileTooLargeErrorProvider + + + reference + urn:xmpp:reference:0 + org.jivesoftware.smackx.reference.provider.ReferenceProvider + + encryption diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java new file mode 100644 index 000000000..5bb499d9c --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java @@ -0,0 +1,103 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * 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.smackx.reference; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNull; +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smackx.reference.element.ReferenceElement; +import org.jivesoftware.smackx.reference.provider.ReferenceProvider; + +import org.junit.Test; + +public class ReferenceTest extends SmackTestSuite { + + @Test + public void providerMentionTest() throws Exception { + String xml = ""; + ReferenceElement element = new ReferenceElement(72, 78, ReferenceElement.Type.mention, null, + "xmpp:juliet@capulet.lit"); + assertXMLEqual(xml, element.toXML().toString()); + assertEquals(72, (int) element.getBegin()); + assertEquals(78, (int) element.getEnd()); + assertEquals(ReferenceElement.Type.mention, element.getType()); + assertNull(element.getAnchor()); + assertEquals("xmpp:juliet@capulet.lit", element.getUri()); + + ReferenceElement parsed = ReferenceProvider.TEST_PROVIDER.parse(TestUtils.getParser(xml)); + assertXMLEqual(xml, parsed.toXML().toString()); + } + + /** + * TODO: The uri might not be following the XMPP schema. + * That shouldn't matter though. + * @throws Exception + */ + @Test + public void providerDataTest() throws Exception { + String xml = ""; + ReferenceElement element = new ReferenceElement(null, null, ReferenceElement.Type.data, null, + "xmpp:fdp.shakespeare.lit?;node=fdp/submitted/stan.isode.net/accidentreport;item=ndina872be"); + assertXMLEqual(xml, element.toXML().toString()); + + assertNull(element.getBegin()); + assertNull(element.getEnd()); + assertNull(element.getAnchor()); + assertEquals(ReferenceElement.Type.data, element.getType()); + assertEquals("xmpp:fdp.shakespeare.lit?;node=fdp/submitted/stan.isode.net/accidentreport;item=ndina872be", element.getUri()); + + ReferenceElement parsed = ReferenceProvider.TEST_PROVIDER.parse(TestUtils.getParser(xml)); + assertXMLEqual(xml, parsed.toXML().toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void beginGreaterEndIllegalTest() { + new ReferenceElement(100, 10, ReferenceElement.Type.mention, null, "test@test.test"); + } + + @Test(expected = IllegalArgumentException.class) + public void beginSmallerZeroTest() { + new ReferenceElement(-1, 12, ReferenceElement.Type.data, null, "test@test.test"); + } + + @Test(expected = IllegalArgumentException.class) + public void endSmallerZeroTest() { + new ReferenceElement(12, -2, ReferenceElement.Type.mention, null, "test@test.test"); + } + + @Test(expected = NullPointerException.class) + public void typeArgumentNullTest() { + new ReferenceElement(1, 2, null, null, "test@test.test"); + } + + /* + * TODO: Later maybe remove this test in case the uri attribute becomes optional. + @Test(expected = NullPointerException.class) + public void uriArgumentNullTest() { + new ReferenceElement(1, 2, ReferenceElement.Type.mention, null, null); + } + */ +}