001/**
002 *
003 * Copyright 2017-2021 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 java.io.ByteArrayInputStream;
020import java.io.InputStream;
021import java.nio.charset.Charset;
022import java.util.Collections;
023import java.util.Date;
024import java.util.List;
025import java.util.Set;
026
027import javax.xml.namespace.QName;
028
029import org.jivesoftware.smack.packet.ExtensionElement;
030import org.jivesoftware.smack.packet.XmlElement;
031import org.jivesoftware.smack.util.MultiMap;
032import org.jivesoftware.smack.util.Objects;
033import org.jivesoftware.smack.util.PacketUtil;
034import org.jivesoftware.smack.util.XmlStringBuilder;
035
036import org.jxmpp.jid.Jid;
037import org.jxmpp.util.XmppDateTime;
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<? extends Jid> to;
052    private final Date timestamp;
053    private final MultiMap<QName, XmlElement> payload;
054
055    private String timestampString;
056
057    protected OpenPgpContentElement(Set<? extends 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(e.getQName(), e);
063        }
064    }
065
066    /**
067     * Return the set of recipients.
068     *
069     * @return recipients.
070     */
071    public final Set<? extends 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<XmlElement> 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<XmlElement> getExtensions(String elementName, String namespace) {
107        QName key = new QName(namespace, elementName);
108        return payload.getAll(key);
109    }
110
111    /**
112     * Returns the first extension of this stanza that has the given namespace.
113     * <p>
114     * When possible, use {@link #getExtension(String, String)} instead.
115     * </p>
116     *
117     * @param namespace the namespace of the extension that is desired.
118     * @return the stanza extension with the given namespace.
119     */
120    public ExtensionElement getExtension(String namespace) {
121        return PacketUtil.extensionElementFrom(getExtensions(), null, namespace);
122    }
123
124    /**
125     * Returns the first extension that matches the specified element name and
126     * namespace, or <code>null</code> if it doesn't exist. If the provided elementName is null,
127     * only the namespace is matched. Extensions are
128     * are arbitrary XML elements in standard XMPP stanzas.
129     *
130     * @param elementName the XML element name of the extension. (May be null)
131     * @param namespace the XML element namespace of the extension.
132     * @param <PE> type of the ExtensionElement.
133     * @return the extension, or <code>null</code> if it doesn't exist.
134     */
135    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
136    public <PE extends ExtensionElement> PE getExtension(String elementName, String namespace) {
137        if (namespace == null) {
138            return null;
139        }
140        QName key = new QName(namespace, elementName);
141        XmlElement packetExtension;
142        synchronized (payload) {
143            packetExtension = payload.getFirst(key);
144        }
145        if (packetExtension == null) {
146            return null;
147        }
148        return (PE) packetExtension;
149    }
150
151
152    @Override
153    public String getNamespace() {
154        return OpenPgpElement.NAMESPACE;
155    }
156
157    protected void ensureTimestampStringSet() {
158        if (timestampString != null) return;
159
160        timestampString = XmppDateTime.formatXEP0082Date(timestamp);
161    }
162
163    protected void addCommonXml(XmlStringBuilder xml) {
164        for (Jid toJid : to != null ? to : Collections.<Jid>emptySet()) {
165            xml.halfOpenElement(ELEM_TO).attribute(ATTR_JID, toJid).closeEmptyElement();
166        }
167
168        ensureTimestampStringSet();
169        xml.halfOpenElement(ELEM_TIME).attribute(ATTR_STAMP, timestampString).closeEmptyElement();
170
171        xml.openElement(ELEM_PAYLOAD);
172        for (XmlElement element : payload.values()) {
173            xml.append(element.toXML(getNamespace()));
174        }
175        xml.closeElement(ELEM_PAYLOAD);
176    }
177
178    /**
179     * Return a {@link ByteArrayInputStream} that reads the bytes of the XML representation of this element.
180     *
181     * @return InputStream over xml.
182     */
183    public InputStream toInputStream() {
184        byte[] encoded = toXML().toString().getBytes(Charset.forName("UTF-8"));
185        return new ByteArrayInputStream(encoded);
186    }
187}