net.simondieterle.wns.server.Sender.java Source code

Java tutorial

Introduction

Here is the source code for net.simondieterle.wns.server.Sender.java

Source

/**
 * Copyright (C) 2015 Open Whisper Systems
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.simondieterle.wns.server;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.nurkiewicz.asyncretry.AsyncRetryExecutor;
import com.nurkiewicz.asyncretry.RetryContext;
import com.nurkiewicz.asyncretry.RetryExecutor;
import com.nurkiewicz.asyncretry.function.RetryCallable;
import net.simondieterle.wns.server.internal.WnsResponseHeader;
import net.simondieterle.wns.server.messages.elements.WNSOAuthError;
import net.simondieterle.wns.server.messages.elements.WnsOAuthToken;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * The main interface to sending WNS messages.  Thread safe.
 *
 * @author Simon Dieterle
 */
public class Sender {// TODO:

    private final CloseableHttpAsyncClient client;
    private final RetryExecutor executor;
    private final RetryExecutor authExecutor;

    private static final String SCOPE = "notify.windows.com";
    private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
    private static final String AUTHENTICATION_URI = "https://login.live.com/accesstoken.srf";

    private final String clientId;
    private final String clientSecret;

    private WnsOAuthToken accessToken;

    private static void printResponse(HttpResponse response, String body) {
        System.out.println("=========STATUS======");
        System.out.println(response.getStatusLine());
        System.out.println("=========HEADER======");
        Header[] headers = response.getAllHeaders();
        for (Header header : headers) {
            System.out.println(header.getName() + ":" + header.getValue());
        }
        System.out.println("=========BODY======");
        System.out.println(body);
        System.out.println("=========END=======");
    }

    public Sender(String id, String secret) {
        this(id, secret, 10);
    }

    @VisibleForTesting
    public Sender(String id, String secret, int retryCount) {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

        this.clientId = id;
        this.clientSecret = secret;

        this.client = HttpAsyncClients.custom().setMaxConnTotal(100).setMaxConnPerRoute(10).build();

        this.authExecutor = new AsyncRetryExecutor(scheduler).retryOn(ServerFailedException.class)
                .withExponentialBackoff(10, 2.0).withMaxRetries(5).withUniformJitter().withMaxDelay(60000);

        this.executor = new AsyncRetryExecutor(scheduler).retryOn(ServerFailedException.class)
                .retryOn(TimeoutException.class).retryOn(IOException.class).withExponentialBackoff(100, 2.0)
                .withUniformJitter().withMaxDelay(4000).withMaxRetries(retryCount);

        this.client.start();

    }

    public void setAuthToken(WnsOAuthToken token) {
        this.accessToken = token;
    }

    public ListenableFuture<WnsOAuthToken> auth() {
        return auth(AUTHENTICATION_URI);
    }

    public ListenableFuture<WnsOAuthToken> auth(final String url) {
        return authExecutor.getFutureWithRetry(new RetryCallable<ListenableFuture<WnsOAuthToken>>() {
            @Override
            public ListenableFuture<WnsOAuthToken> call(RetryContext context) throws Exception {
                SettableFuture<WnsOAuthToken> future = SettableFuture.create();
                HttpPost request = new HttpPost(url);

                List<NameValuePair> urlParameters = new ArrayList<>();
                urlParameters.add(new BasicNameValuePair("grant_type", GRANT_TYPE_CLIENT_CREDENTIALS));
                urlParameters.add(new BasicNameValuePair("client_id", clientId));
                urlParameters.add(new BasicNameValuePair("client_secret", clientSecret));
                urlParameters.add(new BasicNameValuePair("scope", SCOPE));

                request.setEntity(new UrlEncodedFormEntity(urlParameters));

                client.execute(request, new AuthResponseHandler(future));

                return future;
            }
        });
    }

    /**
     * Asynchronously send a message.
     *
     * @param message The message to send.
     * @return A future.
     */
    public ListenableFuture<Result> send(String url, Message message) {
        return send(url, message, null);
    }

