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