/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.usergrid.services.notifications.apns; import com.fasterxml.jackson.databind.ObjectMapper; import com.relayrides.pushy.apns.PushManager; import com.relayrides.pushy.apns.PushManagerConfiguration; import com.relayrides.pushy.apns.util.SimpleApnsPushNotification; import org.apache.usergrid.persistence.EntityManager; import org.apache.usergrid.persistence.entities.Notification; import org.apache.usergrid.persistence.entities.Notifier; import org.apache.usergrid.persistence.exceptions.RequiredPropertyNotFoundException; import org.apache.usergrid.services.ServicePayload; import org.apache.usergrid.services.notifications.ConnectionException; import org.apache.usergrid.services.notifications.ProviderAdapter; import org.apache.usergrid.services.notifications.TaskTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.*; /** * Adapter for Apple push notifications */ public class APNsAdapter implements ProviderAdapter { private static final Logger logger = LoggerFactory.getLogger(APNsAdapter.class); public static int MAX_CONNECTION_POOL_SIZE = 15; private static final Set<String> validEnvironments = new HashSet<String>(); private static final String TEST_TOKEN = "ff026b5a4d2761ef13843e8bcab9fc83b47f1dfbd1d977d225ab296153ce06d6"; private static final String TEST_PAYLOAD = "{}"; private static ObjectMapper objectMapper = new ObjectMapper(); static { validEnvironments.add("development"); validEnvironments.add("production"); validEnvironments.add("mock"); } private final Notifier notifier; private EntityManager entityManager; private EntityPushManager pushManager; private ArrayBlockingQueue<SimpleApnsPushNotification> queue; public APNsAdapter(EntityManager entityManager, Notifier notifier){ this.entityManager = entityManager; this.notifier = notifier; } @Override public void testConnection() throws Exception { TestAPNsNotification notification = TestAPNsNotification.create(TEST_TOKEN, TEST_PAYLOAD); try { CountDownLatch latch = new CountDownLatch(1); notification.setLatch(latch); addToQueue(notification); latch.await(10000, TimeUnit.MILLISECONDS); if (notification.hasFailed()) { // this is expected with a bad certificate (w/message: comes from failedconnectionlistener throw new ConnectionException("Bad certificate. Double-check your environment.", notification.getCause() != null ? notification.getCause() : new Exception("Bad certificate.")); } notification.finished(); } catch (Exception e) { notification.finished(); if (e instanceof ConnectionException) { throw (ConnectionException) e; } if (e instanceof InterruptedException) { throw new ConnectionException("Test notification timed out", e); } logger.warn("testConnection got non-fatal error", e.getCause()); } } private BlockingQueue<SimpleApnsPushNotification> addToQueue(SimpleApnsPushNotification notification) throws Exception { BlockingQueue<SimpleApnsPushNotification> queue = getPushManager(notifier).getQueue(); queue.offer(notification,2500,TimeUnit.MILLISECONDS); return queue; } @Override public void sendNotification(String providerId, Object payload, Notification notification, TaskTracker tracker) throws Exception { APNsNotification apnsNotification = APNsNotification.create(providerId, payload.toString(), notification, tracker); try { addToQueue( apnsNotification); apnsNotification.messageSent(); }catch (InterruptedException ie){ apnsNotification.messageSendFailed(ie); throw ie; } } @Override public void doneSendingNotifications() throws Exception { // do nothing - no batching } @Override public void removeInactiveDevices() throws Exception { PushManager<SimpleApnsPushNotification> pushManager = getPushManager(notifier); pushManager.requestExpiredTokens(); } private EntityPushManager getPushManager(Notifier notifier) throws ExecutionException { if (pushManager == null || !pushManager.isStarted() || pushManager.isShutDown()) { PushManagerConfiguration config = new PushManagerConfiguration(); config.setConcurrentConnectionCount(Runtime.getRuntime().availableProcessors() * 2); queue = new ArrayBlockingQueue<>(10000); pushManager = new EntityPushManager(notifier, entityManager, queue, config); //only tested when a message is sent pushManager.registerRejectedNotificationListener(new RejectedAPNsListener()); //this will get tested when start is called pushManager.registerFailedConnectionListener(new FailedConnectionListener()); //unregistered expired devices pushManager.registerExpiredTokenListener(new ExpiredTokenListener()); try { if (!pushManager.isStarted()) { //ensure manager is started pushManager.start(); } } catch (IllegalStateException ise) { logger.warn("getPushManager: failed to start", ise);//could have failed because its started } } return pushManager; } @Override public Object translatePayload(Object objPayload) throws Exception { String payload; if (objPayload instanceof String) { payload = (String) objPayload; if (!payload.startsWith("{")) { payload = "{\"aps\":{\"alert\":\"" + payload + "\"}}"; } } else { payload = objectMapper.writeValueAsString( objPayload ); } if (payload.length() > 2048) { throw new IllegalArgumentException( "Apple APNs payloads must be 2048 characters or less"); } return payload; } @Override public void validateCreateNotifier(ServicePayload payload) throws Exception { String environment = payload.getStringProperty("environment"); if (!validEnvironments.contains(environment)) { throw new IllegalArgumentException("environment must be one of: " + Arrays.toString(validEnvironments.toArray())); } if (payload.getProperty("p12Certificate") == null) { throw new RequiredPropertyNotFoundException("notifier", "p12Certificate"); } } @Override public void stop() { try { EntityPushManager entityPushManager = getPushManager(notifier); if (!entityPushManager.isShutDown()) { List<SimpleApnsPushNotification> notifications = entityPushManager.shutdown(3000); for (SimpleApnsPushNotification notification1 : notifications) { try { ((APNsNotification) notification1).messageSendFailed(new Exception("Cache Expired: Shutting down sender")); } catch (Exception e) { logger.error("Failed to mark notification", e); } } } } catch (Exception ie) { logger.error("Failed to shutdown from cache", ie); } } @Override public Notifier getNotifier(){return notifier;} }