001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.jiveproperties.packet;
018
019import java.io.ByteArrayOutputStream;
020import java.io.ObjectOutputStream;
021import java.io.Serializable;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.Map;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029
030import org.jivesoftware.smack.packet.ExtensionElement;
031import org.jivesoftware.smack.packet.Message;
032import org.jivesoftware.smack.util.XmlStringBuilder;
033import org.jivesoftware.smack.util.stringencoder.Base64;
034
035/**
036 * Properties provide an easy mechanism for clients to share data. Each property has a
037 * String name, and a value that is a Java primitive (int, long, float, double, boolean)
038 * or any Serializable object (a Java object is Serializable when it implements the
039 * Serializable interface).
040 *
041 */
042public class JivePropertiesExtension implements ExtensionElement {
043    /**
044     * Namespace used to store stanza properties.
045     */
046    public static final String NAMESPACE = "http://www.jivesoftware.com/xmlns/xmpp/properties";
047
048    public static final String ELEMENT = "properties";
049
050    private static final Logger LOGGER = Logger.getLogger(JivePropertiesExtension.class.getName());
051
052    private final Map<String, Object> properties;
053
054    public JivePropertiesExtension() {
055        properties = new HashMap<>();
056    }
057
058    public JivePropertiesExtension(Map<String, Object> properties) {
059        this.properties = properties;
060    }
061
062    /**
063     * Returns the stanza property with the specified name or <tt>null</tt> if the
064     * property doesn't exist. Property values that were originally primitives will
065     * be returned as their object equivalent. For example, an int property will be
066     * returned as an Integer, a double as a Double, etc.
067     *
068     * @param name the name of the property.
069     * @return the property, or <tt>null</tt> if the property doesn't exist.
070     */
071    public synchronized Object getProperty(String name) {
072        if (properties == null) {
073            return null;
074        }
075        return properties.get(name);
076    }
077
078    /**
079     * Sets a property with an Object as the value. The value must be Serializable
080     * or an IllegalArgumentException will be thrown.
081     *
082     * @param name the name of the property.
083     * @param value the value of the property.
084     */
085    public synchronized void setProperty(String name, Object value) {
086        if (!(value instanceof Serializable)) {
087            throw new IllegalArgumentException("Value must be serializable");
088        }
089        properties.put(name, value);
090    }
091
092    /**
093     * Deletes a property.
094     *
095     * @param name the name of the property to delete.
096     */
097    public synchronized void deleteProperty(String name) {
098        if (properties == null) {
099            return;
100        }
101        properties.remove(name);
102    }
103
104    /**
105     * Returns an unmodifiable collection of all the property names that are set.
106     *
107     * @return all property names.
108     */
109    public synchronized Collection<String> getPropertyNames() {
110        if (properties == null) {
111            return Collections.emptySet();
112        }
113        return Collections.unmodifiableSet(new HashSet<>(properties.keySet()));
114    }
115
116    /**
117     * Returns an unmodifiable map of all properties.
118     *
119     * @return all properties.
120     */
121    public synchronized Map<String, Object> getProperties() {
122        if (properties == null) {
123            return Collections.emptyMap();
124        }
125        return Collections.unmodifiableMap(new HashMap<>(properties));
126    }
127
128    @Override
129    public String getElementName() {
130        return ELEMENT;
131    }
132
133    @Override
134    public String getNamespace() {
135        return NAMESPACE;
136    }
137
138    @Override
139    public CharSequence toXML(String enclosingNamespace) {
140        XmlStringBuilder xml = new XmlStringBuilder(this);
141        xml.rightAngleBracket();
142        // Loop through all properties and write them out.
143        for (String name : getPropertyNames()) {
144            Object value = getProperty(name);
145            xml.openElement("property");
146            xml.element("name", name);
147            xml.halfOpenElement("value");
148
149            String type;
150            String valueStr;
151            if (value instanceof Integer) {
152                type = "integer";
153                valueStr = Integer.toString((Integer) value);
154            }
155            else if (value instanceof Long) {
156                type = "long";
157                valueStr = Long.toString((Long) value);
158            }
159            else if (value instanceof Float) {
160                type = "float";
161                valueStr = Float.toString((Float) value);
162            }
163            else if (value instanceof Double) {
164                type = "double";
165                valueStr = Double.toString((Double) value);
166            }
167            else if (value instanceof Boolean) {
168                type = "boolean";
169                valueStr = Boolean.toString((Boolean) value);
170            }
171            else if (value instanceof String) {
172                type = "string";
173                valueStr = (String) value;
174            }
175            // Otherwise, it's a generic Serializable object. Serialized objects are in
176            // a binary format, which won't work well inside of XML. Therefore, we base-64
177            // encode the binary data before adding it.
178            else {
179                ByteArrayOutputStream byteStream = null;
180                ObjectOutputStream out = null;
181                try {
182                    byteStream = new ByteArrayOutputStream();
183                    out = new ObjectOutputStream(byteStream);
184                    out.writeObject(value);
185                    type = "java-object";
186                    valueStr = Base64.encodeToString(byteStream.toByteArray());
187                }
188                catch (Exception e) {
189                    LOGGER.log(Level.SEVERE, "Error encoding java object", e);
190                    type = "java-object";
191                    valueStr = "Serializing error: " + e.getMessage();
192                }
193                finally {
194                    if (out != null) {
195                        try {
196                            out.close();
197                        }
198                        catch (Exception e) {
199                            // Ignore.
200                        }
201                    }
202                    if (byteStream != null) {
203                        try {
204                            byteStream.close();
205                        }
206                        catch (Exception e) {
207                            // Ignore.
208                        }
209                    }
210                }
211            }
212            xml.attribute("type", type);
213            xml.rightAngleBracket();
214            xml.escape(valueStr);
215            xml.closeElement("value");
216            xml.closeElement("property");
217        }
218        xml.closeElement(this);
219
220        return xml;
221    }
222
223    /**
224     * Return a Jive properties extensions of the given message.
225     *
226     * @param message the message to return the extension from.
227     * @return a Jive properties extension or null.
228     * @since 4.2
229     */
230    public static JivePropertiesExtension from(Message message) {
231        return message.getExtension(ELEMENT, NAMESPACE);
232    }
233}