/*
 * Copyright 2009 Mike Cumings
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.igniterealtime.jbosh;

import java.io.IOException;
import java.io.StringReader;
import java.lang.ref.SoftReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

/**
 * Implementation of the BodyParser interface which uses the XmlPullParser
 * API.  When available, this API provides an order of magnitude performance
 * improvement over the default SAX parser implementation.
 */
final class BodyParserXmlPull implements BodyParser {

    /**
     * Logger.
     */
    private static final Logger LOG =
            Logger.getLogger(BodyParserXmlPull.class.getName());

    /**
     * Thread local to contain a XmlPullParser instance for each thread that
     * attempts to use one.  This allows us to gain an order of magnitude of
     * performance as a result of not constructing parsers for each
     * invocation while retaining thread safety.
     */
    private static final ThreadLocal<SoftReference<XmlPullParser>> XPP_PARSER =
        new ThreadLocal<SoftReference<XmlPullParser>>() {
            @Override protected SoftReference<XmlPullParser> initialValue() {
                return new SoftReference<XmlPullParser>(null);
            }
        };

    ///////////////////////////////////////////////////////////////////////////
    // BodyParser interface methods:

    /**
     * {@inheritDoc}
     */
    public BodyParserResults parse(final String xml) throws BOSHException {
        BodyParserResults result = new BodyParserResults();
        Exception thrown;
        try {
            XmlPullParser xpp = getXmlPullParser();

            xpp.setInput(new StringReader(xml));
            int eventType = xpp.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT) {
                if (eventType == XmlPullParser.START_TAG) {
                    if (LOG.isLoggable(Level.FINEST)) {
                        LOG.finest("Start tag: " + xpp.getName());
                    }
                } else {
                    eventType = xpp.next();
                    continue;
                }

                String prefix = xpp.getPrefix();
                if (prefix == null) {
                    prefix = XMLConstants.DEFAULT_NS_PREFIX;
                }
                String uri = xpp.getNamespace();
                String localName = xpp.getName();
                QName name = new QName(uri, localName, prefix);
                if (LOG.isLoggable(Level.FINEST)) {
                    LOG.finest("Start element: ");
                    LOG.finest("    prefix: " + prefix);
                    LOG.finest("    URI: " + uri);
                    LOG.finest("    local: " + localName);
                }

                BodyQName bodyName = AbstractBody.getBodyQName();
                if (!bodyName.equalsQName(name)) {
                    throw(new IllegalStateException(
                            "Root element was not '" + bodyName.getLocalPart()
                            + "' in the '" + bodyName.getNamespaceURI()
                            + "' namespace.  (Was '" + localName
                            + "' in '" + uri + "')"));
                }

                for (int idx=0; idx < xpp.getAttributeCount(); idx++) {
                    String attrURI = xpp.getAttributeNamespace(idx);
                    if (attrURI.length() == 0) {
                        attrURI = xpp.getNamespace(null);
                    }
                    String attrPrefix = xpp.getAttributePrefix(idx);
                    if (attrPrefix == null) {
                        attrPrefix = XMLConstants.DEFAULT_NS_PREFIX;
                    }
                    String attrLN = xpp.getAttributeName(idx);
                    String attrVal = xpp.getAttributeValue(idx);
                    BodyQName aqn = BodyQName.createWithPrefix(
                            attrURI, attrLN, attrPrefix);
                    if (LOG.isLoggable(Level.FINEST)) {
                        LOG.finest("        Attribute: {" + attrURI + "}"
                                + attrLN + " = '" + attrVal + "'");
                    }
                    result.addBodyAttributeValue(aqn, attrVal);
                }
                break;
            }
            return result;
        } catch (RuntimeException rtx) {
            thrown = rtx;
        } catch (XmlPullParserException xmlppx) {
            thrown = xmlppx;
        } catch (IOException iox) {
            thrown = iox;
        }
        throw(new BOSHException("Could not parse body:\n" + xml, thrown));
    }

    ///////////////////////////////////////////////////////////////////////////
    // Private methods:

    /**
     * Gets a XmlPullParser for use in parsing incoming messages.
     *
     * @return parser instance
     */
    private static XmlPullParser getXmlPullParser() {
        SoftReference<XmlPullParser> ref = XPP_PARSER.get();
        XmlPullParser result = ref.get();
        if (result == null) {
            Exception thrown;
            try {
                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                factory.setNamespaceAware(true);
                factory.setValidating(false);
                result = factory.newPullParser();
                ref = new SoftReference<XmlPullParser>(result);
                XPP_PARSER.set(ref);
                return result;
            } catch (Exception ex) {
                thrown = ex;
            }
            throw(new IllegalStateException(
                    "Could not create XmlPull parser", thrown));
        } else {
            return result;
        }
    }

}