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