/* * Copyright (c) 2014. * * BaasBox - info-at-baasbox.com * * 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 com.baasbox.service.push.providers; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import com.baasbox.configuration.IosCertificateHandler; import com.baasbox.exception.BaasBoxPushException; import com.baasbox.service.logging.BaasBoxLogger; import com.baasbox.service.logging.PushLogger; import com.baasbox.service.push.PushNotInitializedException; import com.baasbox.service.push.providers.Factory.ConfigurationKeys; import com.baasbox.util.ConfigurationFileContainer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsDelegate; import com.notnoop.apns.ApnsNotification; import com.notnoop.apns.ApnsService; import com.notnoop.apns.DeliveryError; import com.notnoop.apns.PayloadBuilder; import com.notnoop.exceptions.NetworkIOException; public class APNServer extends PushProviderAbstract { private String certificate; private String password; private boolean sandbox; private int timeout; private boolean isInit=false; private ApnsDelegate apnDelegate=new ApnsDelegate(){ PushLogger pushLogger = PushLogger.getInstance(); @Override public void cacheLengthExceeded(int arg0) { pushLogger.addMessage("The resend cache needed a bigger size: %d",arg0); } @Override public void connectionClosed(DeliveryError err, int msgId) { pushLogger.addMessage("The connection was closed and/or an error packet was received. Message id: %d error: %s error code: %d",msgId,err.name(), err.code()); } @Override public void messageSendFailed(ApnsNotification notification, Throwable e) { pushLogger.addMessage("***** Error sending the message:"); if (notification!=null){ pushLogger.addMessage("** message : ",notification); }else{ pushLogger.addMessage("** unfortunately there is no info to log but the error.."); } pushLogger.addMessage("** error: %s",ExceptionUtils.getMessage(e)); } @Override public void messageSent(ApnsNotification notification, boolean resent) { if (resent) pushLogger.addMessage("+++ Message %s was sent after an error",notification); else pushLogger.addMessage("+++ Message %s was sent",notification); } @Override public void notificationsResent(int arg0) { pushLogger.addMessage("..%d message(s) has/ve queued for resending due to an error-response from server",arg0 ); } }; APNServer(){ } @Override public boolean send(String message, List<String> deviceid, JsonNode bodyJson) throws Exception{ PushLogger pushLogger = PushLogger.getInstance(); pushLogger.addMessage("............ APN Push Message: -%s- to the device(s) %s" , message, deviceid); ApnsService service = null; try{ if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("APN Push message: "+message+" to the device "+deviceid); if (!isInit) { pushLogger.addMessage("............ APNS is not initialized!"); return true; } String payload = null; try{ service=getService(); } catch (com.notnoop.exceptions.InvalidSSLConfig e) { pushLogger.addMessage("Error sending push notification ..."); pushLogger.addMessage(" Exception is: %s ", ExceptionUtils.getStackTrace(e)); BaasBoxLogger.error("Error sending push notification"); throw new PushNotInitializedException("Error decrypting certificate.Verify your password for given certificate"); //icallbackPush.onError(ExceptionUtils.getMessage(e)); } JsonNode contentAvailableNode=bodyJson.findValue("content-available"); Integer contentAvailable = null; if(!(contentAvailableNode == null)) { if(!(contentAvailableNode.isInt())) throw new PushContentAvailableFormatException("Content-available MUST be an Integer (1 for silent notification)"); contentAvailable=contentAvailableNode.asInt(); } JsonNode categoryNode=bodyJson.findValue("category"); String category = null; if(!(categoryNode == null)) { if(!(categoryNode.isTextual())) throw new PushCategoryFormatException("Category MUST be a String"); category=categoryNode.asText(); } JsonNode soundNode=bodyJson.findValue("sound"); String sound =null; if (!(soundNode==null)) { if(!(soundNode.isTextual())) throw new PushSoundKeyFormatException("Sound value MUST be a String"); sound=soundNode.asText(); } JsonNode actionLocKeyNode=bodyJson.findValue("actionLocalizedKey"); String actionLocKey=null; if (!(actionLocKeyNode==null)) { if(!(actionLocKeyNode.isTextual())) throw new PushActionLocalizedKeyFormatException("ActionLocalizedKey MUST be a String"); actionLocKey=actionLocKeyNode.asText(); } JsonNode locKeyNode=bodyJson.findValue("localizedKey"); String locKey=null; if (!(locKeyNode==null)) { if(!(locKeyNode.isTextual())) throw new PushLocalizedKeyFormatException("LocalizedKey MUST be a String"); locKey=locKeyNode.asText(); } JsonNode locArgsNode=bodyJson.get("localizedArguments"); List<String> locArgs = new ArrayList<String>(); if(!(locArgsNode==null)){ if(!(locArgsNode.isArray())) throw new PushLocalizedArgumentsFormatException("LocalizedArguments MUST be an Array of String"); for(JsonNode locArgNode : locArgsNode) { if(locArgNode.isNumber()) throw new PushLocalizedArgumentsFormatException("LocalizedArguments MUST be an Array of String"); locArgs.add(locArgNode.toString()); } } JsonNode customDataNodes=bodyJson.get("custom"); Map<String,JsonNode> customData = new HashMap<String,JsonNode>(); if(!(customDataNodes==null)){ customData.put("custom",customDataNodes); } JsonNode badgeNode=bodyJson.findValue("badge"); int badge=0; if(!(badgeNode==null)) { if(!(badgeNode.isNumber())) throw new PushBadgeFormatException("Badge value MUST be a number"); else badge=badgeNode.asInt(); } if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("APN Push message: "+message+" to the device "+deviceid +" with sound: " + sound + " with badge: " + badge + " with Action-Localized-Key: " + actionLocKey + " with Localized-Key: "+locKey); if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("Localized arguments: " + locArgs.toString()); if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("Custom Data: " + customData.toString()); pushLogger.addMessage("APN Push message: "+message+" to the device "+deviceid +" with sound: " + sound + " with badge: " + badge + " with Action-Localized-Key: " + actionLocKey + " with Localized-Key: "+locKey); pushLogger.addMessage("Localized arguments: " + locArgs.toString()); pushLogger.addMessage("Custom Data: " + customData.toString()); pushLogger.addMessage("Timeout: " + timeout); PayloadBuilder payloadBuilder = APNS.newPayload() .alertBody(message) .sound(sound) .actionKey(actionLocKey) .localizedKey(locKey) .localizedArguments(locArgs) .badge(badge) .customFields(customData) .category(category); if (contentAvailable!=null && contentAvailable.intValue()==1){ payloadBuilder.instantDeliveryOrSilentNotification(); } payload =payloadBuilder.build(); Collection<? extends ApnsNotification> result = null; if(timeout<=0){ try { result = service.push(deviceid, payload); } catch (NetworkIOException e) { pushLogger.addMessage("Error sending push notification ..."); pushLogger.addMessage(" Exception is: %s ", ExceptionUtils.getStackTrace(e)); BaasBoxLogger.error("Error sending push notification"); BaasBoxLogger.error(ExceptionUtils.getStackTrace(e)); throw new PushNotInitializedException("Error processing certificate, maybe it's revoked"); //icallbackPush.onError(ExceptionUtils.getMessage(e)); } } else { try { Date expiry = new Date(Long.MAX_VALUE); pushLogger.addMessage("Timeout is > 0 (%d), expiration date is set to %s", timeout, expiry.toString()); result = service.push (deviceid,payload,expiry); } catch (NetworkIOException e) { pushLogger.addMessage("Error sending push notification ..."); pushLogger.addMessage(" Exception is: %s ", ExceptionUtils.getStackTrace(e)); BaasBoxLogger.error("Error sending enhanced push notification"); BaasBoxLogger.error(ExceptionUtils.getStackTrace(e)); throw new PushNotInitializedException("Error processing certificate, maybe it's revoked"); //icallbackPush.onError(ExceptionUtils.getMessage(e)); } } if (result!=null){ Iterator<? extends ApnsNotification> it = result.iterator(); while (it.hasNext()){ ApnsNotification item = it.next(); //item. } } //icallbackPush.onSuccess(); return false; }catch (Exception e){ pushLogger.addMessage("Error sending push notification (APNS)..."); pushLogger.addMessage(ExceptionUtils.getMessage(e)); throw e; }finally{ if (service!=null) service.stop(); } } public static boolean validatePushPayload(JsonNode bodyJson) throws BaasBoxPushException { JsonNode soundNode=bodyJson.findValue("sound"); JsonNode contentAvailableNode=bodyJson.findValue("content-available"); Integer contentAvailable = null; if(!(contentAvailableNode == null)) { if(!(contentAvailableNode.isInt())) throw new PushContentAvailableFormatException("Content-available MUST be an Integer (1 for silent notification)"); contentAvailable=contentAvailableNode.asInt(); } if(contentAvailable!=null && contentAvailable!=1) { JsonNode categoryNode=bodyJson.findValue("category"); String category = null; if(!(categoryNode == null)) { if(!(categoryNode.isTextual())) throw new PushCategoryFormatException("Category MUST be a String"); category=categoryNode.asText(); } String sound =null; if (!(soundNode==null)) { if(!(soundNode.isTextual())) throw new PushSoundKeyFormatException("Sound value MUST be a String"); sound=soundNode.asText(); } JsonNode actionLocKeyNode=bodyJson.findValue("actionLocalizedKey"); String actionLocKey=null; if (!(actionLocKeyNode==null)) { if(!(actionLocKeyNode.isTextual())) throw new PushActionLocalizedKeyFormatException("ActionLocalizedKey MUST be a String"); actionLocKey=actionLocKeyNode.asText(); } JsonNode locKeyNode=bodyJson.findValue("localizedKey"); String locKey=null; if (!(locKeyNode==null)) { if(!(locKeyNode.isTextual())) throw new PushLocalizedKeyFormatException("LocalizedKey MUST be a String"); locKey=locKeyNode.asText(); } JsonNode locArgsNode=bodyJson.get("localizedArguments"); List<String> locArgs = new ArrayList<String>(); if(!(locArgsNode==null)){ if(!(locArgsNode.isArray())) throw new PushLocalizedArgumentsFormatException("LocalizedArguments MUST be an Array of String"); for(JsonNode locArgNode : locArgsNode) { if(!locArgNode.isTextual()) throw new PushLocalizedArgumentsFormatException("LocalizedArguments MUST be an Array of String"); locArgs.add(locArgNode.toString()); } } JsonNode customDataNodes=bodyJson.get("custom"); Map<String,JsonNode> customData = new HashMap<String,JsonNode>(); if(!(customDataNodes==null)){ if(customDataNodes.isTextual()) { customData.put("custom",customDataNodes); } else { for(JsonNode customDataNode : customDataNodes) { customData.put("custom", customDataNodes); } } } JsonNode badgeNode=bodyJson.findValue("badge"); int badge=0; if(!(badgeNode==null)) { if(!(badgeNode.isNumber())) throw new PushBadgeFormatException("Badge value MUST be a number"); else badge=badgeNode.asInt(); } } return true; } private ApnsService getService() { ApnsService service; PushLogger pushLogger = PushLogger.getInstance(); if (!sandbox) { service=APNS.newService() .withCert(certificate, password) .withProductionDestination() .withDelegate(apnDelegate) .build(); pushLogger.addMessage("............ APNS production mode"); } else { service=APNS.newService() .withCert(certificate, password) .withSandboxDestination() .withDelegate(apnDelegate) .build(); pushLogger.addMessage("............ APNS sandbox mode"); } return service; } @Override public void setConfiguration(ImmutableMap<ConfigurationKeys, String> configuration) { String json = configuration.get(ConfigurationKeys.IOS_CERTIFICATE); String name = null; ObjectMapper mp = new ObjectMapper(); try{ ConfigurationFileContainer cfc = mp.readValue(json, ConfigurationFileContainer.class); if (cfc==null){ isInit=false; return; } name = cfc.getName(); }catch(Exception e){ BaasBoxLogger.error(ExceptionUtils.getMessage(e)); throw new RuntimeException(e); } if(name!=null && !name.equals("null")){ File f = IosCertificateHandler.getCertificate(name); this.certificate=f.getAbsolutePath(); } password=configuration.get(ConfigurationKeys.IOS_CERTIFICATE_PASSWORD); sandbox=configuration.get(ConfigurationKeys.IOS_SANDBOX).equalsIgnoreCase("true"); timeout=Integer.parseInt(configuration.get(ConfigurationKeys.APPLE_TIMEOUT)); isInit=StringUtils.isNotEmpty(this.certificate) && StringUtils.isNotEmpty(password); } }