    /**
     * Asynchronously send a message with a context to be passed in the future result.
     *
     * @param message The message to send.
     * @param requestContext An opaque context to include the future result.
     * @return The future.
     */
    public ListenableFuture<Result> send(final String url, final Message message, final Object requestContext) {
        if (accessToken == null) {
            System.out.println("sucks!!!!!!!!!!!!");
            return null;
        } // not yet able to send

        return executor.getFutureWithRetry(new RetryCallable<ListenableFuture<Result>>() {
            @Override
            public ListenableFuture<Result> call(RetryContext context) throws Exception {
                SettableFuture<Result> future = SettableFuture.create();
                HttpPost request = new HttpPost(url);

                request.setHeader("Authorization", "Bearer " + accessToken.access_token);
                request.setHeader("X-WNS-Type", message.getType().toString());
                if (message.getCachePolicy() != null)
                    request.setHeader("X-WNS-Cache-Policy", message.getCachePolicy().toString());
                if (message.getRequestForStatus() != null)
                    request.setHeader("X-WNS-RequestForStatus", message.getRequestForStatus().toString());
                if (message.getTag() != null)
                    request.setHeader("X-WNS-Tag", message.getTag().toString());
                if (message.getTtl() != null)
                    request.setHeader("X-WNS-TTL", message.getTtl().toString());
                if (message.getSuppressPopup() != null)
                    request.setHeader("X-WNS-SuppressPopup", message.getSuppressPopup().toString());
                if (message.getGroup() != null)
                    request.setHeader("X-WNS-Group", message.getGroup().toString());
                if (message.getMatch() != null)
                    request.setHeader("X-WNS-Match", message.getMatch().toString());

                // set appropriate content types
                StringEntity entity;
                if (message.getType() == Message.Type.RAW) {
                    entity = new StringEntity(message.serialize(), ContentType.parse("application/octet-stream"));
                } else {
                    entity = new StringEntity(message.serialize(), ContentType.parse("text/xml"));
                }

                request.setEntity(entity);

                client.execute(request, new ResponseHandler(future, requestContext));

                return future;
            }
        });
    }

    /**
     * Shut down all existing HTTP connections.
     * @throws IOException
     */
    public void stop() throws IOException {
        this.client.close();
    }

    private static final class AuthResponseHandler implements FutureCallback<HttpResponse> {

        private static final ObjectMapper objectMapper = new ObjectMapper();

        static {
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }

        private final SettableFuture<WnsOAuthToken> future;

        public AuthResponseHandler(SettableFuture<WnsOAuthToken> future) {
            this.future = future;
        }

        @Override
        public void completed(HttpResponse result) {
            try {
                String responseBody = EntityUtils.toString(result.getEntity());
                WnsResponseHeader responseHeader = new WnsResponseHeader(result);

                switch (result.getStatusLine().getStatusCode()) {
                case 400:
                    future.setException(parseError(responseBody, responseHeader));
                    break;
                case 204:
                case 200:
                    future.set(parseResult(responseBody, responseHeader));
                    break;
                default:
                    future.setException(
                            new ServerFailedException("Bad status: " + result.getStatusLine().getStatusCode()));
                }
            } catch (IOException e) {
                future.setException(e);
            }
        }

        @Override
        public void failed(Exception ex) {
            future.setException(ex);
        }

        @Override
        public void cancelled() {
            future.setException(new ServerFailedException("Canceled!"));
        }

        private WnsOAuthToken parseResult(String body, WnsResponseHeader header) throws IOException {
            return objectMapper.readValue(body, WnsOAuthToken.class);
        }

        private Throwable parseError(String body, WnsResponseHeader header) throws IOException {

            WNSOAuthError error = objectMapper.readValue(body, WNSOAuthError.class);
            switch (error.error) {
            case "invalid_client":
                return new InvalidClientException();
            default:
                return new Exception();
            }
        }

    }

    private static final class ResponseHandler implements FutureCallback<HttpResponse> {

        private static final ObjectMapper objectMapper = new ObjectMapper();

        static {
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }

        private final SettableFuture<Result> future;
        private final Object requestContext;

        public ResponseHandler(SettableFuture<Result> future, Object requestContext) {
            this.future = future;
            this.requestContext = requestContext;
        }

        @Override
        public void completed(HttpResponse result) {
            try {
                String responseBody = EntityUtils.toString(result.getEntity());
                WnsResponseHeader responseHeader = new WnsResponseHeader(result);

                switch (result.getStatusLine().getStatusCode()) {
                case 413:
                    future.setException(new ServerFailedException("Payload too large"));
                case 410:
                    future.setException(new ChannelExpiredException(requestContext));
                case 406:
                    int a = 0; // sent to much
                case 405:
                    int b = 1; // invalid method
                case 404:
                    future.setException(new ServerFailedException("Channel not found"));
                case 400:
                    future.setException(new InvalidRequestException());
                    break;
                case 401:
                    future.setException(new AuthenticationFailedException());
                    break;
                case 204:
                case 200:
                    future.set(parseResult(responseBody, responseHeader));
                    break;
                default:
                    future.setException(
                            new ServerFailedException("Bad status: " + result.getStatusLine().getStatusCode()));
                }
            } catch (IOException e) {
                future.setException(e);
            }
        }

        @Override
        public void failed(Exception ex) {
            future.setException(ex);
        }

        @Override
        public void cancelled() {
            future.setException(new ServerFailedException("Canceled!"));
        }

        private Result parseResult(String body, WnsResponseHeader header) throws IOException {
            return new Result(this.requestContext, header);
        }
    }
}