diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers
index 9c83b9ac3..57bea769d 100644
--- a/build/resources/META-INF/smack.providers
+++ b/build/resources/META-INF/smack.providers
@@ -640,4 +640,23 @@
urn:xmpp:attention:0
org.jivesoftware.smackx.packet.AttentionExtension$Provider
-
\ No newline at end of file
+
+
+
+ forwarded
+ urn:xmpp:forward:0
+ org.jivesoftware.smackx.packet.Forwarded$Provider
+
+
+
+
+ sent
+ urn:xmpp:carbons:2
+ org.jivesoftware.smackx.carbons.Carbon$Provider
+
+
+ received
+ urn:xmpp:carbons:2
+ org.jivesoftware.smackx.carbons.Carbon$Provider
+
+
diff --git a/source/org/jivesoftware/smackx/carbons/Carbon.java b/source/org/jivesoftware/smackx/carbons/Carbon.java
new file mode 100644
index 000000000..588688aa1
--- /dev/null
+++ b/source/org/jivesoftware/smackx/carbons/Carbon.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.
+ * 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.carbons;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.forward.Forwarded;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.provider.DelayInfoProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for XEP-0280: Message Carbons. This class implements
+ * the packet extension and a {@link PacketExtensionProvider} to parse
+ * message carbon copies from a packet. The extension
+ * XEP-0280 is
+ * meant to synchronize a message flow to multiple presences of a user.
+ *
+ *
The {@link Carbon.Provider} must be registered in the
+ * smack.properties file for the elements sent and
+ * received with namespace urn:xmpp:carbons:2
to be used.
+ *
+ * @author Georg Lukas
+ */
+public class Carbon implements PacketExtension {
+ public static final String NAMESPACE = "urn:xmpp:carbons:2";
+
+ private Direction dir;
+ private Forwarded fwd;
+
+ public Carbon(Direction dir, Forwarded fwd) {
+ this.dir = dir;
+ this.fwd = fwd;
+ }
+
+ /**
+ * get the direction (sent or received) of the carbon.
+ *
+ * @return the {@link Direction} of the carbon.
+ */
+ public Direction getDirection() {
+ return dir;
+ }
+
+ /**
+ * get the forwarded packet.
+ *
+ * @return the {@link Forwarded} message contained in this Carbon.
+ */
+ public Forwarded getForwarded() {
+ return fwd;
+ }
+
+ @Override
+ public String getElementName() {
+ return dir.toString();
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(" xmlns=\"")
+ .append(getNamespace()).append("\">");
+
+ buf.append(fwd.toXML());
+
+ buf.append("").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * An enum to display the direction of a {@link Carbon} message.
+ */
+ public static enum Direction {
+ received,
+ sent
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ Direction dir = Direction.valueOf(parser.getName());
+ Forwarded fwd = null;
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("forwarded")) {
+ fwd = (Forwarded)new Forwarded.Provider().parseExtension(parser);
+ }
+ else if (eventType == XmlPullParser.END_TAG && dir == Direction.valueOf(parser.getName()))
+ done = true;
+ }
+ if (fwd == null)
+ throw new Exception("sent/received must contain exactly one tag");
+ return new Carbon(dir, fwd);
+ }
+ }
+
+ /**
+ * Packet extension indicating that a message may not be carbon-copied.
+ */
+ public static class Private implements PacketExtension {
+ public static final String ELEMENT = "private";
+
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ public String getNamespace() {
+ return Carbon.NAMESPACE;
+ }
+
+ public String toXML() {
+ return "<" + ELEMENT + " xmlns=\"" + Carbon.NAMESPACE + "\"/>";
+ }
+ }
+}
diff --git a/source/org/jivesoftware/smackx/carbons/CarbonManager.java b/source/org/jivesoftware/smackx/carbons/CarbonManager.java
new file mode 100644
index 000000000..f44701afd
--- /dev/null
+++ b/source/org/jivesoftware/smackx/carbons/CarbonManager.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.
+ * 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.carbons;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+/**
+ * Packet extension for XEP-0280: Message Carbons. This class implements
+ * the manager for registering {@link Carbon} support, enabling and disabling
+ * message carbons.
+ *
+ * You should call enableCarbons() before sending your first undirected
+ * presence.
+ *
+ * @author Georg Lukas
+ */
+public class CarbonManager {
+
+ private static Map instances =
+ Collections.synchronizedMap(new WeakHashMap());
+
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ new CarbonManager(connection);
+ }
+ });
+ }
+
+ private Connection connection;
+ private volatile boolean enabled_state = false;
+
+ private CarbonManager(Connection connection) {
+ ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+ sdm.addFeature(Carbon.NAMESPACE);
+ this.connection = connection;
+ instances.put(connection, this);
+ }
+
+ /**
+ * Obtain the CarbonManager responsible for a connection.
+ *
+ * @param connection the connection object.
+ *
+ * @return a CarbonManager instance
+ */
+ public static CarbonManager getInstanceFor(Connection connection) {
+ CarbonManager carbonManager = instances.get(connection);
+
+ if (carbonManager == null) {
+ carbonManager = new CarbonManager(connection);
+ }
+
+ return carbonManager;
+ }
+
+ private IQ carbonsEnabledIQ(final boolean new_state) {
+ IQ setIQ = new IQ() {
+ public String getChildElementXML() {
+ return "<" + (new_state? "enable" : "disable") + " xmlns='" + Carbon.NAMESPACE + "'/>";
+ }
+ };
+ setIQ.setType(IQ.Type.SET);
+ return setIQ;
+ }
+
+ /**
+ * Returns true if XMPP Carbons are supported by the server.
+ *
+ * @return true if supported
+ */
+ public boolean isSupportedByServer() {
+ try {
+ DiscoverInfo result = ServiceDiscoveryManager
+ .getInstanceFor(connection).discoverInfo(connection.getServiceName());
+ return result.containsFeature(Carbon.NAMESPACE);
+ }
+ catch (XMPPException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Notify server to change the carbons state. This method returns
+ * immediately and changes the variable when the reply arrives.
+ *
+ * You should first check for support using isSupportedByServer().
+ *
+ * @param new_state whether carbons should be enabled or disabled
+ */
+ public void sendCarbonsEnabled(final boolean new_state) {
+ IQ setIQ = carbonsEnabledIQ(new_state);
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ IQ result = (IQ)packet;
+ if (result.getType() == IQ.Type.RESULT) {
+ enabled_state = new_state;
+ }
+ connection.removePacketListener(this);
+ }
+ }, new PacketIDFilter(setIQ.getPacketID()));
+
+ connection.sendPacket(setIQ);
+ }
+
+ /**
+ * Notify server to change the carbons state. This method blocks
+ * some time until the server replies to the IQ and returns true on
+ * success.
+ *
+ * You should first check for support using isSupportedByServer().
+ *
+ * @param new_state whether carbons should be enabled or disabled
+ *
+ * @return true if the operation was successful
+ */
+ public boolean setCarbonsEnabled(final boolean new_state) {
+ if (enabled_state == new_state)
+ return true;
+
+ IQ setIQ = carbonsEnabledIQ(new_state);
+
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(setIQ.getPacketID()));
+ connection.sendPacket(setIQ);
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+
+ if (result != null && result.getType() == IQ.Type.RESULT) {
+ enabled_state = new_state;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Helper method to enable carbons.
+ *
+ * @return true if the operation was successful
+ */
+ public boolean enableCarbons() {
+ return setCarbonsEnabled(true);
+ }
+
+ /**
+ * Helper method to disable carbons.
+ *
+ * @return true if the operation was successful
+ */
+ public boolean disableCarbons() {
+ return setCarbonsEnabled(false);
+ }
+
+ /**
+ * Check if carbons are enabled on this connection.
+ */
+ public boolean getCarbonsEnabled() {
+ return this.enabled_state;
+ }
+
+ /**
+ * Obtain a Carbon from a message, if available.
+ *
+ * @param msg Message object to check for carbons
+ *
+ * @return a Carbon if available, null otherwise.
+ */
+ public static Carbon getCarbon(Message msg) {
+ Carbon cc = (Carbon)msg.getExtension("received", Carbon.NAMESPACE);
+ if (cc == null)
+ cc = (Carbon)msg.getExtension("sent", Carbon.NAMESPACE);
+ return cc;
+ }
+
+ /**
+ * Mark a message as "private", so it will not be carbon-copied.
+ *
+ * @param msg Message object to mark private
+ */
+ public static void disableCarbons(Message msg) {
+ msg.addExtension(new Carbon.Private());
+ }
+}
diff --git a/source/org/jivesoftware/smackx/forward/Forwarded.java b/source/org/jivesoftware/smackx/forward/Forwarded.java
new file mode 100644
index 000000000..817ee27db
--- /dev/null
+++ b/source/org/jivesoftware/smackx/forward/Forwarded.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.
+ * 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.forward;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.provider.DelayInfoProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for XEP-0297: Stanza Forwarding. This class implements
+ * the packet extension and a {@link PacketExtensionProvider} to parse
+ * forwarded messages from a packet. The extension
+ * XEP-0297 is
+ * a prerequisite for XEP-0280 (Message Carbons).
+ *
+ * The {@link Forwarded.Provider} must be registered in the
+ * smack.properties file for the element forwarded with
+ * namespace urn:xmpp:forwarded:0
to be used.
+ *
+ * @author Georg Lukas
+ */
+public class Forwarded implements PacketExtension {
+ public static final String NAMESPACE = "urn:xmpp:forward:0";
+ public static final String ELEMENT_NAME = "forwarded";
+
+ private DelayInfo delay;
+ private Packet forwardedPacket;
+
+ /**
+ * Creates a new Forwarded packet extension.
+ *
+ * @param delay an optional {@link DelayInfo} timestamp of the packet.
+ * @param fwdPacket the packet that is forwarded (required).
+ */
+ public Forwarded(DelayInfo delay, Packet fwdPacket) {
+ this.delay = delay;
+ this.forwardedPacket = fwdPacket;
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(" xmlns=\"")
+ .append(getNamespace()).append("\">");
+
+ if (delay != null)
+ buf.append(delay.toXML());
+ buf.append(forwardedPacket.toXML());
+
+ buf.append("").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * get the packet forwarded by this stanza.
+ *
+ * @return the {@link Packet} instance (typically a message) that was forwarded.
+ */
+ public Packet getForwardedPacket() {
+ return forwardedPacket;
+ }
+
+ /**
+ * get the timestamp of the forwarded packet.
+ *
+ * @return the {@link DelayInfo} representing the time when the original packet was sent. May be null.
+ */
+ public DelayInfo getDelayInfo() {
+ return delay;
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+ DelayInfoProvider dip = new DelayInfoProvider();
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ DelayInfo di = null;
+ Packet packet = null;
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("delay"))
+ di = (DelayInfo)dip.parseExtension(parser);
+ else if (parser.getName().equals("message"))
+ packet = PacketParserUtils.parseMessage(parser);
+ else throw new Exception("Unsupported forwarded packet type: " + parser.getName());
+ }
+ else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME))
+ done = true;
+ }
+ if (packet == null)
+ throw new Exception("forwarded extension must contain a packet");
+ return new Forwarded(di, packet);
+ }
+ }
+}
diff --git a/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java b/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java
new file mode 100644
index 000000000..b8d3d83a1
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java
@@ -0,0 +1,168 @@
+/**
+ * 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.
+ * 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.carbons;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Properties;
+import java.util.TimeZone;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.jivesoftware.smackx.forward.Forwarded;
+import org.junit.Test;
+import org.xmlpull.mxp1.MXParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+public class CarbonForwardedTest {
+
+ private static Properties outputProperties = new Properties();
+ static {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void forwardedTest() throws Exception {
+ XmlPullParser parser;
+ String control;
+ Forwarded fwd;
+
+ control = XMLBuilder.create("forwarded")
+ .a("xmlns", "urn:xmpp:forwarded:0")
+ .e("message")
+ .a("from", "romeo@montague.com")
+ .asString(outputProperties);
+
+ parser = getParser(control, "forwarded");
+ fwd = (Forwarded) new Forwarded.Provider().parseExtension(parser);
+
+ // no delay in packet
+ assertEquals(null, fwd.getDelayInfo());
+
+ // check message
+ assertEquals("romeo@montague.com", fwd.getForwardedPacket().getFrom());
+
+ // check end of tag
+ assertEquals(XmlPullParser.END_TAG, parser.getEventType());
+ assertEquals("forwarded", parser.getName());
+
+ }
+
+ @Test(expected=Exception.class)
+ public void forwardedEmptyTest() throws Exception {
+ XmlPullParser parser;
+ String control;
+
+ control = XMLBuilder.create("forwarded")
+ .a("xmlns", "urn:xmpp:forwarded:0")
+ .asString(outputProperties);
+
+ parser = getParser(control, "forwarded");
+ new Forwarded.Provider().parseExtension(parser);
+ }
+
+ @Test
+ public void carbonSentTest() throws Exception {
+ XmlPullParser parser;
+ String control;
+ Carbon cc;
+ Forwarded fwd;
+
+ control = XMLBuilder.create("sent")
+ .e("forwarded")
+ .a("xmlns", "urn:xmpp:forwarded:0")
+ .e("message")
+ .a("from", "romeo@montague.com")
+ .asString(outputProperties);
+
+ parser = getParser(control, "sent");
+ cc = (Carbon) new Carbon.Provider().parseExtension(parser);
+ fwd = cc.getForwarded();
+
+ // meta
+ assertEquals(Carbon.Direction.sent, cc.getDirection());
+
+ // no delay in packet
+ assertEquals(null, fwd.getDelayInfo());
+
+ // check message
+ assertEquals("romeo@montague.com", fwd.getForwardedPacket().getFrom());
+
+ // check end of tag
+ assertEquals(XmlPullParser.END_TAG, parser.getEventType());
+ assertEquals("sent", parser.getName());
+ }
+
+ @Test
+ public void carbonReceivedTest() throws Exception {
+ XmlPullParser parser;
+ String control;
+ Carbon cc;
+
+ control = XMLBuilder.create("received")
+ .e("forwarded")
+ .a("xmlns", "urn:xmpp:forwarded:0")
+ .e("message")
+ .a("from", "romeo@montague.com")
+ .asString(outputProperties);
+
+ parser = getParser(control, "received");
+ cc = (Carbon) new Carbon.Provider().parseExtension(parser);
+
+ assertEquals(Carbon.Direction.received, cc.getDirection());
+
+ // check end of tag
+ assertEquals(XmlPullParser.END_TAG, parser.getEventType());
+ assertEquals("received", parser.getName());
+ }
+
+ @Test(expected=Exception.class)
+ public void carbonEmptyTest() throws Exception {
+ XmlPullParser parser;
+ String control;
+
+ control = XMLBuilder.create("sent")
+ .a("xmlns", "urn:xmpp:forwarded:0")
+ .asString(outputProperties);
+
+ parser = getParser(control, "sent");
+ new Carbon.Provider().parseExtension(parser);
+ }
+
+ private XmlPullParser getParser(String control, String startTag)
+ throws XmlPullParserException, IOException {
+ XmlPullParser parser = new MXParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(new StringReader(control));
+
+ while (true) {
+ if (parser.next() == XmlPullParser.START_TAG
+ && parser.getName().equals(startTag)) {
+ break;
+ }
+ }
+ return parser;
+ }
+
+}