001/**
002 *
003 * Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
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.ox.element;
018
019import static org.jivesoftware.smack.util.StringUtils.requireNotNullNorEmpty;
020
021import java.io.ByteArrayInputStream;
022import java.io.InputStream;
023import java.nio.charset.Charset;
024import java.util.Collections;
025import java.util.Date;
026import java.util.List;
027import java.util.Set;
028
029import org.jivesoftware.smack.packet.ExtensionElement;
030import org.jivesoftware.smack.util.MultiMap;
031import org.jivesoftware.smack.util.Objects;
032import org.jivesoftware.smack.util.PacketUtil;
033import org.jivesoftware.smack.util.XmlStringBuilder;
034
035import org.jxmpp.jid.Jid;
036import org.jxmpp.util.XmppDateTime;
037import org.jxmpp.util.XmppStringUtils;
038
039/**
040 * This class describes an OpenPGP content element. It defines the elements and fields that OpenPGP content elements
041 * do have in common.
042 */
043public abstract class OpenPgpContentElement implements ExtensionElement {
044
045    public static final String ELEM_TO = "to";
046    public static final String ATTR_JID = "jid";
047    public static final String ELEM_TIME = "time";
048    public static final String ATTR_STAMP = "stamp";
049    public static final String ELEM_PAYLOAD = "payload";
050
051    private final Set<Jid> to;
052    private final Date timestamp;
053    private final MultiMap<String, ExtensionElement> payload;
054
055    private String timestampString;
056
057    protected OpenPgpContentElement(Set<Jid> to, Date timestamp, List<ExtensionElement> payload) {
058        this.to = to;
059        this.timestamp = Objects.requireNonNull(timestamp);
060        this.payload = new MultiMap<>();
061        for (ExtensionElement e : payload) {
062            this.payload.put(XmppStringUtils.generateKey(e.getElementName(), e.getNamespace()), e);
063        }
064    }
065
066    /**
067     * Return the set of recipients.
068     *
069     * @return recipients.
070     */
071    public final Set<Jid> getTo() {
072        return to;
073    }
074
075    /**
076     * Return the timestamp on which the encrypted element has been created.
077     * This should be checked for sanity by the client.
078     *
079     * @return timestamp.
080     */
081    public final Date getTimestamp() {
082        return timestamp;
083    }
084
085    /**
086     * Return the payload of the message.
087     *
088     * @return payload.
089     */
090    public final List<ExtensionElement> getExtensions() {
091        synchronized (payload) {
092            return payload.values();
093        }
094    }
095
096    /**
097     * Return a list of all extensions with the given element name <em>and</em> namespace.
098     * <p>
099     * Changes to the returned set will update the stanza extensions, if the returned set is not the empty set.
100     * </p>
101     *
102     * @param elementName the element name, must not be null.
103     * @param namespace the namespace of the element(s), must not be null.
104     * @return a set of all matching extensions.
105     */
106    public List<ExtensionElement> getExtensions(String elementName, String namespace) {
107        requireNotNullNorEmpty(elementName, "elementName must not be null or empty");
108        requireNotNullNorEmpty(namespace, "namespace must not be null or empty");
109        String key = XmppStringUtils.generateKey(elementName, namespace);
110        return payload.getAll(key);
111    }
112
113    /**
114     * Returns the first extension of this stanza that has the given namespace.
115     * <p>
116     * When possible, use {@link #getExtension(String,String)} instead.
117     * </p>
118     *
119     * @param namespace the namespace of the extension that is desired.
120     * @return the stanza extension with the given namespace.
121     */
122    public ExtensionElement getExtension(String namespace) {
123        return PacketUtil.extensionElementFrom(getExtensions(), null, namespace);
124    }
125
126    /**
127     * Returns the first extension that matches the specified element name and
128     * namespace, or <tt>null</tt> if it doesn't exist. If the provided elementName is null,
129     * only the namespace is matched. Extensions are
130     * are arbitrary XML elements in standard XMPP stanzas.
131     *
132     * @param elementName the XML element name of the extension. (May be null)
133     * @param namespace the XML element namespace of the extension.
134     * @param <PE> type of the ExtensionElement.
135     * @return the extension, or <tt>null</tt> if it doesn't exist.
136     */
137    @SuppressWarnings("unchecked")
138    public <PE extends ExtensionElement> PE getExtension(String elementName, String namespace) {
139        if (namespace == null) {
140            return null;
141        }
142        String key = XmppStringUtils.generateKey(elementName, namespace);
143        ExtensionElement packetExtension;
144        synchronized (payload) {
145            packetExtension = payload.getFirst(key);
146        }
147        if (packetExtension == null) {
148            return null;
149        }
150        return (PE) packetExtension;
151    }
152
153
154    @Override
155    public String getNamespace() {
156        return OpenPgpElement.NAMESPACE;
157    }
158
159    protected void ensureTimestampStringSet() {
160        if (timestampString != null) return;
161
162        timestampString = XmppDateTime.formatXEP0082Date(timestamp);
163    }
164
165    protected void addCommonXml(XmlStringBuilder xml) {
166        for (Jid toJid : (to != null ? to : Collections.<Jid>emptySet())) {
167            xml.halfOpenElement(ELEM_TO).attribute(ATTR_JID, toJid).closeEmptyElement();
168        }
169
170        ensureTimestampStringSet();
171        xml.halfOpenElement(ELEM_TIME).attribute(ATTR_STAMP, timestampString).closeEmptyElement();
172
173        xml.openElement(ELEM_PAYLOAD);
174        for (ExtensionElement element : payload.values()) {
175            xml.append(element.toXML(getNamespace()));
176        }
177        xml.closeElement(ELEM_PAYLOAD);
178    }
179
180    /**
181     * Return a {@link ByteArrayInputStream} that reads the bytes of the XML representation of this element.
182     *
183     * @return InputStream over xml.
184     */
185    public InputStream toInputStream() {
186        byte[] encoded = toXML(null).toString().getBytes(Charset.forName("UTF-8"));
187        return new ByteArrayInputStream(encoded);
188    }
189}