/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.xml.messaging.saaj.client.p2p;

import java.io.*;
import java.lang.reflect.Method;
import java.net.*;
import java.security.*;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.soap.*;

import com.sun.xml.messaging.saaj.SOAPExceptionImpl;
import com.sun.xml.messaging.saaj.util.*;

/**
 * This represents a "connection" to the simple HTTP-based provider.
 *
 * @author Anil Vijendran (akv@eng.sun.com)
 * @author Rajiv Mordani (rajiv.mordani@sun.com)
 * @author Manveen Kaur (manveen.kaur@sun.com)
 *
 */
class HttpSOAPConnection extends SOAPConnection {

    public static final String vmVendor = SAAJUtil.getSystemProperty("java.vendor.url");
    private static final String ibmVmVendor = "http://www.ibm.com/";
    private static final boolean isIBMVM = ibmVmVendor.equals(vmVendor) ? true : false;
    private static final String JAXM_URLENDPOINT="javax.xml.messaging.URLEndpoint";
    
    protected static final Logger log =
        Logger.getLogger(LogDomainConstants.HTTP_CONN_DOMAIN,
                         "com.sun.xml.messaging.saaj.client.p2p.LocalStrings");


    MessageFactory messageFactory = null;

    boolean closed = false;

    public HttpSOAPConnection() throws SOAPException {

        try {
            messageFactory = MessageFactory.newInstance(SOAPConstants.DYNAMIC_SOAP_PROTOCOL);
        } catch (NoSuchMethodError ex) {
            //fallback to default SOAP 1.1 in this case for backward compatibility
            messageFactory = MessageFactory.newInstance();
        } catch (Exception ex) {
            log.log(Level.SEVERE, "SAAJ0001.p2p.cannot.create.msg.factory", ex);
            throw new SOAPExceptionImpl("Unable to create message factory", ex);
        }
    }

    @Override
    public void close() throws SOAPException {
        if (closed) {
            log.severe("SAAJ0002.p2p.close.already.closed.conn");
            throw new SOAPExceptionImpl("Connection already closed");
        }

        messageFactory = null;
        closed = true;
    }

    @Override
   public SOAPMessage call(SOAPMessage message, Object endPoint)
        throws SOAPException {
        if (closed) {
            log.severe("SAAJ0003.p2p.call.already.closed.conn");
            throw new SOAPExceptionImpl("Connection is closed");
        }

        Class<?> urlEndpointClass = null;
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try {
            if (loader != null) {
                urlEndpointClass = loader.loadClass(JAXM_URLENDPOINT);
            } else {
                urlEndpointClass = Class.forName(JAXM_URLENDPOINT);
            }
        } catch (ClassNotFoundException ex) {
            //Do nothing. URLEndpoint is available only when JAXM is there.
            if (log.isLoggable(Level.FINEST))
                log.finest("SAAJ0090.p2p.endpoint.available.only.for.JAXM");
        }

        if (urlEndpointClass != null) {
            if (urlEndpointClass.isInstance(endPoint)) {
                String url = null;

                try {
                    Method m = urlEndpointClass.getMethod("getURL", (Class[])null);
                    url = (String) m.invoke(endPoint, (Object[])null);
                } catch (Exception ex) {
                    // TBD -- exception chaining
                    log.log(Level.SEVERE,"SAAJ0004.p2p.internal.err",ex);
                    throw new SOAPExceptionImpl(
                        "Internal error: " + ex.getMessage());
                }
                try {
                    endPoint = new URL(url);
                } catch (MalformedURLException mex) {
                    log.log(Level.SEVERE,"SAAJ0005.p2p.", mex);
                    throw new SOAPExceptionImpl("Bad URL: " + mex.getMessage());
                }
            }
        }

        if (endPoint instanceof java.lang.String) {
            try {
                endPoint = new URL((String) endPoint);
            } catch (MalformedURLException mex) {
                log.log(Level.SEVERE, "SAAJ0006.p2p.bad.URL", mex);
                throw new SOAPExceptionImpl("Bad URL: " + mex.getMessage());
            }
        }

        if (endPoint instanceof URL)
            try {
                SOAPMessage response = post(message, (URL)endPoint);
                return response;
            } catch (Exception ex) {
                // TBD -- chaining?
                throw new SOAPExceptionImpl(ex);
            } else {
            log.severe("SAAJ0007.p2p.bad.endPoint.type");
            throw new SOAPExceptionImpl("Bad endPoint type " + endPoint);
        }
    }

