/*
 * Copyright (c) 2016, CleverTap
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * - Neither the name of CleverTap nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.clevertap.jetty.apns.http2.clients;

import com.clevertap.jetty.apns.http2.ApnsClient;
import com.clevertap.jetty.apns.http2.Notification;
import com.clevertap.jetty.apns.http2.NotificationResponseListener;
import com.clevertap.jetty.apns.http2.internal.Constants;
import com.clevertap.jetty.apns.http2.internal.ResponseListener;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.concurrent.Semaphore;

/**
 * A wrapper around Jetty's HttpClient to send out notifications using Apple's HTTP/2 API.
 */
public class AsyncApnsClient implements ApnsClient {
    private static final Logger logger = LoggerFactory.getLogger(AsyncApnsClient.class);

    protected final HttpClient client;

    protected final String gateway;

    /**
     * This semaphore is used as we cannot tell whether Jetty's internal queue is full or not.
     * Hopefully, this will change in the future.
     */
    private final Semaphore semaphore;

    /**
     * Creates a new client and automatically loads the key store
     * with the push certificate read from the input stream.
     *
     * @param certificate The client certificate to be used
     * @param password    The password (if required, else null)
     * @param production  Whether to use the production endpoint or the sandbox endpoint
     */
    public AsyncApnsClient(InputStream certificate, String password, boolean production, int maxRequestsQueued)
            throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
        password = password == null ? "" : password;
        SslContextFactory sslContext = new SslContextFactory(false);
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(certificate, password.toCharArray());
        sslContext.setKeyStore(ks);
        sslContext.setKeyStorePassword(password);
        client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()), sslContext);

        setMaxConnections(1);
        client.setMaxRequestsQueuedPerDestination(maxRequestsQueued);

        if (production) {
            gateway = Constants.ENDPOINT_PRODUCTION;
        } else {
            gateway = Constants.ENDPOINT_SANDBOX;
        }

        semaphore = new Semaphore(maxRequestsQueued);

        logger.debug("HTTP/2 client started...");
    }

    /**
     * Starts the HTTP client for pushing messages.
     *
     * @throws IOException When an error occurs
     */
    public void start() throws IOException {
        try {
            client.start();
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    /**
     * Creates a new client and automatically loads the key store
     * with the push certificate read from the input stream.
     * <p>
     * Same as calling {@link AsyncApnsClient#AsyncApnsClient(InputStream, String, boolean, int)}
     * with maximum queued requests as 2000.
     */
    public AsyncApnsClient(InputStream certificate, String password, boolean production)
            throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
        this(certificate, password, production, 2000);
    }

    /**
     * Returns the underlying HTTP client.
     *
     * @return The HttpClient instance to send messages
     */
    public HttpClient getHttpClient() {
        return client;
    }

    /**
     * Sets the underlying HttpClient's maximum connections per destination.
     * Generally, one connection can handle up to 2000 push notifications per second.
     * <p>
     * Default is 1.
     *
     * @param maxConnections The number of connections to keep open
     */
    public void setMaxConnections(int maxConnections) {
        client.setMaxConnectionsPerDestination(maxConnections);
    }

    /**
     * Sends a notification to the Apple Push Notification Service.
     *
     * @param notification The notification built using
     *                     {@link com.clevertap.jetty.apns.http2.Notification.Builder}
     * @param listener     The listener to be called after the request is complete
     */
    public void push(Notification notification, NotificationResponseListener listener) {
        _push(notification, listener);
    }

    private void _push(Notification notification, NotificationResponseListener listener) {
        Request req = client.POST(gateway)
                .path("/3/device/" + notification.getToken())
                .content(new StringContentProvider(notification.getPayload(), Charset.forName("UTF-8")));

        semaphore.acquireUninterruptibly();

        req.send(new ResponseListener(semaphore, notification, listener));
    }

    public void shutdown() throws Exception {
        client.stop();
    }
}

@@ -2526,32 +2526,98 @@
 icateException;%0A
+import java.security.cert.X509Certificate;%0Aimport java.util.Date;%0A
 import java.util
@@ -3961,24 +3961,249 @@
 arArray());%0A
+%0A        // Test our certificate%0A        if (((X509Certificate) ks.getCertificate(ks.aliases().nextElement())).getNotAfter().before(new Date())) %7B%0A            throw new CertificateException(%22Certificate expired%22);%0A        %7D%0A%0A
         sslC
@@ -6843,16 +6843,132 @@
 oken())%0A
+                .header(%22content-length%22, notification.getPayload().getBytes(Charset.forName(%22UTF-8%22)).length + %22%22)%0A
         

Set the content-length header, and test our certificate