001/**
002 *
003 * Copyright 2014 Andriy Tsykholyas, 2015-2019 Florian Schmaus
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.hoxt.provider;
018
019import java.io.IOException;
020
021import org.jivesoftware.smack.packet.NamedElement;
022import org.jivesoftware.smack.parsing.SmackParsingException;
023import org.jivesoftware.smack.provider.IqProvider;
024import org.jivesoftware.smack.util.StringUtils;
025import org.jivesoftware.smack.xml.XmlPullParser;
026import org.jivesoftware.smack.xml.XmlPullParserException;
027
028import org.jivesoftware.smackx.hoxt.packet.AbstractHttpOverXmpp;
029import org.jivesoftware.smackx.shim.packet.HeadersExtension;
030import org.jivesoftware.smackx.shim.provider.HeadersProvider;
031
032/**
033 * Abstract parent for Req and Resp stanza providers.
034 *
035 * @author Andriy Tsykholyas
036 * @see <a href="http://xmpp.org/extensions/xep-0332.html">XEP-0332: HTTP over XMPP transport</a>
037 */
038public abstract class AbstractHttpOverXmppProvider<H extends AbstractHttpOverXmpp> extends IqProvider<H> {
039
040    private static final String ELEMENT_DATA = "data";
041    private static final String ELEMENT_TEXT = "text";
042    private static final String ELEMENT_BASE_64 = "base64";
043    private static final String ELEMENT_CHUNKED_BASE_64 = "chunkedBase64";
044    private static final String ELEMENT_XML = "xml";
045    static final String ELEMENT_IBB = "ibb";
046    static final String ELEMENT_SIPUB = "sipub";
047    static final String ELEMENT_JINGLE = "jingle";
048
049    private static final String ATTRIBUTE_STREAM_ID = "streamId";
050    private static final String ATTRIBUTE_SID = "sid";
051    static final String ATTRIBUTE_VERSION = "version";
052
053    /**
054     * Parses HeadersExtension element if any.
055     *
056     * @param parser parser
057     * @return HeadersExtension or null if no headers
058     * @throws XmlPullParserException if an error in the XML parser occurred.
059     * @throws IOException if an I/O error occurred.
060     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
061     */
062    protected HeadersExtension parseHeaders(XmlPullParser parser) throws IOException, XmlPullParserException, SmackParsingException {
063        HeadersExtension headersExtension = null;
064        /* We are either at start of headers, start of data or end of req/res */
065        if (parser.next() == XmlPullParser.Event.START_ELEMENT && parser.getName().equals(HeadersExtension.ELEMENT)) {
066            headersExtension = HeadersProvider.INSTANCE.parse(parser);
067            parser.next();
068        }
069
070        return headersExtension;
071    }
072
073    /**
074     * Parses Data element if any.
075     *
076     * @param parser parser
077     * @return Data or null if no data
078     *
079     * @throws XmlPullParserException if an error in the XML parser occurred.
080     * @throws IOException if an I/O error occurred.
081     */
082    protected AbstractHttpOverXmpp.Data parseData(XmlPullParser parser) throws XmlPullParserException, IOException {
083        NamedElement child = null;
084        boolean done = false;
085        AbstractHttpOverXmpp.Data data = null;
086        /* We are either at start of data or end of req/res */
087        if (parser.getEventType() == XmlPullParser.Event.START_ELEMENT) {
088            while (!done) {
089                XmlPullParser.Event eventType = parser.next();
090
091                if (eventType == XmlPullParser.Event.START_ELEMENT) {
092                    switch (parser.getName()) {
093                    case ELEMENT_TEXT:
094                        child = parseText(parser);
095                        break;
096                    case ELEMENT_BASE_64:
097                        child = parseBase64(parser);
098                        break;
099                    case ELEMENT_CHUNKED_BASE_64:
100                        child = parseChunkedBase64(parser);
101                        break;
102                    case ELEMENT_XML:
103                        child = parseXml(parser);
104                        break;
105                    case ELEMENT_IBB:
106                        child = parseIbb(parser);
107                        break;
108                    case ELEMENT_SIPUB:
109                        // TODO: sipub is allowed by xep-0332, but is not
110                        // implemented yet
111                        throw new UnsupportedOperationException("sipub is not supported yet");
112                    case ELEMENT_JINGLE:
113                        // TODO: jingle is allowed by xep-0332, but is not
114                        // implemented yet
115                        throw new UnsupportedOperationException("jingle is not supported yet");
116                    default:
117                        // other elements are not allowed
118                        throw new IllegalArgumentException("unsupported child tag: " + parser.getName());
119                    }
120                } else if (eventType == XmlPullParser.Event.END_ELEMENT) {
121                    if (parser.getName().equals(ELEMENT_DATA)) {
122                        done = true;
123                    }
124                }
125            }
126            data = new AbstractHttpOverXmpp.Data(child);
127        }
128        return data;
129    }
130
131    private static AbstractHttpOverXmpp.Text parseText(XmlPullParser parser) throws XmlPullParserException, IOException {
132        String text = null;
133        boolean done = false;
134
135        while (!done) {
136            XmlPullParser.Event eventType = parser.next();
137
138            if (eventType == XmlPullParser.Event.END_ELEMENT) {
139                if (parser.getName().equals(ELEMENT_TEXT)) {
140                    done = true;
141                } else {
142                    throw new IllegalArgumentException("unexpected end tag of: " + parser.getName());
143                }
144            } else if (eventType == XmlPullParser.Event.TEXT_CHARACTERS) {
145                text = parser.getText();
146            } else {
147                throw new IllegalArgumentException("unexpected eventType: " + eventType);
148            }
149        }
150
151        return new AbstractHttpOverXmpp.Text(text);
152    }
153
154    private static AbstractHttpOverXmpp.Xml parseXml(XmlPullParser parser)
155            throws XmlPullParserException, IOException {
156        StringBuilder builder = new StringBuilder();
157        boolean done = false;
158        boolean startClosed = true;
159
160        while (!done) {
161            XmlPullParser.Event eventType = parser.next();
162
163            if ((eventType == XmlPullParser.Event.END_ELEMENT) && parser.getName().equals(ELEMENT_XML)) {
164                done = true;
165            } else { // just write everything else as text
166
167                if (eventType == XmlPullParser.Event.START_ELEMENT) {
168
169                    if (!startClosed) {
170                        builder.append('>');
171                    }
172
173                    builder.append('<');
174                    builder.append(parser.getName());
175                    appendXmlAttributes(parser, builder);
176                    startClosed = false;
177                } else if (eventType == XmlPullParser.Event.END_ELEMENT) {
178
179                    if (startClosed) {
180                        builder.append("</");
181                        builder.append(parser.getName());
182                        builder.append('>');
183                    } else {
184                        builder.append("/>");
185                        startClosed = true;
186                    }
187                } else if (eventType == XmlPullParser.Event.TEXT_CHARACTERS) {
188
189                    if (!startClosed) {
190                        builder.append('>');
191                        startClosed = true;
192                    }
193                    builder.append(StringUtils.escapeForXmlText(parser.getText()));
194                } else {
195                    throw new IllegalArgumentException("unexpected eventType: " + eventType);
196                }
197            }
198        }
199
200        return new AbstractHttpOverXmpp.Xml(builder.toString());
201    }
202
203    private static void appendXmlAttributes(XmlPullParser parser, StringBuilder builder) {
204        // NOTE: for now we ignore namespaces
205        int count = parser.getAttributeCount();
206
207        if (count > 0) {
208
209            for (int i = 0; i < count; i++) {
210                builder.append(' ');
211                builder.append(parser.getAttributeName(i));
212                builder.append("=\"");
213                builder.append(StringUtils.escapeForXml(parser.getAttributeValue(i)));
214                builder.append('"');
215            }
216        }
217    }
218
219    private static AbstractHttpOverXmpp.Base64 parseBase64(XmlPullParser parser) throws XmlPullParserException,
220                    IOException {
221        String text = null;
222        boolean done = false;
223
224        while (!done) {
225            XmlPullParser.Event eventType = parser.next();
226
227            if (eventType == XmlPullParser.Event.END_ELEMENT) {
228
229                if (parser.getName().equals(ELEMENT_BASE_64)) {
230                    done = true;
231                } else {
232                    throw new IllegalArgumentException("unexpected end tag of: " + parser.getName());
233                }
234            } else if (eventType == XmlPullParser.Event.TEXT_CHARACTERS) {
235                text = parser.getText();
236            } else {
237                throw new IllegalArgumentException("unexpected eventType: " + eventType);
238            }
239        }
240
241        return new AbstractHttpOverXmpp.Base64(text);
242    }
243
244    private static AbstractHttpOverXmpp.ChunkedBase64 parseChunkedBase64(XmlPullParser parser)
245                    throws XmlPullParserException, IOException {
246        String streamId = parser.getAttributeValue("", ATTRIBUTE_STREAM_ID);
247        AbstractHttpOverXmpp.ChunkedBase64 child = new AbstractHttpOverXmpp.ChunkedBase64(streamId);
248        boolean done = false;
249
250        while (!done) {
251            XmlPullParser.Event eventType = parser.next();
252
253            if (eventType == XmlPullParser.Event.END_ELEMENT) {
254                if (parser.getName().equals(ELEMENT_CHUNKED_BASE_64)) {
255                    done = true;
256                } else {
257                    throw new IllegalArgumentException("unexpected end tag: " + parser.getName());
258                }
259            } else {
260                throw new IllegalArgumentException("unexpected event type: " + eventType);
261            }
262        }
263        return child;
264    }
265
266    private static AbstractHttpOverXmpp.Ibb parseIbb(XmlPullParser parser) throws XmlPullParserException, IOException {
267        String sid = parser.getAttributeValue("", ATTRIBUTE_SID);
268        AbstractHttpOverXmpp.Ibb child = new AbstractHttpOverXmpp.Ibb(sid);
269        boolean done = false;
270
271        while (!done) {
272            XmlPullParser.Event eventType = parser.next();
273
274            if (eventType == XmlPullParser.Event.END_ELEMENT) {
275                if (parser.getName().equals(ELEMENT_IBB)) {
276                    done = true;
277                } else {
278                    throw new IllegalArgumentException("unexpected end tag: " + parser.getName());
279                }
280            } else {
281                throw new IllegalArgumentException("unexpected event type: " + eventType);
282            }
283        }
284        return child;
285    }
286}