    SOAPMessage post(SOAPMessage message, URL endPoint) throws SOAPException, IOException {
        boolean isFailure = false;

        URL url = null;
        HttpURLConnection httpConnection = null;

        int responseCode = 0;
        try {
            if (endPoint.getProtocol().equals("https"))
                //if(!setHttps)
                initHttps();
            // Process the URL
            URI uri = new URI(endPoint.toString());
            String userInfo = uri.getRawUserInfo();

            url = endPoint;

            if (dL > 0)
                d("uri: " + userInfo + " " + url + " " + uri);

            // TBD
            //    Will deal with https later.
            if (!url.getProtocol().equalsIgnoreCase("http")
                && !url.getProtocol().equalsIgnoreCase("https")) {
                log.severe("SAAJ0052.p2p.protocol.mustbe.http.or.https");
                throw new IllegalArgumentException(
                    "Protocol "
                        + url.getProtocol()
                        + " not supported in URL "
                        + url);
            }
            httpConnection = (HttpURLConnection) createConnection(url);

            httpConnection.setRequestMethod("POST");

            httpConnection.setDoOutput(true);
            httpConnection.setDoInput(true);
            httpConnection.setUseCaches(false);
            httpConnection.setInstanceFollowRedirects(true);

            if (message.saveRequired())
                message.saveChanges();

            MimeHeaders headers = message.getMimeHeaders();

            Iterator<?> it = headers.getAllHeaders();
            boolean hasAuth = false; // true if we find explicit Auth header
            while (it.hasNext()) {
                MimeHeader header = (MimeHeader) it.next();

                String[] values = headers.getHeader(header.getName());
                if (values.length == 1)
                    httpConnection.setRequestProperty(
                        header.getName(),
                        header.getValue());
                else {
                    StringBuilder concat = new StringBuilder();
                    int i = 0;
                    while (i < values.length) {
                        if (i != 0)
                            concat.append(',');
                        concat.append(values[i]);
                        i++;
                    }

                    httpConnection.setRequestProperty(
                        header.getName(),
                        concat.toString());
                }

                if ("Authorization".equals(header.getName())) {
                    hasAuth = true;
                    if (log.isLoggable(Level.FINE))
                        log.fine("SAAJ0091.p2p.https.auth.in.POST.true");
                }
            }

            if (!hasAuth && userInfo != null) {
                initAuthUserInfo(httpConnection, userInfo);
            }

            OutputStream out = httpConnection.getOutputStream();
            try {
                message.writeTo(out);
                out.flush();
            } finally {
                out.close();
            }

            httpConnection.connect();

            try {

                responseCode = httpConnection.getResponseCode();

                // let HTTP_INTERNAL_ERROR (500) through because it is used for SOAP faults
                if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
                    isFailure = true;
                }
                //else if (responseCode != HttpURLConnection.HTTP_OK)
                //else if (!(responseCode >= HttpURLConnection.HTTP_OK && responseCode < 207))
                else if ((responseCode / 100) != 2) {
                    log.log(Level.SEVERE,
                            "SAAJ0008.p2p.bad.response",
                            new String[] {httpConnection.getResponseMessage()});
                    throw new SOAPExceptionImpl(
                        "Bad response: ("
                            + responseCode
                            + httpConnection.getResponseMessage());

                }
            } catch (IOException e) {
                // on JDK1.3.1_01, we end up here, but then getResponseCode() succeeds!
                responseCode = httpConnection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
                    isFailure = true;
                } else {
                    throw e;
                }

            }

        } catch (SOAPException ex) {
            throw ex;
        } catch (Exception ex) {
            log.severe("SAAJ0009.p2p.msg.send.failed");
            throw new SOAPExceptionImpl("Message send failed", ex);
        }

        SOAPMessage response = null;
        InputStream httpIn = null;
        if (responseCode == HttpURLConnection.HTTP_OK || isFailure) {
            try {
                MimeHeaders headers = new MimeHeaders();

                String key, value;

                // Header field 0 is the status line so we skip it.

                int i = 1;

                while (true) {
                    key = httpConnection.getHeaderFieldKey(i);
                    value = httpConnection.getHeaderField(i);

                    if (key == null && value == null)
                        break;

                    if (key != null) {
                        StringTokenizer values =
                            new StringTokenizer(value, ",");
                        while (values.hasMoreTokens())
                            headers.addHeader(key, values.nextToken().trim());
                    }
                    i++;
                }

                httpIn =
                    (isFailure
                        ? httpConnection.getErrorStream()
                        : httpConnection.getInputStream());

                byte[] bytes = readFully(httpIn);

                int length =
                    httpConnection.getContentLength() == -1
                        ? bytes.length
                        : httpConnection.getContentLength();
                        
                // If no reply message is returned,
                // content-Length header field value is expected to be zero.
                if (length == 0) {
                    response = null;
                    log.warning("SAAJ0014.p2p.content.zero");
                } else {
                    ByteInputStream in = new ByteInputStream(bytes, length);
                    response = messageFactory.createMessage(headers, in);
                }

            } catch (SOAPException ex) {
                throw ex;
            } catch (Exception ex) {
                log.log(Level.SEVERE,"SAAJ0010.p2p.cannot.read.resp", ex);
                throw new SOAPExceptionImpl(
                    "Unable to read response: " + ex.getMessage());
            } finally {
               if (httpIn != null)
                   httpIn.close();
               httpConnection.disconnect();
            }
        }
        return response;
    }

    // Object identifies where the request should be sent.
    // It is required to support objects of type String and java.net.URL.

    @Override
    public SOAPMessage get(Object endPoint) throws SOAPException {
        if (closed) {
            log.severe("SAAJ0011.p2p.get.already.closed.conn");
            throw new SOAPExceptionImpl("Connection is closed");
        }
        Class<?> urlEndpointClass = null;

        try {
            urlEndpointClass = Class.forName("javax.xml.messaging.URLEndpoint");
        } catch (Exception ex) {
            //Do nothing. URLEndpoint is available only when JAXM is there.
        }

        if (urlEndpointClass != null) {
            if (urlEndpointClass.isInstance(endPoint)) {
                String url = null;

                try {
                    Method m = urlEndpointClass.getMethod("getURL", (Class[])null);
                    url = (String) m.invoke(endPoint, (Object[])null);
                } catch (Exception ex) {
                    log.severe("SAAJ0004.p2p.internal.err");
                    throw new SOAPExceptionImpl(
                        "Internal error: " + ex.getMessage());
                }
                try {
                    endPoint = new URL(url);
                } catch (MalformedURLException mex) {
                    log.severe("SAAJ0005.p2p.");
                    throw new SOAPExceptionImpl("Bad URL: " + mex.getMessage());
                }
            }
        }

        if (endPoint instanceof java.lang.String) {
            try {
                endPoint = new URL((String) endPoint);
            } catch (MalformedURLException mex) {
                log.severe("SAAJ0006.p2p.bad.URL");
                throw new SOAPExceptionImpl("Bad URL: " + mex.getMessage());
            }
        }

        if (endPoint instanceof URL)
            try {
                SOAPMessage response = doGet((URL)endPoint);
                return response;
            } catch (Exception ex) {
                throw new SOAPExceptionImpl(ex);
            } else
            throw new SOAPExceptionImpl("Bad endPoint type " + endPoint);
    }

    SOAPMessage doGet(URL endPoint) throws SOAPException, IOException {
        boolean isFailure = false;

        URL url = null;
        HttpURLConnection httpConnection = null;

        int responseCode = 0;
        try {
            /// Is https GET allowed??
            if (endPoint.getProtocol().equals("https"))
                initHttps();
            // Process the URL
            URI uri = new URI(endPoint.toString());
            String userInfo = uri.getRawUserInfo();

            url = endPoint;

            if (dL > 0)
                d("uri: " + userInfo + " " + url + " " + uri);

            // TBD
            //    Will deal with https later.
            if (!url.getProtocol().equalsIgnoreCase("http")
                && !url.getProtocol().equalsIgnoreCase("https")) {
                log.severe("SAAJ0052.p2p.protocol.mustbe.http.or.https");
                throw new IllegalArgumentException(
                    "Protocol "
                        + url.getProtocol()
                        + " not supported in URL "
                        + url);
            }
            httpConnection = (HttpURLConnection) createConnection(url);

            httpConnection.setRequestMethod("GET");

            httpConnection.setDoOutput(true);
            httpConnection.setDoInput(true);
            httpConnection.setUseCaches(false);
            httpConnection.setInstanceFollowRedirects(true);

            httpConnection.connect();

            try {

                responseCode = httpConnection.getResponseCode();

                // let HTTP_INTERNAL_ERROR (500) through because it is used for SOAP faults
                if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
                    isFailure = true;
                } else if ((responseCode / 100) != 2) {
                    log.log(Level.SEVERE,
                            "SAAJ0008.p2p.bad.response",
                            new String[] { httpConnection.getResponseMessage()});
                    throw new SOAPExceptionImpl(
                        "Bad response: ("
                            + responseCode
                            + httpConnection.getResponseMessage());

                }
            } catch (IOException e) {
                // on JDK1.3.1_01, we end up here, but then getResponseCode() succeeds!
                responseCode = httpConnection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
                    isFailure = true;
                } else {
                    throw e;
                }

            }

        } catch (SOAPException ex) {
            throw ex;
        } catch (Exception ex) {
            log.severe("SAAJ0012.p2p.get.failed");
            throw new SOAPExceptionImpl("Get failed", ex);
        }

        SOAPMessage response = null;
        InputStream httpIn = null;
        if (responseCode == HttpURLConnection.HTTP_OK || isFailure) {
            try {
                MimeHeaders headers = new MimeHeaders();

                String key, value;

                // Header field 0 is the status line so we skip it.

                int i = 1;

                while (true) {
                    key = httpConnection.getHeaderFieldKey(i);
                    value = httpConnection.getHeaderField(i);

                    if (key == null && value == null)
                        break;

                    if (key != null) {
                        StringTokenizer values =
                            new StringTokenizer(value, ",");
                        while (values.hasMoreTokens())
                            headers.addHeader(key, values.nextToken().trim());
                    }
                    i++;
                }

                httpIn =
                        (isFailure
                        ? httpConnection.getErrorStream()
                        : httpConnection.getInputStream());
                // If no reply message is returned,
                // content-Length header field value is expected to be zero.
                // java SE 6 documentation says :
                // available() : an estimate of the number of bytes that can be read
                //(or skipped over) from this input stream without blocking
                //or 0 when it reaches the end of the input stream.
                if ((httpIn == null )
                        || (httpConnection.getContentLength() == 0)
                        || (httpIn.available() == 0)) {
                    response = null;
                    log.warning("SAAJ0014.p2p.content.zero");
                } else {
                    response = messageFactory.createMessage(headers, httpIn);
                }

            } catch (SOAPException ex) {
                throw ex;
            } catch (Exception ex) {
                log.log(Level.SEVERE,
                        "SAAJ0010.p2p.cannot.read.resp",
                        ex);
                throw new SOAPExceptionImpl(
                    "Unable to read response: " + ex.getMessage());
            } finally {
               if (httpIn != null)
                   httpIn.close();
               httpConnection.disconnect();
            }
        }
        return response;
    }

    private byte[] readFully(InputStream istream) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int num = 0;

        while ((num = istream.read(buf)) != -1) {
            bout.write(buf, 0, num);
        }

        byte[] ret = bout.toByteArray();

        return ret;
    }

    //private static String SSL_PKG = "com.sun.net.ssl.internal.www.protocol";
    //private static String SSL_PROVIDER =
      //  "com.sun.net.ssl.internal.ssl.Provider";
    private static final String SSL_PKG;
    private static final String SSL_PROVIDER;
    
    static {    
        if (isIBMVM) {
            SSL_PKG ="com.ibm.net.ssl.internal.www.protocol";
            SSL_PROVIDER ="com.ibm.net.ssl.internal.ssl.Provider";
        } else {
            //if not IBM VM default to Sun.
            SSL_PKG = "com.sun.net.ssl.internal.www.protocol";
            SSL_PROVIDER ="com.sun.net.ssl.internal.ssl.Provider";
        }
    }
    
    private void initHttps() {
        //if(!setHttps) {
        String pkgs = SAAJUtil.getSystemProperty("java.protocol.handler.pkgs");
        if (log.isLoggable(Level.FINE))
            log.log(Level.FINE, "SAAJ0053.p2p.providers", new String[] { pkgs });

        if (pkgs == null || pkgs.indexOf(SSL_PKG) < 0) {
            if (pkgs == null)
                pkgs = SSL_PKG;
            else
                pkgs = pkgs + "|" + SSL_PKG;
            System.setProperty("java.protocol.handler.pkgs", pkgs);
            if (log.isLoggable(Level.FINE))
                log.log(Level.FINE, "SAAJ0054.p2p.set.providers",
                        new String[] { pkgs });
            try {
                Class<?> c = Class.forName(SSL_PROVIDER);
                Provider p = (Provider) c.newInstance();
                Security.addProvider(p);
                if (log.isLoggable(Level.FINE))
                    log.log(Level.FINE, "SAAJ0055.p2p.added.ssl.provider",
                            new String[] { SSL_PROVIDER });
                //System.out.println("Added SSL_PROVIDER " + SSL_PROVIDER);
                //setHttps = true;
            } catch (Exception ex) {
            }
        }
        //}
    }

    private void initAuthUserInfo(HttpURLConnection conn, String userInfo) {
        String user;
        String password;
        if (userInfo != null) { // get the user and password
            //System.out.println("UserInfo= " + userInfo );
            int delimiter = userInfo.indexOf(':');
            if (delimiter == -1) {
                user = ParseUtil.decode(userInfo);
                password = null;
            } else {
                user = ParseUtil.decode(userInfo.substring(0, delimiter++));
                password = ParseUtil.decode(userInfo.substring(delimiter));
            }

            String plain = user + ":";
            byte[] nameBytes = plain.getBytes();
            byte[] passwdBytes = (password == null ? new byte[0] : password
                    .getBytes());

            // concatenate user name and password bytes and encode them
            byte[] concat = new byte[nameBytes.length + passwdBytes.length];

            System.arraycopy(nameBytes, 0, concat, 0, nameBytes.length);
            System.arraycopy(
                passwdBytes,
                0,
                concat,
                nameBytes.length,
                passwdBytes.length);
            String auth = "Basic " + new String(Base64.encode(concat));
            conn.setRequestProperty("Authorization", auth);
            if (dL > 0)
                d("Adding auth " + auth);
        }
    }

    private static final int dL = 0;
    private void d(String s) {
        log.log(Level.SEVERE,
                "SAAJ0013.p2p.HttpSOAPConnection", 
                new String[] { s });
        System.err.println("HttpSOAPConnection: " + s);
    }

    private java.net.HttpURLConnection createConnection(URL endpoint)
        throws IOException {
        return (HttpURLConnection) endpoint.openConnection();
    }

}
