commit push server

This commit is contained in:
heavyrain.lee
2019-01-18 21:41:21 +08:00
parent 903b9b947f
commit cc513446b9
63 changed files with 5896 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package cn.wildfirechat.push;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PushApplication {
public static void main(String[] args) {
SpringApplication.run(PushApplication.class, args);
}
}

View File

@@ -0,0 +1,28 @@
package cn.wildfirechat.push;
import cn.wildfirechat.push.android.AndroidPushService;
import cn.wildfirechat.push.ios.IOSPushService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PushController {
@Autowired
private AndroidPushService mAndroidPushService;
@Autowired
private IOSPushService mIOSPushService;
@PostMapping(value = "/android/push", produces = "application/json;charset=UTF-8" )
public Object androidPush(@RequestBody PushMessage pushMessage) {
return mAndroidPushService.push(pushMessage);
}
@PostMapping(value = "/ios/push", produces = "application/json;charset=UTF-8" )
public Object iOSPush(@RequestBody PushMessage pushMessage) {
return mIOSPushService.push(pushMessage);
}
}

View File

@@ -0,0 +1,161 @@
package cn.wildfirechat.push;
public class PushMessage {
public String sender;
public String senderName;
public int convType;
public String target;
public String targetName;
public int line;
public int cntType;
public long serverTime;
//消息的类型普通消息通知栏voip要透传。
public int pushMessageType;
//推送类型android推送分为小米/华为/魅族等。ios分别为开发和发布。
public int pushType;
public String pushContent;
public int unReceivedMsg;
public int mentionedType;
public String packageName;
public String deviceToken;
public String voipDeviceToken;
public String language;
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getSenderName() {
return senderName;
}
public void setSenderName(String senderName) {
this.senderName = senderName;
}
public int getConvType() {
return convType;
}
public void setConvType(int convType) {
this.convType = convType;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public String getTargetName() {
return targetName;
}
public void setTargetName(String targetName) {
this.targetName = targetName;
}
public int getLine() {
return line;
}
public void setLine(int line) {
this.line = line;
}
public int getCntType() {
return cntType;
}
public void setCntType(int cntType) {
this.cntType = cntType;
}
public long getServerTime() {
return serverTime;
}
public void setServerTime(long serverTime) {
this.serverTime = serverTime;
}
public int getPushMessageType() {
return pushMessageType;
}
public void setPushMessageType(int pushMessageType) {
this.pushMessageType = pushMessageType;
}
public int getPushType() {
return pushType;
}
public void setPushType(int pushType) {
this.pushType = pushType;
}
public String getPushContent() {
return pushContent;
}
public void setPushContent(String pushContent) {
this.pushContent = pushContent;
}
public int getUnReceivedMsg() {
return unReceivedMsg;
}
public void setUnReceivedMsg(int unReceivedMsg) {
this.unReceivedMsg = unReceivedMsg;
}
public int getMentionedType() {
return mentionedType;
}
public void setMentionedType(int mentionedType) {
this.mentionedType = mentionedType;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getDeviceToken() {
return deviceToken;
}
public void setDeviceToken(String deviceToken) {
this.deviceToken = deviceToken;
}
public String getVoipDeviceToken() {
return voipDeviceToken;
}
public void setVoipDeviceToken(String voipDeviceToken) {
this.voipDeviceToken = voipDeviceToken;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}

View File

@@ -0,0 +1,7 @@
package cn.wildfirechat.push;
public interface PushMessageType {
int PUSH_MESSAGE_TYPE_NORMAL = 0;
int PUSH_MESSAGE_TYPE_VOIP_INVITE = 1;
int PUSH_MESSAGE_TYPE_VOIP_BYE = 2;
}

View File

@@ -0,0 +1,7 @@
package cn.wildfirechat.push.android;
import cn.wildfirechat.push.PushMessage;
public interface AndroidPushService {
Object push(PushMessage pushMessage);
}

View File

@@ -0,0 +1,45 @@
package cn.wildfirechat.push.android;
import cn.wildfirechat.push.PushMessage;
import cn.wildfirechat.push.android.hms.HMSPush;
import cn.wildfirechat.push.android.meizu.MeiZuPush;
import cn.wildfirechat.push.android.xiaomi.XiaomiPush;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AndroidPushServiceImpl implements AndroidPushService {
private static final Logger LOG = LoggerFactory.getLogger(AndroidPushServiceImpl.class);
@Autowired
private HMSPush hmsPush;
@Autowired
private MeiZuPush meiZuPush;
@Autowired
private XiaomiPush xiaomiPush;
@Override
public Object push(PushMessage pushMessage) {
LOG.info("Android push {}", new Gson().toJson(pushMessage));
switch (pushMessage.getPushType()) {
case AndroidPushType.ANDROID_PUSH_TYPE_XIAOMI:
xiaomiPush.push(pushMessage);
break;
case AndroidPushType.ANDROID_PUSH_TYPE_HUAWEI:
hmsPush.push(pushMessage);
break;
case AndroidPushType.ANDROID_PUSH_TYPE_MEIZU:
meiZuPush.push(pushMessage);
break;
default:
LOG.info("unknown push type");
break;
}
return "ok";
}
}

View File

@@ -0,0 +1,7 @@
package cn.wildfirechat.push.android;
public interface AndroidPushType {
int ANDROID_PUSH_TYPE_XIAOMI = 1;
int ANDROID_PUSH_TYPE_HUAWEI = 2;
int ANDROID_PUSH_TYPE_MEIZU = 3;
}

View File

@@ -0,0 +1,29 @@
package cn.wildfirechat.push.android.hms;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ConfigurationProperties(prefix="hms")
@PropertySource(value = "classpath:hms.properties")
public class HMSConfig {
private String appSecret;
private String appId;
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,136 @@
package cn.wildfirechat.push.android.hms;
import cn.wildfirechat.push.PushMessage;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.List;
@Component
public class HMSPush {
private static final Logger LOG = LoggerFactory.getLogger(HMSPush.class);
private static final String tokenUrl = "https://login.vmall.com/oauth2/token"; //获取认证Token的URL
private static final String apiUrl = "https://api.push.hicloud.com/pushsend.do"; //应用级消息下发API
private String accessToken;//下发通知消息的认证Token
private long tokenExpiredTime; //accessToken的过期时间
@Autowired
private HMSConfig mConfig;
//获取下发通知消息的认证Token
private void refreshToken() throws IOException {
LOG.info("hms refresh token");
String msgBody = MessageFormat.format(
"grant_type=client_credentials&client_secret={0}&client_id={1}",
URLEncoder.encode(mConfig.getAppSecret(), "UTF-8"), mConfig.getAppId());
String response = httpPost(tokenUrl, msgBody, 5000, 5000);
JSONObject obj = JSONObject.parseObject(response);
accessToken = obj.getString("access_token");
tokenExpiredTime = System.currentTimeMillis() + obj.getLong("expires_in") - 5*60*1000;
LOG.info("hms refresh token with result {}", response);
}
//发送Push消息
public void push(PushMessage pushMessage) {
if (tokenExpiredTime <= System.currentTimeMillis()) {
try {
refreshToken();
} catch (IOException e) {
e.printStackTrace();
}
}
/*PushManager.requestToken为客户端申请token的方法可以调用多次以防止申请token失败*/
/*PushToken不支持手动编写需使用客户端的onToken方法获取*/
JSONArray deviceTokens = new JSONArray();//目标设备Token
deviceTokens.add(pushMessage.getDeviceToken());
JSONObject msg = new JSONObject();
msg.put("type", 1);//3: 通知栏消息,异步透传消息请根据接口文档设置
msg.put("body", new Gson().toJson(pushMessage));//通知栏消息body内容
JSONObject hps = new JSONObject();//华为PUSH消息总结构体
hps.put("msg", msg);
JSONObject payload = new JSONObject();
payload.put("hps", hps);
LOG.info("send push to HMS {}", payload);
try {
String postBody = MessageFormat.format(
"access_token={0}&nsp_svc={1}&nsp_ts={2}&device_token_list={3}&payload={4}",
URLEncoder.encode(accessToken,"UTF-8"),
URLEncoder.encode("openpush.message.api.send","UTF-8"),
URLEncoder.encode(String.valueOf(System.currentTimeMillis() / 1000),"UTF-8"),
URLEncoder.encode(deviceTokens.toString(),"UTF-8"),
URLEncoder.encode(payload.toString(),"UTF-8"));
String postUrl = apiUrl + "?nsp_ctx=" + URLEncoder.encode("{\"ver\":\"1\", \"appId\":\"" + mConfig.getAppId() + "\"}", "UTF-8");
String response = httpPost(postUrl, postBody, 5000, 5000);
LOG.info("Push to {} response {}", pushMessage.getDeviceToken(), response);
} catch (IOException e) {
e.printStackTrace();
LOG.info("Push to {} with exception", pushMessage.getDeviceToken(), e);
}
}
public String httpPost(String httpUrl, String data, int connectTimeout, int readTimeout) throws IOException {
OutputStream outPut = null;
HttpURLConnection urlConnection = null;
InputStream in = null;
try {
URL url = new URL(httpUrl);
urlConnection = (HttpURLConnection)url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
urlConnection.setDoInput(true);
urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
urlConnection.setConnectTimeout(connectTimeout);
urlConnection.setReadTimeout(readTimeout);
urlConnection.connect();
// POST data
outPut = urlConnection.getOutputStream();
outPut.write(data.getBytes("UTF-8"));
outPut.flush();
// read response
if (urlConnection.getResponseCode() < 400) {
in = urlConnection.getInputStream();
} else {
in = urlConnection.getErrorStream();
}
List<String> lines = IOUtils.readLines(in, urlConnection.getContentEncoding());
StringBuffer strBuf = new StringBuffer();
for (String line : lines) {
strBuf.append(line);
}
LOG.info(strBuf.toString());
return strBuf.toString();
}
finally {
IOUtils.closeQuietly(outPut);
IOUtils.closeQuietly(in);
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
}

View File

@@ -0,0 +1,29 @@
package cn.wildfirechat.push.android.meizu;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ConfigurationProperties(prefix="meizu")
@PropertySource(value = "classpath:meizu.properties")
public class MeiZuConfig {
private String appSecret;
private long appId;
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public long getAppId() {
return appId;
}
public void setAppId(long appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,68 @@
package cn.wildfirechat.push.android.meizu;
import cn.wildfirechat.push.PushMessage;
import com.meizu.push.sdk.server.IFlymePush;
import com.meizu.push.sdk.server.constant.ResultPack;
import com.meizu.push.sdk.server.model.push.PushResult;
import com.meizu.push.sdk.server.model.push.VarnishedMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class MeiZuPush {
private static final Logger LOG = LoggerFactory.getLogger(MeiZuPush.class);
private IFlymePush flymePush;
@PostConstruct
public void init() {
this.flymePush = new IFlymePush(mConfig.getAppSecret());
}
@Autowired
private MeiZuConfig mConfig;
public void push(PushMessage pushMessage) {
//组装透传消息
VarnishedMessage message = new VarnishedMessage.Builder()
.appId(mConfig.getAppId())
.title("WildfireChat")
.content(pushMessage.pushContent)
.validTime(1)
.build();
//目标用户
List<String> pushIds = new ArrayList<String>();
pushIds.add(pushMessage.getDeviceToken());
try {
// 1 调用推送服务
ResultPack<PushResult> result = flymePush.pushMessage(message, pushIds);
if (result.isSucceed()) {
// 2 调用推送服务成功 其中map为设备的具体推送结果一般业务针对超速的code类型做处理
PushResult pushResult = result.value();
String msgId = pushResult.getMsgId();//推送消息ID用于推送流程明细排查
Map<String, List<String>> targetResultMap = pushResult.getRespTarget();//推送结果全部推送成功则map为empty
LOG.info("push result:" + pushResult);
if (targetResultMap != null && !targetResultMap.isEmpty()) {
System.err.println("push fail token:" + targetResultMap);
}
} else {
// 调用推送接口服务异常 eg: appId、appKey非法、推送消息非法.....
// result.code(); //服务异常码
// result.comment();//服务异常描述
LOG.info(String.format("pushMessage error code:%s comment:%s", result.code(), result.comment()));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,20 @@
package cn.wildfirechat.push.android.xiaomi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ConfigurationProperties(prefix="xiaomi")
@PropertySource(value = "classpath:xiaomi.properties")
public class XiaomiConfig {
private String appSecret;
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
}

View File

@@ -0,0 +1,71 @@
package cn.wildfirechat.push.android.xiaomi;
import cn.wildfirechat.push.PushMessage;
import cn.wildfirechat.push.PushMessageType;
import com.google.gson.Gson;
import com.xiaomi.xmpush.server.Constants;
import com.xiaomi.xmpush.server.Message;
import com.xiaomi.xmpush.server.Result;
import com.xiaomi.xmpush.server.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.json.simple.parser.ParseException;
import java.io.IOException;
import static com.xiaomi.xmpush.server.Message.NOTIFY_TYPE_ALL;
@Component
public class XiaomiPush {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiPush.class);
@Autowired
private XiaomiConfig mConfig;
public void push(PushMessage pushMessage) {
Constants.useOfficial();
Sender sender = new Sender(mConfig.getAppSecret());
Message message;
if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) {
//voip
long timeToLive = 60 * 1000; // 1 min
message = new Message.Builder()
.payload(new Gson().toJson(pushMessage))
.restrictedPackageName(pushMessage.getPackageName())
.passThrough(1) //透传
.timeToLive(timeToLive)
.enableFlowControl(false)
.build();
} else {
long timeToLive = 600 * 1000;//10 min
message = new Message.Builder()
.payload(new Gson().toJson(pushMessage))
.title("新消息提醒")
.description(pushMessage.pushContent)
.notifyType(NOTIFY_TYPE_ALL)
.restrictedPackageName(pushMessage.getPackageName())
.passThrough(0)
.timeToLive(timeToLive)
.enableFlowControl(true)
.build();
}
Result result = null;
try {
result = sender.send(message, pushMessage.getDeviceToken(), 3);
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
LOG.info("Server response: MessageId: " + result.getMessageId()
+ " ErrorCode: " + result.getErrorCode().toString()
+ " Reason: " + result.getReason());
}
}

View File

@@ -0,0 +1,86 @@
package cn.wildfirechat.push.ios;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ConfigurationProperties(prefix="apns")
@PropertySource(value = "classpath:apns.properties")
public class ApnsConfig {
String productCerPath;
String productCerPwd;
String developCerPath;
String developCerPwd;
String voipCerPath;
String voipCerPwd;
String alert;
String voipAlert;
public String getProductCerPath() {
return productCerPath;
}
public void setProductCerPath(String productCerPath) {
this.productCerPath = productCerPath;
}
public String getProductCerPwd() {
return productCerPwd;
}
public void setProductCerPwd(String productCerPwd) {
this.productCerPwd = productCerPwd;
}
public String getDevelopCerPath() {
return developCerPath;
}
public void setDevelopCerPath(String developCerPath) {
this.developCerPath = developCerPath;
}
public String getDevelopCerPwd() {
return developCerPwd;
}
public void setDevelopCerPwd(String developCerPwd) {
this.developCerPwd = developCerPwd;
}
public String getVoipCerPath() {
return voipCerPath;
}
public void setVoipCerPath(String voipCerPath) {
this.voipCerPath = voipCerPath;
}
public String getVoipCerPwd() {
return voipCerPwd;
}
public void setVoipCerPwd(String voipCerPwd) {
this.voipCerPwd = voipCerPwd;
}
public String getAlert() {
return alert;
}
public void setAlert(String alert) {
this.alert = alert;
}
public String getVoipAlert() {
return voipAlert;
}
public void setVoipAlert(String voipAlert) {
this.voipAlert = voipAlert;
}
}

View File

@@ -0,0 +1,187 @@
package cn.wildfirechat.push.ios;
import cn.wildfirechat.push.PushMessage;
import cn.wildfirechat.push.PushMessageType;
import com.notnoop.apns.*;
import com.notnoop.exceptions.ApnsDeliveryErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.Map;
import static com.notnoop.apns.DeliveryError.INVALID_TOKEN;
@Component
public class ApnsServer implements ApnsDelegate {
private static final Logger LOG = LoggerFactory.getLogger(ApnsServer.class);
@Override
public void messageSent(ApnsNotification message, boolean resent) {
LOG.info("APNS push sent:{}", message.getDeviceToken());
}
@Override
public void messageSendFailed(ApnsNotification message, Throwable e) {
LOG.info("APNS push failure:{}", e.getMessage());
if(e instanceof ApnsDeliveryErrorException) {
ApnsDeliveryErrorException apnsDeliveryErrorException = (ApnsDeliveryErrorException)e;
LOG.info("APNS error code:{}", apnsDeliveryErrorException.getDeliveryError());
if (apnsDeliveryErrorException.getDeliveryError() == INVALID_TOKEN) {
if (message.getDeviceId() != null) {
LOG.error("Invalide token!!!");
} else {
LOG.error("APNS ERROR without deviceId:{}", message);
}
}
}
}
@Override
public void connectionClosed(DeliveryError e, int messageIdentifier) {
LOG.info("111");
}
@Override
public void cacheLengthExceeded(int newCacheLength) {
LOG.info("111");
}
@Override
public void notificationsResent(int resendCount) {
LOG.info("111");
}
ApnsService productSvc;
ApnsService developSvc;
ApnsService voipSvc;
@Autowired
private ApnsConfig mConfig;
@PostConstruct
private void init() {
if (StringUtils.isEmpty(mConfig.alert)) {
mConfig.alert = "default";
}
if (StringUtils.isEmpty(mConfig.voipAlert)) {
mConfig.alert = "default";
}
productSvc = APNS.newService()
.asBatched(3, 10)
.withAppleDestination(true)
.withCert(mConfig.productCerPath, mConfig.productCerPwd)
.withDelegate(this)
.build();
developSvc = APNS.newService()
.asBatched(3, 10)
.withAppleDestination(false)
.withCert(mConfig.developCerPath, mConfig.developCerPwd)
.withDelegate(this)
.build();
voipSvc = APNS.newService()
.withAppleDestination(true)
.withCert(mConfig.voipCerPath, mConfig.voipCerPwd)
.withDelegate(this)
.build();
productSvc.start();
developSvc.start();
voipSvc.start();
}
public void pushMessage(PushMessage pushMessage) {
ApnsService service = developSvc;
if (pushMessage.getPushType() == IOSPushType.IOS_PUSH_TYPE_DISTRIBUTION) {
if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_NORMAL || StringUtils.isEmpty(pushMessage.getVoipDeviceToken())) {
service = productSvc;
} else {
service = voipSvc;
}
}
if (service == null) {
LOG.error("Service not exist!!!!");
return;
}
String sound = mConfig.alert;
String pushContent = pushMessage.getPushContent();
if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE) {
pushContent = "通话邀请";
sound = mConfig.voipAlert;
} else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE) {
pushContent = "通话结束";
sound = null;
}
int badge = pushMessage.getUnReceivedMsg();
if (badge <= 0) {
badge = 1;
}
String title;
String body;
//todo 这里需要加上语言的处理客户端会上报自己的语言在DeviceInfo那个类中
// if (pushMessage.language == "zh_CN") {
//
// } else if(pushMessage.language == "US_EN") {
//
// }
if (pushMessage.convType == 1) {
title = pushMessage.targetName;
if (StringUtils.isEmpty(title)) {
title = "群聊";
}
if (StringUtils.isEmpty(pushMessage.senderName)) {
body = pushContent;
} else {
body = pushMessage.senderName + ":" + pushContent;
}
if (pushMessage.mentionedType == 1) {
if (StringUtils.isEmpty(pushMessage.senderName)) {
body = "有人在群里@了你";
} else {
body = pushMessage.senderName + "在群里@了你";
}
} else if(pushMessage.mentionedType == 2) {
if (StringUtils.isEmpty(pushMessage.senderName)) {
body = "有人在群里@了大家";
} else {
body = pushMessage.senderName + "在群里@了大家";
}
}
} else {
if (StringUtils.isEmpty(pushMessage.senderName)) {
title = "消息";
} else {
title = pushMessage.senderName;
}
body = pushContent;
}
final String payload = APNS.newPayload().alertBody(body).badge(badge).alertTitle(title).sound(sound).build();
final ApnsNotification goodMsg = service.push(service == voipSvc ? pushMessage.getVoipDeviceToken() : pushMessage.getDeviceToken(), payload, null);
LOG.info("Message id: " + goodMsg.getIdentifier());
//检查key到期日期
final Map<String, Date> inactiveDevices = service.getInactiveDevices();
for (final Map.Entry<String, Date> ent : inactiveDevices.entrySet()) {
LOG.info("Inactive " + ent.getKey() + " at date " + ent.getValue());
}
}
}

View File

@@ -0,0 +1,7 @@
package cn.wildfirechat.push.ios;
import cn.wildfirechat.push.PushMessage;
public interface IOSPushService {
Object push(PushMessage pushMessage);
}

View File

@@ -0,0 +1,22 @@
package cn.wildfirechat.push.ios;
import cn.wildfirechat.push.PushMessage;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class IOSPushServiceImpl implements IOSPushService {
private static final Logger LOG = LoggerFactory.getLogger(IOSPushServiceImpl.class);
@Autowired
public ApnsServer apnsServer;
@Override
public Object push(PushMessage pushMessage) {
LOG.info("iOS push {}", new Gson().toJson(pushMessage));
apnsServer.pushMessage(pushMessage);
return "OK";
}
}

View File

@@ -0,0 +1,6 @@
package cn.wildfirechat.push.ios;
public interface IOSPushType {
int IOS_PUSH_TYPE_DISTRIBUTION = 0;
int IOS_PUSH_TYPE_DEVELOPEMENT = 1;
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* The main class to interact with the APNS Service.
*
* Provides an interface to create the {@link ApnsServiceBuilder} and
* {@code ApnsNotification} payload.
*
*/
public final class APNS {
private APNS() { throw new AssertionError("Uninstantiable class"); }
/**
* Returns a new Payload builder
*/
public static PayloadBuilder newPayload() {
return new PayloadBuilder();
}
/**
* Returns a new APNS Service for sending iPhone notifications
*/
public static ApnsServiceBuilder newService() {
return new ApnsServiceBuilder();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* A delegate that gets notified of the status of notification delivery to the
* Apple Server.
*
* The delegate doesn't get notified when the notification actually arrives at
* the phone.
*/
public interface ApnsDelegate {
/**
* Called when message was successfully sent to the Apple servers
*
* @param message the notification that was sent
* @param resent whether the notification was resent after an error
*/
public void messageSent(ApnsNotification message, boolean resent);
/**
* Called when the delivery of the message failed for any reason
*
* If message is null, then your notification has been rejected by Apple but
* it has been removed from the cache so it is not possible to identify
* which notification caused the error. In this case subsequent
* notifications may be lost. If this happens you should consider increasing
* your cacheLength value to prevent data loss.
*
* @param message the notification that was attempted to be sent
* @param e the cause and description of the failure
*/
public void messageSendFailed(ApnsNotification message, Throwable e);
/**
* The connection was closed and/or an error packet was received while
* monitoring was turned on.
*
* @param e the delivery error
* @param messageIdentifier id of the message that failed
*/
public void connectionClosed(DeliveryError e, int messageIdentifier);
/**
* The resend cache needed a bigger size (while resending messages)
*
* @param newCacheLength new size of the resend cache.
*/
public void cacheLengthExceeded(int newCacheLength);
/**
* A number of notifications has been queued for resending due to a error-response
* packet being received.
*
* @param resendCount the number of messages being queued for resend
*/
public void notificationsResent(int resendCount);
/**
* A no operation delegate that does nothing!
*/
public final static ApnsDelegate EMPTY = new ApnsDelegateAdapter();
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* A no operation delegate that does nothing!
*/
public class ApnsDelegateAdapter implements ApnsDelegate {
public void messageSent(ApnsNotification message, boolean resent) {
}
public void messageSendFailed(ApnsNotification message, Throwable e) {
}
public void connectionClosed(DeliveryError e, int messageIdentifier) {
}
public void cacheLengthExceeded(int newCacheLength) {
}
public void notificationsResent(int resendCount) {
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* Represents an APNS notification to be sent to Apple service.
*/
public interface ApnsNotification {
/**
* Returns the binary representation of the device token.
*/
public byte[] getDeviceToken();
/**
* Returns the binary representation of the payload.
*
*/
public byte[] getPayload();
/**
* Returns the identifier of the current message. The
* identifier is an application generated identifier.
*
* @return the notification identifier
*/
public int getIdentifier();
/**
* Returns the expiry date of the notification, a fixed UNIX
* epoch date expressed in seconds
*
* @return the expiry date of the notification
*/
public int getExpiry();
/**
* Returns the binary representation of the message as expected by the
* APNS server.
*
* The returned array can be used to sent directly to the APNS server
* (on the wire/socket) without any modification.
*/
public byte[] marshall();
public String getDeviceId();
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import com.notnoop.exceptions.NetworkIOException;
/**
* Represents the connection and interface to the Apple APNS servers.
*
* The service is created by {@link ApnsServiceBuilder} like:
*
* <pre>
* ApnsService = APNS.newService()
* .withCert("/path/to/certificate.p12", "MyCertPassword")
* .withSandboxDestination()
* .build()
* </pre>
*/
public interface ApnsService {
/**
* Sends a push notification with the provided {@code payload} to the
* iPhone of {@code deviceToken}.
*
* The payload needs to be a valid JSON object, otherwise it may fail
* silently. It is recommended to use {@link PayloadBuilder} to create
* one.
*
* @param deviceToken the destination iPhone device token
* @param payload The payload message
* @throws NetworkIOException if a network error occurred while
* attempting to send the message
*/
ApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException;
EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException;
/**
* Sends a push notification with the provided {@code payload} to the
* iPhone of {@code deviceToken}.
*
* The payload needs to be a valid JSON object, otherwise it may fail
* silently. It is recommended to use {@link PayloadBuilder} to create
* one.
*
* @param deviceToken the destination iPhone device token
* @param payload The payload message
* @throws NetworkIOException if a network error occurred while
* attempting to send the message
*/
ApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException;
EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException;
/**
* Sends the provided notification {@code message} to the desired
* destination.
* @throws NetworkIOException if a network error occurred while
* attempting to send the message
*/
void push(ApnsNotification message) throws NetworkIOException;
/**
* Starts the service.
*
* The underlying implementation may prepare its connections or
* data structures to be able to send the messages.
*
* This method is a blocking call, even if the service represents
* a Non-blocking push service. Once the service is returned, it is ready
* to accept push requests.
*
* @throws NetworkIOException if a network error occurred while
* starting the service
*/
void start();
/**
* Stops the service and frees any allocated resources it created for this
* service.
*
* The underlying implementation should close all connections it created,
* and possibly stop any threads as well.
*/
void stop();
/**
* Returns the list of devices that reported failed-delivery
* attempts to the Apple Feedback services.
*
* The result is map, mapping the device tokens as Hex Strings
* mapped to the timestamp when APNs determined that the
* application no longer exists on the device.
* @throws NetworkIOException if a network error occurred
* while retrieving invalid device connection
*/
Map<String, Date> getInactiveDevices() throws NetworkIOException;
/**
* Test that the service is setup properly and the Apple servers
* are reachable.
*
* @throws NetworkIOException if the Apple servers aren't reachable
* or the service cannot send notifications for now
*/
void testConnection() throws NetworkIOException;
}

View File

@@ -0,0 +1,760 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import com.notnoop.apns.internal.ApnsConnection;
import com.notnoop.apns.internal.ApnsConnectionImpl;
import com.notnoop.apns.internal.ApnsFeedbackConnection;
import com.notnoop.apns.internal.ApnsPooledConnection;
import com.notnoop.apns.internal.ApnsServiceImpl;
import com.notnoop.apns.internal.BatchApnsService;
import com.notnoop.apns.internal.QueuedApnsService;
import com.notnoop.apns.internal.SSLContextBuilder;
import com.notnoop.apns.internal.Utilities;
import com.notnoop.exceptions.InvalidSSLConfig;
import com.notnoop.exceptions.RuntimeIOException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.security.KeyStore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_HOST;
import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_PORT;
import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_HOST;
import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_PORT;
import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_HOST;
import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_PORT;
import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_HOST;
import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_PORT;
import static java.util.concurrent.Executors.defaultThreadFactory;
/**
* The class is used to create instances of {@link ApnsService}.
*
* Note that this class is not synchronized. If multiple threads access a
* {@code ApnsServiceBuilder} instance concurrently, and at least on of the
* threads modifies one of the attributes structurally, it must be
* synchronized externally.
*
* Starting a new {@code ApnsService} is easy:
*
* <pre>
* ApnsService = APNS.newService()
* .withCert("/path/to/certificate.p12", "MyCertPassword")
* .withSandboxDestination()
* .build()
* </pre>
*/
public class ApnsServiceBuilder {
private static final String KEYSTORE_TYPE = "PKCS12";
private static final String KEY_ALGORITHM = ((java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm") == null)? "sunx509" : java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm"));
private SSLContext sslContext;
private int readTimeout;
private int connectTimeout;
private String gatewayHost;
private int gatewayPort = -1;
private String feedbackHost;
private int feedbackPort;
private int pooledMax = 1;
private int cacheLength = ApnsConnection.DEFAULT_CACHE_LENGTH;
private boolean autoAdjustCacheLength = true;
private ExecutorService executor;
private ReconnectPolicy reconnectPolicy = ReconnectPolicy.Provided.EVERY_HALF_HOUR.newObject();
private boolean isQueued;
private ThreadFactory queueThreadFactory;
private boolean isBatched;
private int batchWaitTimeInSec;
private int batchMaxWaitTimeInSec;
private ScheduledExecutorService batchThreadPoolExecutor;
private ApnsDelegate delegate = ApnsDelegate.EMPTY;
private Proxy proxy;
private String proxyUsername;
private String proxyPassword;
private boolean errorDetection = true;
private ThreadFactory errorDetectionThreadFactory;
/**
* Constructs a new instance of {@code ApnsServiceBuilder}
*/
public ApnsServiceBuilder() { sslContext = null; }
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on the path (absolute or relative to
* working path) to the keystore (*.p12) containing the
* certificate, along with the given password.
*
* The keystore needs to be of PKCS12 and the keystore
* needs to be encrypted using the SunX509 algorithm. Both
* of these settings are the default.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or construct
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param fileName the path to the certificate
* @param password the password of the keystore
* @return this
* @throws RuntimeIOException if it {@code fileName} cannot be
* found or read
* @throws InvalidSSLConfig if fileName is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(String fileName, String password)
throws RuntimeIOException, InvalidSSLConfig {
FileInputStream stream = null;
try {
stream = new FileInputStream(fileName);
return withCert(stream, password);
} catch (FileNotFoundException e) {
throw new RuntimeIOException(e);
} finally {
Utilities.close(stream);
}
}
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on the stream of keystore (*.p12)
* containing the certificate, along with the given password.
*
* The keystore needs to be of PKCS12 and the keystore
* needs to be encrypted using the SunX509 algorithm. Both
* of these settings are the default.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or constract
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param stream the keystore represented as input stream
* @param password the password of the keystore
* @return this
* @throws InvalidSSLConfig if stream is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(InputStream stream, String password)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(new SSLContextBuilder()
.withAlgorithm(KEY_ALGORITHM)
.withCertificateKeyStore(stream, password, KEYSTORE_TYPE)
.withDefaultTrustKeyStore()
.build());
}
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on a keystore (*.p12)
* containing the certificate, along with the given password.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or construct
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param keyStore the keystore
* @param password the password of the keystore
* @return this
* @throws InvalidSSLConfig if stream is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(KeyStore keyStore, String password)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(new SSLContextBuilder()
.withAlgorithm(KEY_ALGORITHM)
.withCertificateKeyStore(keyStore, password)
.withDefaultTrustKeyStore()
.build());
}
/**
* Specify the certificate store used to connect to Apple APNS
* servers. This relies on the stream of keystore (*.p12 | *.jks)
* containing the keys and certificates, along with the given
* password and alias.
*
* The keystore can be either PKCS12 or JKS and the keystore
* needs to be encrypted using the SunX509 algorithm.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or constract
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param stream the keystore represented as input stream
* @param password the password of the keystore
* @param alias the alias identifing the key to be used
* @return this
* @throws InvalidSSLConfig if stream is an invalid Keystore,
* the password is invalid or the alias is not found
*/
public ApnsServiceBuilder withCert(InputStream stream, String password, String alias)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(new SSLContextBuilder()
.withAlgorithm(KEY_ALGORITHM)
.withCertificateKeyStore(stream, password, KEYSTORE_TYPE, alias)
.withDefaultTrustKeyStore()
.build());
}
/**
* Specify the certificate store used to connect to Apple APNS
* servers. This relies on the stream of keystore (*.p12 | *.jks)
* containing the keys and certificates, along with the given
* password and alias.
*
* The keystore can be either PKCS12 or JKS and the keystore
* needs to be encrypted using the SunX509 algorithm.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or constract
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param keyStore the keystore
* @param password the password of the keystore
* @param alias the alias identifing the key to be used
* @return this
* @throws InvalidSSLConfig if stream is an invalid Keystore,
* the password is invalid or the alias is not found
*/
public ApnsServiceBuilder withCert(KeyStore keyStore, String password, String alias)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(new SSLContextBuilder()
.withAlgorithm(KEY_ALGORITHM)
.withCertificateKeyStore(keyStore, password, alias)
.withDefaultTrustKeyStore()
.build());
}
private void assertPasswordNotEmpty(String password) {
if (password == null || password.length() == 0) {
throw new IllegalArgumentException("Passwords must be specified." +
"Oracle Java SDK does not support passwordless p12 certificates");
}
}
/**
* Specify the SSLContext that should be used to initiate the
* connection to Apple Server.
*
* Most clients would use {@link #withCert(InputStream, String)}
* or {@link #withCert(String, String)} instead. But some
* clients may need to represent the Keystore in a different
* format than supported.
*
* @param sslContext Context to be used to create secure connections
* @return this
*/
public ApnsServiceBuilder withSSLContext(SSLContext sslContext) {
this.sslContext = sslContext;
return this;
}
/**
* Specify the timeout value to be set in new setSoTimeout in created
* sockets, for both feedback and push connections, in milliseconds.
* @param readTimeout timeout value to be set in new setSoTimeout
* @return this
*/
public ApnsServiceBuilder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* Specify the timeout value to use for connectionTimeout in created
* sockets, for both feedback and push connections, in milliseconds.
* @param connectTimeout timeout value to use for connectionTimeout
* @return this
*/
public ApnsServiceBuilder withConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* Specify the gateway server for sending Apple iPhone
* notifications.
*
* Most clients should use {@link #withSandboxDestination()}
* or {@link #withProductionDestination()}. Clients may use
* this method to connect to mocking tests and such.
*
* @param host hostname the notification gateway of Apple
* @param port port of the notification gateway of Apple
* @return this
*/
public ApnsServiceBuilder withGatewayDestination(String host, int port) {
this.gatewayHost = host;
this.gatewayPort = port;
return this;
}
/**
* Specify the Feedback for getting failed devices from
* Apple iPhone Push servers.
*
* Most clients should use {@link #withSandboxDestination()}
* or {@link #withProductionDestination()}. Clients may use
* this method to connect to mocking tests and such.
*
* @param host hostname of the feedback server of Apple
* @param port port of the feedback server of Apple
* @return this
*/
public ApnsServiceBuilder withFeedbackDestination(String host, int port) {
this.feedbackHost = host;
this.feedbackPort = port;
return this;
}
/**
* Specify to use Apple servers as iPhone gateway and feedback servers.
*
* If the passed {@code isProduction} is true, then it connects to the
* production servers, otherwise, it connects to the sandbox servers
*
* @param isProduction determines which Apple servers should be used:
* production or sandbox
* @return this
*/
public ApnsServiceBuilder withAppleDestination(boolean isProduction) {
if (isProduction) {
return withProductionDestination();
} else {
return withSandboxDestination();
}
}
/**
* Specify to use the Apple sandbox servers as iPhone gateway
* and feedback servers.
*
* This is desired when in testing and pushing notifications
* with a development provision.
*
* @return this
*/
public ApnsServiceBuilder withSandboxDestination() {
return withGatewayDestination(SANDBOX_GATEWAY_HOST, SANDBOX_GATEWAY_PORT)
.withFeedbackDestination(SANDBOX_FEEDBACK_HOST, SANDBOX_FEEDBACK_PORT);
}
/**
* Specify to use the Apple Production servers as iPhone gateway
* and feedback servers.
*
* This is desired when sending notifications to devices with
* a production provision (whether through App Store or Ad hoc
* distribution).
*
* @return this
*/
public ApnsServiceBuilder withProductionDestination() {
return withGatewayDestination(PRODUCTION_GATEWAY_HOST, PRODUCTION_GATEWAY_PORT)
.withFeedbackDestination(PRODUCTION_FEEDBACK_HOST, PRODUCTION_FEEDBACK_PORT);
}
/**
* Specify the reconnection policy for the socket connection.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy rp) {
this.reconnectPolicy = rp;
return this;
}
/**
* Specify if the notification cache should auto adjust.
* Default is true
*
* @param autoAdjustCacheLength the notification cache should auto adjust.
* @return this
*/
public ApnsServiceBuilder withAutoAdjustCacheLength(boolean autoAdjustCacheLength) {
this.autoAdjustCacheLength = autoAdjustCacheLength;
return this;
}
/**
* Specify the reconnection policy for the socket connection.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy.Provided rp) {
this.reconnectPolicy = rp.newObject();
return this;
}
/**
* Specify the address of the SOCKS proxy the connection should
* use.
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* <p>Be aware that this method only handles SOCKS proxies, not
* HTTPS proxies. Use {@link #withProxy(Proxy)} instead.
*
* @param host the hostname of the SOCKS proxy
* @param port the port of the SOCKS proxy server
* @return this
*/
public ApnsServiceBuilder withSocksProxy(String host, int port) {
Proxy proxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress(host, port));
return withProxy(proxy);
}
/**
* Specify the proxy and the authentication parameters to be used
* to establish the connections to Apple Servers.
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* @param proxy the proxy object to be used to create connections
* @param proxyUsername a String object representing the username of the proxy server
* @param proxyPassword a String object representing the password of the proxy server
* @return this
*/
public ApnsServiceBuilder withAuthProxy(Proxy proxy, String proxyUsername, String proxyPassword) {
this.proxy = proxy;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
return this;
}
/**
* Specify the proxy to be used to establish the connections
* to Apple Servers
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* @param proxy the proxy object to be used to create connections
* @return this
*/
public ApnsServiceBuilder withProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* Specify the number of notifications to cache for error purposes.
* Default is 100
*
* @param cacheLength Number of notifications to cache for error purposes
* @return this
*/
public ApnsServiceBuilder withCacheLength(int cacheLength) {
this.cacheLength = cacheLength;
return this;
}
/**
* Specify the socket to be used as underlying socket to connect
* to the APN service.
*
* This assumes that the socket connects to a SOCKS proxy.
*
* @deprecated use {@link ApnsServiceBuilder#withProxy(Proxy)} instead
* @param proxySocket the underlying socket for connections
* @return this
*/
@Deprecated
public ApnsServiceBuilder withProxySocket(Socket proxySocket) {
return this.withProxy(new Proxy(Proxy.Type.SOCKS,
proxySocket.getRemoteSocketAddress()));
}
/**
* Constructs a pool of connections to the notification servers.
*
* Apple servers recommend using a pooled connection up to
* 15 concurrent persistent connections to the gateways.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder asPool(int maxConnections) {
return asPool(Executors.newFixedThreadPool(maxConnections), maxConnections);
}
/**
* Constructs a pool of connections to the notification servers.
*
* Apple servers recommend using a pooled connection up to
* 15 concurrent persistent connections to the gateways.
*
* Note: This option has no effect when using non-blocking
* connections.
*
* Note: The maxConnections here is used as a hint to how many connections
* get created.
*/
public ApnsServiceBuilder asPool(ExecutorService executor, int maxConnections) {
this.pooledMax = maxConnections;
this.executor = executor;
return this;
}
/**
* Constructs a new thread with a processing queue to process
* notification requests.
*
* @return this
*/
public ApnsServiceBuilder asQueued() {
return asQueued(Executors.defaultThreadFactory());
}
/**
* Constructs a new thread with a processing queue to process
* notification requests.
*
* @param threadFactory
* thread factory to use for queue processing
* @return this
*/
public ApnsServiceBuilder asQueued(ThreadFactory threadFactory) {
this.isQueued = true;
this.queueThreadFactory = threadFactory;
return this;
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec (set as 5sec)</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec (set as 10sec)</code>
*
* Note: It is not recommended to use pooled connection
*/
public ApnsServiceBuilder asBatched() {
return asBatched(5, 10);
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec</code>
*
* Note: It is not recommended to use pooled connection
*
* @param waitTimeInSec
* time to wait for more notification request before executing
* batch
* @param maxWaitTimeInSec
* maximum wait time for batch before executing
*/
public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) {
return asBatched(waitTimeInSec, maxWaitTimeInSec, (ThreadFactory)null);
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec</code>
*
* Each batch creates new connection and close it after finished.
* In case reconnect policy is specified it will be applied by batch processing.
* E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour
*
* Note: It is not recommended to use pooled connection
*
* @param waitTimeInSec
* time to wait for more notification request before executing
* batch
* @param maxWaitTimeInSec
* maximum wait time for batch before executing
* @param threadFactory
* thread factory to use for batch processing
*/
public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ThreadFactory threadFactory) {
return asBatched(waitTimeInSec, maxWaitTimeInSec, new ScheduledThreadPoolExecutor(1, threadFactory != null ? threadFactory : defaultThreadFactory()));
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec</code>
*
* Each batch creates new connection and close it after finished.
* In case reconnect policy is specified it will be applied by batch processing.
* E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour
*
* Note: It is not recommended to use pooled connection
*
* @param waitTimeInSec
* time to wait for more notification request before executing
* batch
* @param maxWaitTimeInSec
* maximum wait time for batch before executing
* @param batchThreadPoolExecutor
* executor for batched processing (may be null)
*/
public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ScheduledExecutorService batchThreadPoolExecutor) {
this.isBatched = true;
this.batchWaitTimeInSec = waitTimeInSec;
this.batchMaxWaitTimeInSec = maxWaitTimeInSec;
this.batchThreadPoolExecutor = batchThreadPoolExecutor;
return this;
}
/**
* Sets the delegate of the service, that gets notified of the
* status of message delivery.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder withDelegate(ApnsDelegate delegate) {
this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate;
return this;
}
/**
* Disables the enhanced error detection, enabled by the
* enhanced push notification interface. Error detection is
* enabled by default.
*
* This setting is desired when the application shouldn't spawn
* new threads.
*
* @return this
*/
public ApnsServiceBuilder withNoErrorDetection() {
this.errorDetection = false;
return this;
}
/**
* Provide a custom source for threads used for monitoring connections.
*
* This setting is desired when the application must obtain threads from a
* controlled environment Google App Engine.
* @param threadFactory
* thread factory to use for error detection
* @return this
*/
public ApnsServiceBuilder withErrorDetectionThreadFactory(ThreadFactory threadFactory) {
this.errorDetectionThreadFactory = threadFactory;
return this;
}
/**
* Returns a fully initialized instance of {@link ApnsService},
* according to the requested settings.
*
* @return a new instance of ApnsService
*/
public ApnsService build() {
checkInitialization();
ApnsService service;
SSLSocketFactory sslFactory = sslContext.getSocketFactory();
ApnsFeedbackConnection feedback = new ApnsFeedbackConnection(sslFactory, feedbackHost, feedbackPort, proxy, readTimeout, connectTimeout, proxyUsername, proxyPassword);
ApnsConnection conn = new ApnsConnectionImpl(sslFactory, gatewayHost,
gatewayPort, proxy, proxyUsername, proxyPassword, reconnectPolicy,
delegate, errorDetection, errorDetectionThreadFactory, cacheLength,
autoAdjustCacheLength, readTimeout, connectTimeout);
if (pooledMax != 1) {
conn = new ApnsPooledConnection(conn, pooledMax, executor);
}
service = new ApnsServiceImpl(conn, feedback);
if (isQueued) {
service = new QueuedApnsService(service, queueThreadFactory);
}
if (isBatched) {
service = new BatchApnsService(conn, feedback, batchWaitTimeInSec, batchMaxWaitTimeInSec, batchThreadPoolExecutor);
}
service.start();
return service;
}
private void checkInitialization() {
if (sslContext == null)
throw new IllegalStateException(
"SSL Certificates and attribute are not initialized\n"
+ "Use .withCert() methods.");
if (gatewayHost == null || gatewayPort == -1)
throw new IllegalStateException(
"The Destination APNS server is not stated\n"
+ "Use .withDestination(), withSandboxDestination(), "
+ "or withProductionDestination().");
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* Errors in delivery that may get reported by Apple APN servers
*/
public enum DeliveryError {
/**
* Connection closed without any error.
*
* This may occur if the APN service faces an invalid simple
* APNS notification while running in enhanced mode
*/
NO_ERROR(0),
PROCESSING_ERROR(1),
MISSING_DEVICE_TOKEN(2),
MISSING_TOPIC(3),
MISSING_PAYLOAD(4),
INVALID_TOKEN_SIZE(5),
INVALID_TOPIC_SIZE(6),
INVALID_PAYLOAD_SIZE(7),
INVALID_TOKEN(8),
NONE(255),
UNKNOWN(254);
private final byte code;
DeliveryError(int code) {
this.code = (byte)code;
}
/** The status code as specified by Apple */
public int code() {
return code;
}
/**
* Returns the appropriate {@code DeliveryError} enum
* corresponding to the Apple provided status code
*
* @param code status code provided by Apple
* @return the appropriate DeliveryError
*/
public static DeliveryError ofCode(int code) {
for (DeliveryError e : DeliveryError.values()) {
if (e.code == code)
return e;
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import com.notnoop.apns.internal.Utilities;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
/**
* Represents an APNS notification to be sent to Apple service.
*/
public class EnhancedApnsNotification implements ApnsNotification {
private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedApnsNotification.class);
private final static byte COMMAND = 1;
private static AtomicInteger nextId = new AtomicInteger(0);
private final int identifier;
private final int expiry;
private final byte[] deviceToken;
private final byte[] payload;
private String deviceId;
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public static int INCREMENT_ID() {
return nextId.incrementAndGet();
}
/**
* The infinite future for the purposes of Apple expiry date
*/
public final static int MAXIMUM_EXPIRY = Integer.MAX_VALUE;
/**
* Constructs an instance of {@code ApnsNotification}.
*
* The message encodes the payload with a {@code UTF-8} encoding.
*
* @param dtoken The Hex of the device token of the destination phone
* @param payload The payload message to be sent
*/
public EnhancedApnsNotification(
int identifier, int expiryTime,
String dtoken, String payload) {
this.identifier = identifier;
this.expiry = expiryTime;
this.deviceToken = Utilities.decodeHex(dtoken);
this.payload = Utilities.toUTF8Bytes(payload);
}
/**
* Constructs an instance of {@code ApnsNotification}.
*
* @param dtoken The binary representation of the destination device token
* @param payload The binary representation of the payload to be sent
*/
public EnhancedApnsNotification(
int identifier, int expiryTime,
byte[] dtoken, byte[] payload) {
this.identifier = identifier;
this.expiry = expiryTime;
this.deviceToken = Utilities.copyOf(dtoken);
this.payload = Utilities.copyOf(payload);
}
/**
* Returns the binary representation of the device token.
*
*/
public byte[] getDeviceToken() {
return Utilities.copyOf(deviceToken);
}
/**
* Returns the binary representation of the payload.
*
*/
public byte[] getPayload() {
return Utilities.copyOf(payload);
}
public int getIdentifier() {
return identifier;
}
public int getExpiry() {
return expiry;
}
private byte[] marshall;
/**
* Returns the binary representation of the message as expected by the
* APNS server.
*
* The returned array can be used to sent directly to the APNS server
* (on the wire/socket) without any modification.
*/
public byte[] marshall() {
if (marshall == null) {
marshall = Utilities.marshallEnhanced(COMMAND, identifier,
expiry, deviceToken, payload);
}
return marshall.clone();
}
@Override
public String getDeviceId() {
return deviceId;
}
/**
* Returns the length of the message in bytes as it is encoded on the wire.
*
* Apple require the message to be of length 255 bytes or less.
*
* @return length of encoded message in bytes
*/
public int length() {
int length = 1 + 4 + 4 + 2 + deviceToken.length + 2 + payload.length;
final int marshalledLength = marshall().length;
assert marshalledLength == length;
return length;
}
@Override
public int hashCode() {
return (21
+ 31 * identifier
+ 31 * expiry
+ 31 * Arrays.hashCode(deviceToken)
+ 31 * Arrays.hashCode(payload));
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof EnhancedApnsNotification))
return false;
EnhancedApnsNotification o = (EnhancedApnsNotification)obj;
return (identifier == o.identifier
&& expiry == o.expiry
&& Arrays.equals(this.deviceToken, o.deviceToken)
&& Arrays.equals(this.payload, o.payload));
}
@Override
@SuppressFBWarnings("DE_MIGHT_IGNORE")
public String toString() {
String payloadString;
try {
payloadString = new String(payload, "UTF-8");
} catch (UnsupportedEncodingException ex) {
LOGGER.debug("UTF-8 charset not found on the JRE", ex);
payloadString = "???";
}
return "Message(Id="+identifier+"; Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")";
}
}

View File

@@ -0,0 +1,535 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.notnoop.apns.internal.Utilities;
/**
* Represents a builder for constructing Payload requests, as
* specified by Apple Push Notification Programming Guide.
*/
public final class PayloadBuilder {
private static final ObjectMapper mapper = new ObjectMapper();
private final Map<String, Object> root;
private final Map<String, Object> aps;
private final Map<String, Object> customAlert;
/**
* Constructs a new instance of {@code PayloadBuilder}
*/
PayloadBuilder() {
root = new HashMap<String, Object>();
aps = new HashMap<String, Object>();
customAlert = new HashMap<String, Object>();
}
/**
* Sets the alert body text, the text the appears to the user,
* to the passed value
*
* @param alert the text to appear to the user
* @return this
*/
public PayloadBuilder alertBody(final String alert) {
customAlert.put("body", alert);
return this;
}
/**
* Sets the alert title text, the text the appears to the user,
* to the passed value.
*
* Used on iOS 8.2, iWatch and also Safari
*
* @param title the text to appear to the user
* @return this
*/
public PayloadBuilder alertTitle(final String title) {
customAlert.put("title", title);
return this;
}
/**
* The key to a title string in the Localizable.strings file for the current localization.
*
* @param key the localizable message title key
* @return this
*/
public PayloadBuilder localizedTitleKey(final String key) {
customAlert.put("title-loc-key", key);
return this;
}
/**
* Sets the arguments for the localizable title key.
*
* @param arguments the arguments to the localized alert message
* @return this
*/
public PayloadBuilder localizedTitleArguments(final Collection<String> arguments) {
customAlert.put("title-loc-args", arguments);
return this;
}
/**
* Sets the arguments for the localizable title key.
*
* @param arguments the arguments to the localized alert message
* @return this
*/
public PayloadBuilder localizedTitleArguments(final String... arguments) {
return localizedTitleArguments(Arrays.asList(arguments));
}
/**
* Sets the alert action text
*
* @param action The label of the action button
* @return this
*/
public PayloadBuilder alertAction(final String action) {
customAlert.put("action", action);
return this;
}
/**
* Sets the "url-args" key that are paired with the placeholders
* inside the urlFormatString value of your website.json file.
* The order of the placeholders in the URL format string determines
* the order of the values supplied by the url-args array.
*
* @param urlArgs the values to be paired with the placeholders inside
* the urlFormatString value of your website.json file.
* @return this
*/
public PayloadBuilder urlArgs(final String... urlArgs){
aps.put("url-args", urlArgs);
return this;
}
/**
* Sets the alert sound to be played.
*
* Passing {@code null} disables the notification sound.
*
* @param sound the file name or song name to be played
* when receiving the notification
* @return this
*/
public PayloadBuilder sound(final String sound) {
if (sound != null) {
aps.put("sound", sound);
} else {
aps.remove("sound");
}
return this;
}
/**
* Sets the category of the notification for iOS8 notification
* actions. See 13 minutes into "What's new in iOS Notifications"
*
* Passing {@code null} removes the category.
*
* @param category the name of the category supplied to the app
* when receiving the notification
* @return this
*/
public PayloadBuilder category(final String category) {
if (category != null) {
aps.put("category", category);
} else {
aps.remove("category");
}
return this;
}
/**
* Sets the notification badge to be displayed next to the
* application icon.
*
* The passed value is the value that should be displayed
* (it will be added to the previous badge number), and
* a badge of 0 clears the badge indicator.
*
* @param badge the badge number to be displayed
* @return this
*/
public PayloadBuilder badge(final int badge) {
aps.put("badge", badge);
return this;
}
/**
* Requests clearing of the badge number next to the application
* icon.
*
* This is an alias to {@code badge(0)}.
*
* @return this
*/
public PayloadBuilder clearBadge() {
return badge(0);
}
/**
* Sets the value of action button (the right button to be
* displayed). The default value is "View".
*
* The value can be either the simple String to be displayed or
* a localizable key, and the iPhone will show the appropriate
* localized message.
*
* A {@code null} actionKey indicates no additional button
* is displayed, just the Cancel button.
*
* @param actionKey the title of the additional button
* @return this
*/
public PayloadBuilder actionKey(final String actionKey) {
customAlert.put("action-loc-key", actionKey);
return this;
}
/**
* Set the notification view to display an action button.
*
* This is an alias to {@code actionKey(null)}
*
* @return this
*/
public PayloadBuilder noActionButton() {
return actionKey(null);
}
/**
* Sets the notification type to be a 'newstand' notification.
*
* A Newstand Notification targets the Newstands app so that the app
* updates the subscription info and content.
*
* @return this
*/
public PayloadBuilder forNewsstand() {
aps.put("content-available", 1);
return this;
}
/**
* With iOS7 it is possible to have the application wake up before the user opens the app.
*
* The same key-word can also be used to send 'silent' notifications. With these 'silent' notification
* a different app delegate is being invoked, allowing the app to perform background tasks.
*
* @return this
*/
public PayloadBuilder instantDeliveryOrSilentNotification() {
aps.put("content-available", 1);
return this;
}
/**
* Set the notification localized key for the alert body
* message.
*
* @param key the localizable message body key
* @return this
*/
public PayloadBuilder localizedKey(final String key) {
customAlert.put("loc-key", key);
return this;
}
/**
* Sets the arguments for the alert message localizable message.
*
* The iPhone doesn't localize the arguments.
*
* @param arguments the arguments to the localized alert message
* @return this
*/
public PayloadBuilder localizedArguments(final Collection<String> arguments) {
customAlert.put("loc-args", arguments);
return this;
}
/**
* Sets the arguments for the alert message localizable message.
*
* The iPhone doesn't localize the arguments.
*
* @param arguments the arguments to the localized alert message
* @return this
*/
public PayloadBuilder localizedArguments(final String... arguments) {
return localizedArguments(Arrays.asList(arguments));
}
/**
* Sets the launch image file for the push notification
*
* @param launchImage the filename of the image file in the
* application bundle.
* @return this
*/
public PayloadBuilder launchImage(final String launchImage) {
customAlert.put("launch-image", launchImage);
return this;
}
/**
* Sets any application-specific custom fields. The values
* are presented to the application and the iPhone doesn't
* display them automatically.
*
* This can be used to pass specific values (urls, ids, etc) to
* the application in addition to the notification message
* itself.
*
* @param key the custom field name
* @param value the custom field value
* @return this
*/
public PayloadBuilder customField(final String key, final Object value) {
root.put(key, value);
return this;
}
public PayloadBuilder mdm(final String s) {
return customField("mdm", s);
}
/**
* Set any application-specific custom fields. These values
* are presented to the application and the iPhone doesn't
* display them automatically.
*
* This method *adds* the custom fields in the map to the
* payload, and subsequent calls add but doesn't reset the
* custom fields.
*
* @param values the custom map
* @return this
*/
public PayloadBuilder customFields(final Map<String, ?> values) {
root.putAll(values);
return this;
}
/**
* Returns the length of payload bytes once marshaled to bytes
*
* @return the length of the payload
*/
public int length() {
return copy().buildBytes().length;
}
/**
* Returns true if the payload built so far is larger than
* the size permitted by Apple (which is 2048 bytes).
*
* @return true if the result payload is too long
*/
public boolean isTooLong() {
return length() > Utilities.MAX_PAYLOAD_LENGTH;
}
/**
* Shrinks the alert message body so that the resulting payload
* message fits within the passed expected payload length.
*
* This method performs best-effort approach, and its behavior
* is unspecified when handling alerts where the payload
* without body is already longer than the permitted size, or
* if the break occurs within word.
*
* @param payloadLength the expected max size of the payload
* @return this
*/
public PayloadBuilder resizeAlertBody(final int payloadLength) {
return resizeAlertBody(payloadLength, "");
}
/**
* Shrinks the alert message body so that the resulting payload
* message fits within the passed expected payload length.
*
* This method performs best-effort approach, and its behavior
* is unspecified when handling alerts where the payload
* without body is already longer than the permitted size, or
* if the break occurs within word.
*
* @param payloadLength the expected max size of the payload
* @param postfix for the truncated body, e.g. "..."
* @return this
*/
public PayloadBuilder resizeAlertBody(final int payloadLength, final String postfix) {
int currLength = length();
if (currLength <= payloadLength) {
return this;
}
// now we are sure that truncation is required
String body = (String)customAlert.get("body");
final int acceptableSize = Utilities.toUTF8Bytes(body).length
- (currLength - payloadLength
+ Utilities.toUTF8Bytes(postfix).length);
body = Utilities.truncateWhenUTF8(body, acceptableSize) + postfix;
// set it back
customAlert.put("body", body);
// calculate the length again
currLength = length();
if(currLength > payloadLength) {
// string is still too long, just remove the body as the body is
// anyway not the cause OR the postfix might be too long
customAlert.remove("body");
}
return this;
}
/**
* Shrinks the alert message body so that the resulting payload
* message fits within require Apple specification (2048 bytes).
*
* This method performs best-effort approach, and its behavior
* is unspecified when handling alerts where the payload
* without body is already longer than the permitted size, or
* if the break occurs within word.
*
* @return this
*/
public PayloadBuilder shrinkBody() {
return shrinkBody("");
}
/**
* Shrinks the alert message body so that the resulting payload
* message fits within require Apple specification (2048 bytes).
*
* This method performs best-effort approach, and its behavior
* is unspecified when handling alerts where the payload
* without body is already longer than the permitted size, or
* if the break occurs within word.
*
* @param postfix for the truncated body, e.g. "..."
*
* @return this
*/
public PayloadBuilder shrinkBody(final String postfix) {
return resizeAlertBody(Utilities.MAX_PAYLOAD_LENGTH, postfix);
}
/**
* Returns the JSON String representation of the payload
* according to Apple APNS specification
*
* @return the String representation as expected by Apple
*/
public String build() {
if (!root.containsKey("mdm")) {
insertCustomAlert();
root.put("aps", aps);
}
try {
return mapper.writeValueAsString(root);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private void insertCustomAlert() {
switch (customAlert.size()) {
case 0:
aps.remove("alert");
break;
case 1:
if (customAlert.containsKey("body")) {
aps.put("alert", customAlert.get("body"));
break;
}
// else follow through
//$FALL-THROUGH$
default:
aps.put("alert", customAlert);
}
}
/**
* Returns the bytes representation of the payload according to
* Apple APNS specification
*
* @return the bytes as expected by Apple
*/
public byte[] buildBytes() {
return Utilities.toUTF8Bytes(build());
}
@Override
public String toString() {
return build();
}
private PayloadBuilder(final Map<String, Object> root,
final Map<String, Object> aps,
final Map<String, Object> customAlert) {
this.root = new HashMap<String, Object>(root);
this.aps = new HashMap<String, Object>(aps);
this.customAlert = new HashMap<String, Object>(customAlert);
}
/**
* Returns a copy of this builder
*
* @return a copy of this builder
*/
public PayloadBuilder copy() {
return new PayloadBuilder(root, aps, customAlert);
}
/**
* @return a new instance of Payload Builder
*/
public static PayloadBuilder newPayload() {
return new PayloadBuilder();
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import com.notnoop.apns.internal.ReconnectPolicies;
/**
* Represents the reconnection policy for the library.
*
* Each object should be used exclusively for one
* {@code ApnsService} only.
*/
public interface ReconnectPolicy {
/**
* Returns {@code true} if the library should initiate a new
* connection for sending the message.
*
* The library calls this method at every message push.
*
* @return true if the library should be reconnected
*/
public boolean shouldReconnect();
/**
* Callback method to be called whenever the library
* makes a new connection
*/
public void reconnected();
/**
* Returns a deep copy of this reconnection policy, if needed.
*
* Subclasses may return this instance if the object is immutable.
*/
public ReconnectPolicy copy();
/**
* Types of the library provided reconnection policies.
*
* This should capture most of the commonly used cases.
*/
public enum Provided {
/**
* Only reconnect if absolutely needed, e.g. when the connection is dropped.
* <p>
* Apple recommends using a persistent connection. This improves the latency of sending push notification messages.
* <p>
* The down-side is that once the connection is closed ungracefully (e.g. because Apple server drops it), the library wouldn't
* detect such failure and not warn against the messages sent after the drop before the detection.
*/
NEVER {
@Override
public ReconnectPolicy newObject() {
return new ReconnectPolicies.Never();
}
},
/**
* Makes a new connection if the current connection has lasted for more than half an hour.
* <p>
* This is the recommended mode.
* <p>
* This is the sweat-spot in my experiments between dropped connections while minimizing latency.
*/
EVERY_HALF_HOUR {
@Override
public ReconnectPolicy newObject() {
return new ReconnectPolicies.EveryHalfHour();
}
},
/**
* Makes a new connection for every message being sent.
*
* This option ensures that each message is actually
* delivered to Apple.
*
* If you send <strong>a lot</strong> of messages though,
* Apple may consider your requests to be a DoS attack.
*/
EVERY_NOTIFICATION {
@Override
public ReconnectPolicy newObject() {
return new ReconnectPolicies.Always();
}
};
abstract ReconnectPolicy newObject();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
import java.util.Arrays;
import com.notnoop.apns.internal.Utilities;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
/**
* Represents an APNS notification to be sent to Apple service. This is for legacy use only
* and should not be used in new development.
* https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/LegacyFormat.html
*
* This SimpleApnsNotification also only has limited error handling (by the APNS closing the connection
* when a bad message was received) This prevents us from location the malformed notification.
*
* As push messages sent after a malformed notification are discarded by APNS messages will get lost
* and not be delivered with the SimpleApnsNotification.
*
* @deprecated use EnhancedApnsNotification instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
public class SimpleApnsNotification implements ApnsNotification {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleApnsNotification.class);
private final static byte COMMAND = 0;
private final byte[] deviceToken;
private final byte[] payload;
/**
* Constructs an instance of {@code ApnsNotification}.
*
* The message encodes the payload with a {@code UTF-8} encoding.
*
* @param dtoken The Hex of the device token of the destination phone
* @param payload The payload message to be sent
*/
public SimpleApnsNotification(String dtoken, String payload) {
this.deviceToken = Utilities.decodeHex(dtoken);
this.payload = Utilities.toUTF8Bytes(payload);
}
/**
* Constructs an instance of {@code ApnsNotification}.
*
* @param dtoken The binary representation of the destination device token
* @param payload The binary representation of the payload to be sent
*/
public SimpleApnsNotification(byte[] dtoken, byte[] payload) {
this.deviceToken = Utilities.copyOf(dtoken);
this.payload = Utilities.copyOf(payload);
}
/**
* Returns the binary representation of the device token.
*
*/
public byte[] getDeviceToken() {
return Utilities.copyOf(deviceToken);
}
/**
* Returns the binary representation of the payload.
*
*/
public byte[] getPayload() {
return Utilities.copyOf(payload);
}
private byte[] marshall;
/**
* Returns the binary representation of the message as expected by the
* APNS server.
*
* The returned array can be used to sent directly to the APNS server
* (on the wire/socket) without any modification.
*/
public byte[] marshall() {
if (marshall == null)
marshall = Utilities.marshall(COMMAND, deviceToken, payload);
return marshall.clone();
}
/**
* Returns the length of the message in bytes as it is encoded on the wire.
*
* Apple require the message to be of length 255 bytes or less.
*
* @return length of encoded message in bytes
*/
public int length() {
int length = 1 + 2 + deviceToken.length + 2 + payload.length;
final int marshalledLength = marshall().length;
assert marshalledLength == length;
return length;
}
@Override
public int hashCode() {
return 21
+ 31 * Arrays.hashCode(deviceToken)
+ 31 * Arrays.hashCode(payload);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SimpleApnsNotification))
return false;
SimpleApnsNotification o = (SimpleApnsNotification)obj;
return Arrays.equals(this.deviceToken, o.deviceToken)
&& Arrays.equals(this.payload, o.payload);
}
public int getIdentifier() {
return -1;
}
public int getExpiry() {
return -1;
}
@Override
@SuppressFBWarnings("DE_MIGHT_IGNORE")
public String toString() {
String payloadString;
try {
payloadString = new String(payload, "UTF-8");
} catch (UnsupportedEncodingException ex) {
LOGGER.debug("UTF-8 charset not found on the JRE", ex);
payloadString = "???";
}
return "Message(Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")";
}
@Override
public String getDeviceId() {
return null;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns;
/**
* A delegate that also gets notified just before a notification is being delivered to the
* Apple Server.
*/
public interface StartSendingApnsDelegate extends ApnsDelegate {
/**
* Called when message is about to be sent to the Apple servers.
*
* @param message the notification that is about to be sent
* @param resent whether the notification is being resent after an error
*/
public void startSending(ApnsNotification message, boolean resent);
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.apns.ApnsService;
import com.notnoop.apns.EnhancedApnsNotification;
import com.notnoop.exceptions.NetworkIOException;
abstract class AbstractApnsService implements ApnsService {
private ApnsFeedbackConnection feedback;
private AtomicInteger c = new AtomicInteger();
public AbstractApnsService(ApnsFeedbackConnection feedback) {
this.feedback = feedback;
}
public EnhancedApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException {
EnhancedApnsNotification notification =
new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload);
notification.setDeviceId(deviceId);
push(notification);
return notification;
}
public EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException {
EnhancedApnsNotification notification =
new EnhancedApnsNotification(c.incrementAndGet(), (int)(expiry.getTime() / 1000), deviceToken, payload);
notification.setDeviceId(deviceId);
push(notification);
return notification;
}
public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException {
EnhancedApnsNotification notification =
new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload);
notification.setDeviceId(deviceId);
push(notification);
return notification;
}
public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException {
EnhancedApnsNotification notification =
new EnhancedApnsNotification(c.incrementAndGet(), expiry, deviceToken, payload);
notification.setDeviceId(deviceId);
push(notification);
return notification;
}
public abstract void push(ApnsNotification message) throws NetworkIOException;
public Map<String, Date> getInactiveDevices() throws NetworkIOException {
return feedback.getInactiveDevices();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.io.Closeable;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.exceptions.NetworkIOException;
public interface ApnsConnection extends Closeable {
//Default number of notifications to keep for error purposes
public static final int DEFAULT_CACHE_LENGTH = 100;
void sendMessage(ApnsNotification m) throws NetworkIOException;
void testConnection() throws NetworkIOException;
ApnsConnection copy();
void setCacheLength(int cacheLength);
int getCacheLength();
}

View File

@@ -0,0 +1,412 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.SocketFactory;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import com.notnoop.apns.ApnsDelegate;
import com.notnoop.apns.StartSendingApnsDelegate;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.apns.DeliveryError;
import com.notnoop.apns.EnhancedApnsNotification;
import com.notnoop.apns.ReconnectPolicy;
import com.notnoop.exceptions.ApnsDeliveryErrorException;
import com.notnoop.exceptions.NetworkIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApnsConnectionImpl implements ApnsConnection {
private static final Logger logger = LoggerFactory.getLogger(ApnsConnectionImpl.class);
private final SocketFactory factory;
private final String host;
private final int port;
private final int readTimeout;
private final int connectTimeout;
private final Proxy proxy;
private final String proxyUsername;
private final String proxyPassword;
private final ReconnectPolicy reconnectPolicy;
private final ApnsDelegate delegate;
private int cacheLength;
private final boolean errorDetection;
private final ThreadFactory threadFactory;
private final boolean autoAdjustCacheLength;
private final ConcurrentLinkedQueue<ApnsNotification> cachedNotifications, notificationsBuffer;
private Socket socket;
private final AtomicInteger threadId = new AtomicInteger(0);
public ApnsConnectionImpl(SocketFactory factory, String host, int port) {
this(factory, host, port, new ReconnectPolicies.Never(), ApnsDelegate.EMPTY);
}
private ApnsConnectionImpl(SocketFactory factory, String host, int port, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) {
this(factory, host, port, null, null, null, reconnectPolicy, delegate);
}
private ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword,
ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) {
this(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy, delegate, false, null,
ApnsConnection.DEFAULT_CACHE_LENGTH, true, 0, 0);
}
public ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword,
ReconnectPolicy reconnectPolicy, ApnsDelegate delegate, boolean errorDetection, ThreadFactory tf, int cacheLength,
boolean autoAdjustCacheLength, int readTimeout, int connectTimeout) {
this.factory = factory;
this.host = host;
this.port = port;
this.reconnectPolicy = reconnectPolicy;
this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate;
this.proxy = proxy;
this.errorDetection = errorDetection;
this.threadFactory = tf == null ? defaultThreadFactory() : tf;
this.cacheLength = cacheLength;
this.autoAdjustCacheLength = autoAdjustCacheLength;
this.readTimeout = readTimeout;
this.connectTimeout = connectTimeout;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
cachedNotifications = new ConcurrentLinkedQueue<ApnsNotification>();
notificationsBuffer = new ConcurrentLinkedQueue<ApnsNotification>();
}
private ThreadFactory defaultThreadFactory() {
return new ThreadFactory() {
ThreadFactory wrapped = Executors.defaultThreadFactory();
@Override
public Thread newThread( Runnable r )
{
Thread result = wrapped.newThread(r);
result.setName("MonitoringThread-"+threadId.incrementAndGet());
result.setDaemon(true);
return result;
}
};
}
public synchronized void close() {
Utilities.close(socket);
}
private void monitorSocket(final Socket socketToMonitor) {
logger.debug("Launching Monitoring Thread for socket {}", socketToMonitor);
Thread t = threadFactory.newThread(new Runnable() {
final static int EXPECTED_SIZE = 6;
@SuppressWarnings("InfiniteLoopStatement")
@Override
public void run() {
logger.debug("Started monitoring thread");
try {
InputStream in;
try {
in = socketToMonitor.getInputStream();
} catch (IOException ioe) {
logger.warn("The value of socket is null", ioe);
in = null;
}
byte[] bytes = new byte[EXPECTED_SIZE];
while (in != null && readPacket(in, bytes)) {
logger.debug("Error-response packet {}", Utilities.encodeHex(bytes));
// Quickly close socket, so we won't ever try to send push notifications
// using the defective socket.
Utilities.close(socketToMonitor);
int command = bytes[0] & 0xFF;
if (command != 8) {
throw new IOException("Unexpected command byte " + command);
}
int statusCode = bytes[1] & 0xFF;
DeliveryError e = DeliveryError.ofCode(statusCode);
int id = Utilities.parseBytes(bytes[2], bytes[3], bytes[4], bytes[5]);
logger.debug("Closed connection cause={}; id={}", e, id);
delegate.connectionClosed(e, id);
Queue<ApnsNotification> tempCache = new LinkedList<ApnsNotification>();
ApnsNotification notification = null;
boolean foundNotification = false;
while (!cachedNotifications.isEmpty()) {
notification = cachedNotifications.poll();
logger.debug("Candidate for removal, message id {}", notification.getIdentifier());
if (notification.getIdentifier() == id) {
logger.debug("Bad message found {}", notification.getIdentifier());
foundNotification = true;
break;
}
tempCache.add(notification);
}
if (foundNotification) {
logger.debug("delegate.messageSendFailed, message id {}", notification.getIdentifier());
delegate.messageSendFailed(notification, new ApnsDeliveryErrorException(e));
} else {
cachedNotifications.addAll(tempCache);
int resendSize = tempCache.size();
logger.warn("Received error for message that wasn't in the cache...");
if (autoAdjustCacheLength) {
cacheLength = cacheLength + (resendSize / 2);
delegate.cacheLengthExceeded(cacheLength);
}
logger.debug("delegate.messageSendFailed, unknown id");
delegate.messageSendFailed(null, new ApnsDeliveryErrorException(e));
}
int resendSize = 0;
while (!cachedNotifications.isEmpty()) {
resendSize++;
final ApnsNotification resendNotification = cachedNotifications.poll();
logger.debug("Queuing for resend {}", resendNotification.getIdentifier());
notificationsBuffer.add(resendNotification);
}
logger.debug("resending {} notifications", resendSize);
delegate.notificationsResent(resendSize);
}
logger.debug("Monitoring input stream closed by EOF");
} catch (IOException e) {
// An exception when reading the error code is non-critical, it will cause another retry
// sending the message. Other than providing a more stable network connection to the APNS
// server we can't do much about it - so let's not spam the application's error log.
logger.info("Exception while waiting for error code", e);
delegate.connectionClosed(DeliveryError.UNKNOWN, -1);
} finally {
Utilities.close(socketToMonitor);
drainBuffer();
}
}
/**
* Read a packet like in.readFully(bytes) does - but do not throw an exception and return false if nothing
* could be read at all.
* @param in the input stream
* @param bytes the array to be filled with data
* @return true if a packet as been read, false if the stream was at EOF right at the beginning.
* @throws IOException When a problem occurs, especially EOFException when there's an EOF in the middle of the packet.
*/
private boolean readPacket(final InputStream in, final byte[] bytes) throws IOException {
final int len = bytes.length;
int n = 0;
while (n < len) {
try {
int count = in.read(bytes, n, len - n);
if (count < 0) {
throw new EOFException("EOF after reading "+n+" bytes of new packet.");
}
n += count;
} catch (IOException ioe) {
if (n == 0)
return false;
throw new IOException("Error after reading "+n+" bytes of packet", ioe);
}
}
return true;
}
});
t.start();
}
private synchronized Socket getOrCreateSocket(boolean resend) throws NetworkIOException {
if (reconnectPolicy.shouldReconnect()) {
logger.debug("Reconnecting due to reconnectPolicy dictating it");
Utilities.close(socket);
socket = null;
}
if (socket == null || socket.isClosed()) {
try {
if (proxy == null) {
socket = factory.createSocket(host, port);
logger.debug("Connected new socket {}", socket);
} else if (proxy.type() == Proxy.Type.HTTP) {
TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder();
socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port);
logger.debug("Connected new socket through http tunnel {}", socket);
} else {
boolean success = false;
Socket proxySocket = null;
try {
proxySocket = new Socket(proxy);
proxySocket.connect(new InetSocketAddress(host, port), connectTimeout);
socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false);
success = true;
} finally {
if (!success) {
Utilities.close(proxySocket);
}
}
logger.debug("Connected new socket through socks tunnel {}", socket);
}
socket.setSoTimeout(readTimeout);
socket.setKeepAlive(true);
if (errorDetection) {
monitorSocket(socket);
}
reconnectPolicy.reconnected();
logger.debug("Made a new connection to APNS");
} catch (IOException e) {
logger.error("Couldn't connect to APNS server", e);
// indicate to clients whether this is a resend or initial send
throw new NetworkIOException(e, resend);
}
}
return socket;
}
int DELAY_IN_MS = 1000;
private static final int RETRIES = 3;
public synchronized void sendMessage(ApnsNotification m) throws NetworkIOException {
sendMessage(m, false);
drainBuffer();
}
private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) throws NetworkIOException {
logger.debug("sendMessage {} fromBuffer: {}", m, fromBuffer);
if (delegate instanceof StartSendingApnsDelegate) {
((StartSendingApnsDelegate) delegate).startSending(m, fromBuffer);
}
int attempts = 0;
while (true) {
try {
attempts++;
Socket socket = getOrCreateSocket(fromBuffer);
socket.getOutputStream().write(m.marshall());
socket.getOutputStream().flush();
cacheNotification(m);
delegate.messageSent(m, fromBuffer);
//logger.debug("Message \"{}\" sent", m);
attempts = 0;
break;
} catch (SSLHandshakeException e) {
// No use retrying this, it's dead Jim
throw new NetworkIOException(e);
} catch (IOException e) {
Utilities.close(socket);
if (attempts >= RETRIES) {
logger.error("Couldn't send message after " + RETRIES + " retries." + m, e);
delegate.messageSendFailed(m, e);
Utilities.wrapAndThrowAsRuntimeException(e);
}
// The first failure might be due to closed connection (which in turn might be caused by
// a message containing a bad token), so don't delay for the first retry.
//
// Additionally we don't want to spam the log file in this case, only after the second retry
// which uses the delay.
if (attempts != 1) {
logger.info("Failed to send message " + m + "... trying again after delay", e);
Utilities.sleep(DELAY_IN_MS);
}
}
}
}
private synchronized void drainBuffer() {
logger.debug("draining buffer");
while (!notificationsBuffer.isEmpty()) {
final ApnsNotification notification = notificationsBuffer.poll();
try {
sendMessage(notification, true);
}
catch (NetworkIOException ex) {
// at this point we are retrying the submission of messages but failing to connect to APNS, therefore
// notify the client of this
delegate.messageSendFailed(notification, ex);
}
}
}
private void cacheNotification(ApnsNotification notification) {
cachedNotifications.add(notification);
while (cachedNotifications.size() > cacheLength) {
cachedNotifications.poll();
logger.debug("Removing notification from cache " + notification);
}
}
public ApnsConnectionImpl copy() {
return new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate,
errorDetection, threadFactory, cacheLength, autoAdjustCacheLength, readTimeout, connectTimeout);
}
public void testConnection() throws NetworkIOException {
ApnsConnectionImpl testConnection = null;
try {
testConnection =
new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate);
final ApnsNotification notification = new EnhancedApnsNotification(0, 0, new byte[]{0}, new byte[]{0});
testConnection.sendMessage(notification);
} finally {
if (testConnection != null) {
testConnection.close();
}
}
}
public void setCacheLength(int cacheLength) {
this.cacheLength = cacheLength;
}
public int getCacheLength() {
return cacheLength;
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.util.Date;
import java.util.Map;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.notnoop.exceptions.NetworkIOException;
public class ApnsFeedbackConnection {
private static final Logger logger = LoggerFactory.getLogger(ApnsFeedbackConnection.class);
private final SocketFactory factory;
private final String host;
private final int port;
private final Proxy proxy;
private final int readTimeout;
private final int connectTimeout;
private final String proxyUsername;
private final String proxyPassword;
public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port) {
this(factory, host, port, null, 0, 0, null, null);
}
public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port,
final Proxy proxy, int readTimeout, int connectTimeout, final String proxyUsername, final String proxyPassword) {
this.factory = factory;
this.host = host;
this.port = port;
this.proxy = proxy;
this.readTimeout = readTimeout;
this.connectTimeout = connectTimeout;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
}
int DELAY_IN_MS = 1000;
private static final int RETRIES = 3;
public Map<String, Date> getInactiveDevices() throws NetworkIOException {
int attempts = 0;
while (true) {
try {
attempts++;
final Map<String, Date> result = getInactiveDevicesImpl();
attempts = 0;
return result;
} catch (final Exception e) {
logger.warn("Failed to retrieve invalid devices", e);
if (attempts >= RETRIES) {
logger.error("Couldn't get feedback connection", e);
Utilities.wrapAndThrowAsRuntimeException(e);
}
Utilities.sleep(DELAY_IN_MS);
}
}
}
public Map<String, Date> getInactiveDevicesImpl() throws IOException {
Socket proxySocket = null;
Socket socket = null;
try {
if (proxy == null) {
socket = factory.createSocket(host, port);
} else if (proxy.type() == Proxy.Type.HTTP) {
TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder();
socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port);
} else {
proxySocket = new Socket(proxy);
proxySocket.connect(new InetSocketAddress(host, port), connectTimeout);
socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false);
}
socket.setSoTimeout(readTimeout);
socket.setKeepAlive(true);
final InputStream stream = socket.getInputStream();
return Utilities.parseFeedbackStream(stream);
} finally {
Utilities.close(socket);
Utilities.close(proxySocket);
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.util.concurrent.*;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.exceptions.NetworkIOException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApnsPooledConnection implements ApnsConnection {
private static final Logger logger = LoggerFactory.getLogger(ApnsPooledConnection.class);
private final ApnsConnection prototype;
private final int max;
private final ExecutorService executors;
private final ConcurrentLinkedQueue<ApnsConnection> prototypes;
public ApnsPooledConnection(ApnsConnection prototype, int max) {
this(prototype, max, Executors.newFixedThreadPool(max));
}
public ApnsPooledConnection(ApnsConnection prototype, int max, ExecutorService executors) {
this.prototype = prototype;
this.max = max;
this.executors = executors;
this.prototypes = new ConcurrentLinkedQueue<ApnsConnection>();
}
private final ThreadLocal<ApnsConnection> uniquePrototype =
new ThreadLocal<ApnsConnection>() {
protected ApnsConnection initialValue() {
ApnsConnection newCopy = prototype.copy();
prototypes.add(newCopy);
return newCopy;
}
};
public void sendMessage(final ApnsNotification m) throws NetworkIOException {
Future<Void> future = executors.submit(new Callable<Void>() {
public Void call() throws Exception {
uniquePrototype.get().sendMessage(m);
return null;
}
});
try {
future.get();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (ExecutionException ee) {
if (ee.getCause() instanceof NetworkIOException) {
throw (NetworkIOException) ee.getCause();
}
}
}
public ApnsConnection copy() {
// TODO: Should copy executor properly.... What should copy do
// really?!
return new ApnsPooledConnection(prototype, max);
}
public void close() {
executors.shutdown();
try {
executors.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.warn("pool termination interrupted", e);
}
for (ApnsConnection conn : prototypes) {
Utilities.close(conn);
}
Utilities.close(prototype);
}
public void testConnection() {
prototype.testConnection();
}
public synchronized void setCacheLength(int cacheLength) {
for (ApnsConnection conn : prototypes) {
conn.setCacheLength(cacheLength);
}
}
@SuppressFBWarnings(value = "UG_SYNC_SET_UNSYNC_GET", justification = "prototypes is a MT-safe container")
public int getCacheLength() {
return prototypes.peek().getCacheLength();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.exceptions.NetworkIOException;
public class ApnsServiceImpl extends AbstractApnsService {
private ApnsConnection connection;
public ApnsServiceImpl(ApnsConnection connection, ApnsFeedbackConnection feedback) {
super(feedback);
this.connection = connection;
}
@Override
public void push(ApnsNotification msg) throws NetworkIOException {
connection.sendMessage(msg);
}
public void start() {
}
public void stop() {
Utilities.close(connection);
}
public void testConnection() {
connection.testConnection();
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import static java.util.concurrent.Executors.defaultThreadFactory;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.exceptions.NetworkIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BatchApnsService extends AbstractApnsService {
private static final Logger logger = LoggerFactory.getLogger(BatchApnsService.class);
/**
* How many seconds to wait for more messages before batch is send.
* Each message reset the wait time
*
* @see #maxBatchWaitTimeInSec
*/
private int batchWaitTimeInSec = 5;
/**
* How many seconds can be batch delayed before execution.
* This time is not exact amount after which the batch will run its roughly the time
*/
private int maxBatchWaitTimeInSec = 10;
private long firstMessageArrivedTime;
private ApnsConnection prototype;
private Queue<ApnsNotification> batch = new ConcurrentLinkedQueue<ApnsNotification>();
private ScheduledExecutorService scheduleService;
private ScheduledFuture<?> taskFuture;
private Runnable batchRunner = new SendMessagesBatch();
public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ThreadFactory tf) {
this(prototype, feedback, batchWaitTimeInSec, maxBachWaitTimeInSec,
new ScheduledThreadPoolExecutor(1,
tf != null ? tf : defaultThreadFactory()));
}
public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ScheduledExecutorService executor) {
super(feedback);
this.prototype = prototype;
this.batchWaitTimeInSec = batchWaitTimeInSec;
this.maxBatchWaitTimeInSec = maxBachWaitTimeInSec;
this.scheduleService = executor != null ? executor : new ScheduledThreadPoolExecutor(1, defaultThreadFactory());
}
public void start() {
// no code
}
public void stop() {
Utilities.close(prototype);
if (taskFuture != null) {
taskFuture.cancel(true);
}
scheduleService.shutdownNow();
}
public void testConnection() throws NetworkIOException {
prototype.testConnection();
}
@Override
public void push(ApnsNotification message) throws NetworkIOException {
if (batch.isEmpty()) {
firstMessageArrivedTime = System.nanoTime();
}
long sinceFirstMessageSec = (System.nanoTime() - firstMessageArrivedTime) / 1000 / 1000 / 1000;
if (taskFuture != null && sinceFirstMessageSec < maxBatchWaitTimeInSec) {
taskFuture.cancel(false);
}
batch.add(message);
if (taskFuture == null || taskFuture.isDone()) {
taskFuture = scheduleService.schedule(batchRunner, batchWaitTimeInSec, TimeUnit.SECONDS);
}
}
class SendMessagesBatch implements Runnable {
public void run() {
ApnsConnection newConnection = prototype.copy();
try {
ApnsNotification msg;
while ((msg = batch.poll()) != null) {
try {
newConnection.sendMessage(msg);
} catch (NetworkIOException e) {
logger.warn("Network exception sending message msg "+ msg.getIdentifier(), e);
}
}
} finally {
Utilities.close(newConnection);
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
public class QueuedApnsService extends AbstractApnsService {
private static final Logger logger = LoggerFactory.getLogger(QueuedApnsService.class);
private ApnsService service;
private BlockingQueue<ApnsNotification> queue;
private AtomicBoolean started = new AtomicBoolean(false);
public QueuedApnsService(ApnsService service) {
this(service, null);
}
public QueuedApnsService(ApnsService service, final ThreadFactory tf) {
super(null);
this.service = service;
this.queue = new LinkedBlockingQueue<ApnsNotification>();
this.threadFactory = tf == null ? Executors.defaultThreadFactory() : tf;
this.thread = null;
}
@Override
public void push(ApnsNotification msg) {
if (!started.get()) {
throw new IllegalStateException("service hasn't be started or was closed");
}
queue.add(msg);
}
private final ThreadFactory threadFactory;
private Thread thread;
private volatile boolean shouldContinue;
public void start() {
if (started.getAndSet(true)) {
// I prefer if we throw a runtime IllegalStateException here,
// but I want to maintain semantic backward compatibility.
// So it is returning immediately here
return;
}
service.start();
shouldContinue = true;
thread = threadFactory.newThread(new Runnable() {
public void run() {
while (shouldContinue) {
try {
ApnsNotification msg = queue.take();
service.push(msg);
} catch (InterruptedException e) {
// ignore
} catch (NetworkIOException e) {
// ignore: failed connect...
} catch (Exception e) {
// weird if we reached here - something wrong is happening, but we shouldn't stop the service anyway!
logger.warn("Unexpected message caught... Shouldn't be here", e);
}
}
}
});
thread.start();
}
public void stop() {
started.set(false);
shouldContinue = false;
thread.interrupt();
service.stop();
}
@Override
public Map<String, Date> getInactiveDevices() throws NetworkIOException {
return service.getInactiveDevices();
}
public void testConnection() throws NetworkIOException {
service.testConnection();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import com.notnoop.apns.ReconnectPolicy;
public final class ReconnectPolicies {
public static class Never implements ReconnectPolicy {
public boolean shouldReconnect() { return false; }
public void reconnected() { }
public Never copy() { return this; }
}
public static class Always implements ReconnectPolicy {
public boolean shouldReconnect() { return true; }
public void reconnected() { }
public Always copy() { return this; }
}
public static class EveryHalfHour implements ReconnectPolicy {
private static final long PERIOD = 30 * 60 * 1000;
private long lastRunning = System.currentTimeMillis();
public boolean shouldReconnect() {
return System.currentTimeMillis() - lastRunning > PERIOD;
}
public void reconnected() {
lastRunning = System.currentTimeMillis();
}
public EveryHalfHour copy() {
return new EveryHalfHour();
}
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import com.notnoop.exceptions.InvalidSSLConfig;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
public class SSLContextBuilder {
private String algorithm = "sunx509";
private KeyManagerFactory keyManagerFactory;
private TrustManager[] trustManagers;
public SSLContextBuilder withAlgorithm(String algorithm) {
this.algorithm = algorithm;
return this;
}
public SSLContextBuilder withDefaultTrustKeyStore() throws InvalidSSLConfig {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
trustManagerFactory.init((KeyStore)null);
trustManagers = trustManagerFactory.getTrustManagers();
return this;
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withTrustKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig {
try {
final KeyStore ks = KeyStore.getInstance(keyStoreType);
ks.load(keyStoreStream, keyStorePassword.toCharArray());
return withTrustKeyStore(ks, keyStorePassword);
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
} catch (IOException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withTrustKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
trustManagerFactory.init(keyStore);
trustManagers = trustManagerFactory.getTrustManagers();
return this;
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withTrustManager(TrustManager trustManager) {
trustManagers = new TrustManager[] { trustManager };
return this;
}
public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig {
try {
final KeyStore ks = KeyStore.getInstance(keyStoreType);
ks.load(keyStoreStream, keyStorePassword.toCharArray());
return withCertificateKeyStore(ks, keyStorePassword);
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
} catch (IOException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType, String keyAlias) throws InvalidSSLConfig {
try {
final KeyStore ks = KeyStore.getInstance(keyStoreType);
ks.load(keyStoreStream, keyStorePassword.toCharArray());
return withCertificateKeyStore(ks, keyStorePassword, keyAlias);
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
} catch (IOException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig {
try {
keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
return this;
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
}
}
public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword, String keyAlias) throws InvalidSSLConfig {
try {
if (!keyStore.containsAlias(keyAlias)) {
throw new InvalidSSLConfig("No key with alias " + keyAlias);
}
KeyStore singleKeyKeyStore = getKeyStoreWithSingleKey(keyStore, keyStorePassword, keyAlias);
return withCertificateKeyStore(singleKeyKeyStore, keyStorePassword);
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
} catch (IOException e) {
throw new InvalidSSLConfig(e);
}
}
/*
* Workaround for keystores containing multiple keys. Java will take the first key that matches
* and this way we can still offer configuration for a keystore with multiple keys and a selection
* based on alias. Also much easier than making a subclass of a KeyManagerFactory
*/
private KeyStore getKeyStoreWithSingleKey(KeyStore keyStore, String keyStorePassword, String keyAlias)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
KeyStore singleKeyKeyStore = KeyStore.getInstance(keyStore.getType(), keyStore.getProvider());
final char[] password = keyStorePassword.toCharArray();
singleKeyKeyStore.load(null, password);
Key key = keyStore.getKey(keyAlias, password);
Certificate[] chain = keyStore.getCertificateChain(keyAlias);
singleKeyKeyStore.setKeyEntry(keyAlias, key, password, chain);
return singleKeyKeyStore;
}
public SSLContext build() throws InvalidSSLConfig {
if (keyManagerFactory == null) {
throw new InvalidSSLConfig("Missing KeyManagerFactory");
}
if (trustManagers == null) {
throw new InvalidSSLConfig("Missing TrustManagers");
}
try {
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);
return sslContext;
} catch (GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.Socket;
import javax.net.ssl.SSLSocketFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.httpclient.ConnectMethod;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.ProxyClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Establishes a TLS connection using an HTTP proxy. See <a
* href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817 5.2</a>. This class does
* not support proxies requiring a "Proxy-Authorization" header.
*/
public final class TlsTunnelBuilder {
private static final Logger logger = LoggerFactory.getLogger(TlsTunnelBuilder.class);
public Socket build(SSLSocketFactory factory, Proxy proxy, String proxyUsername, String proxyPassword, String host, int port)
throws IOException {
boolean success = false;
Socket proxySocket = null;
try {
logger.debug("Attempting to use proxy : " + proxy.toString());
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
proxySocket = makeTunnel(host, port, proxyUsername, proxyPassword, proxyAddress);
// Handshake with the origin server.
if(proxySocket == null) {
throw new ProtocolException("Unable to create tunnel through proxy server.");
}
Socket socket = factory.createSocket(proxySocket, host, port, true /* auto close */);
success = true;
return socket;
} finally {
if (!success) {
Utilities.close(proxySocket);
}
}
}
@SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE",
justification = "use <CR><LF> as according to RFC, not platform-linefeed")
Socket makeTunnel(String host, int port, String proxyUsername,
String proxyPassword, InetSocketAddress proxyAddress) throws IOException {
if(host == null || port < 0 || host.isEmpty() || proxyAddress == null){
throw new ProtocolException("Incorrect parameters to build tunnel.");
}
logger.debug("Creating socket for Proxy : " + proxyAddress.getAddress() + ":" + proxyAddress.getPort());
Socket socket;
try {
ProxyClient client = new ProxyClient();
client.getParams().setParameter("http.useragent", "java-apns");
client.getHostConfiguration().setHost(host, port);
String proxyHost = proxyAddress.getAddress().toString().substring(0, proxyAddress.getAddress().toString().indexOf("/"));
client.getHostConfiguration().setProxy(proxyHost, proxyAddress.getPort());
ProxyClient.ConnectResponse response = client.connect();
socket = response.getSocket();
if (socket == null) {
ConnectMethod method = response.getConnectMethod();
// Read the proxy's HTTP response.
if(method.getStatusLine().getStatusCode() == 407) {
// Proxy server returned 407. We will now try to connect with auth Header
if(proxyUsername != null && proxyPassword != null) {
socket = AuthenticateProxy(method, client,proxyHost, proxyAddress.getPort(),
proxyUsername, proxyPassword);
} else {
throw new ProtocolException("Socket not created: " + method.getStatusLine());
}
}
}
} catch (Exception e) {
throw new ProtocolException("Error occurred while creating proxy socket : " + e.toString());
}
if (socket != null) {
logger.debug("Socket for proxy created successfully : " + socket.getRemoteSocketAddress().toString());
}
return socket;
}
private Socket AuthenticateProxy(ConnectMethod method, ProxyClient client,
String proxyHost, int proxyPort,
String proxyUsername, String proxyPassword) throws IOException {
if("ntlm".equalsIgnoreCase(method.getProxyAuthState().getAuthScheme().getSchemeName())) {
// If Auth scheme is NTLM, set NT credentials with blank host and domain name
client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort),
new NTCredentials(proxyUsername, proxyPassword,"",""));
} else {
// If Auth scheme is Basic/Digest, set regular Credentials
client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort),
new UsernamePasswordCredentials(proxyUsername, proxyPassword));
}
ProxyClient.ConnectResponse response = client.connect();
Socket socket = response.getSocket();
if (socket == null) {
method = response.getConnectMethod();
throw new ProtocolException("Proxy Authentication failed. Socket not created: "
+ method.getStatusLine());
}
return socket;
}
}

View File

@@ -0,0 +1,296 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.apns.internal;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import com.notnoop.exceptions.InvalidSSLConfig;
import com.notnoop.exceptions.NetworkIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class Utilities {
private static Logger logger = LoggerFactory.getLogger(Utilities.class);
public static final String SANDBOX_GATEWAY_HOST = "gateway.sandbox.push.apple.com";
public static final int SANDBOX_GATEWAY_PORT = 2195;
public static final String SANDBOX_FEEDBACK_HOST = "feedback.sandbox.push.apple.com";
public static final int SANDBOX_FEEDBACK_PORT = 2196;
public static final String PRODUCTION_GATEWAY_HOST = "gateway.push.apple.com";
public static final int PRODUCTION_GATEWAY_PORT = 2195;
public static final String PRODUCTION_FEEDBACK_HOST = "feedback.push.apple.com";
public static final int PRODUCTION_FEEDBACK_PORT = 2196;
public static final int MAX_PAYLOAD_LENGTH = 2048;
private Utilities() { throw new AssertionError("Uninstantiable class"); }
private static final Pattern pattern = Pattern.compile("[ -]");
public static byte[] decodeHex(final String deviceToken) {
final String hex = pattern.matcher(deviceToken).replaceAll("");
final byte[] bts = new byte[hex.length() / 2];
for (int i = 0; i < bts.length; i++) {
bts[i] = (byte) (charVal(hex.charAt(2 * i)) * 16 + charVal(hex.charAt(2 * i + 1)));
}
return bts;
}
private static int charVal(final char a) {
if ('0' <= a && a <= '9') {
return (a - '0');
} else if ('a' <= a && a <= 'f') {
return (a - 'a') + 10;
} else if ('A' <= a && a <= 'F') {
return (a - 'A') + 10;
} else {
throw new RuntimeException("Invalid hex character: " + a);
}
}
private static final char base[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
public static String encodeHex(final byte[] bytes) {
final char[] chars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
final int b = (bytes[i]) & 0xFF;
chars[2 * i] = base[b >>> 4];
chars[2 * i + 1] = base[b & 0xF];
}
return new String(chars);
}
public static byte[] toUTF8Bytes(final String s) {
try {
return s.getBytes("UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static byte[] marshall(final byte command, final byte[] deviceToken, final byte[] payload) {
final ByteArrayOutputStream boas = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(boas);
try {
dos.writeByte(command);
dos.writeShort(deviceToken.length);
dos.write(deviceToken);
dos.writeShort(payload.length);
dos.write(payload);
return boas.toByteArray();
} catch (final IOException e) {
throw new AssertionError();
}
}
public static byte[] marshallEnhanced(final byte command, final int identifier,
final int expiryTime, final byte[] deviceToken, final byte[] payload) {
final ByteArrayOutputStream boas = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(boas);
try {
dos.writeByte(command);
dos.writeInt(identifier);
dos.writeInt(expiryTime);
dos.writeShort(deviceToken.length);
dos.write(deviceToken);
dos.writeShort(payload.length);
dos.write(payload);
return boas.toByteArray();
} catch (final IOException e) {
throw new AssertionError();
}
}
public static Map<byte[], Integer> parseFeedbackStreamRaw(final InputStream in) {
final Map<byte[], Integer> result = new HashMap<byte[], Integer>();
final DataInputStream data = new DataInputStream(in);
while (true) {
try {
final int time = data.readInt();
final int dtLength = data.readUnsignedShort();
final byte[] deviceToken = new byte[dtLength];
data.readFully(deviceToken);
result.put(deviceToken, time);
} catch (final EOFException e) {
break;
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
return result;
}
public static Map<String, Date> parseFeedbackStream(final InputStream in) {
final Map<String, Date> result = new HashMap<String, Date>();
final Map<byte[], Integer> raw = parseFeedbackStreamRaw(in);
for (final Map.Entry<byte[], Integer> entry : raw.entrySet()) {
final byte[] dtArray = entry.getKey();
final int time = entry.getValue(); // in seconds
final Date date = new Date(time * 1000L); // in ms
final String dtString = encodeHex(dtArray);
result.put(dtString, date);
}
return result;
}
public static void close(final Closeable closeable) {
logger.debug("close {}", closeable);
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException e) {
logger.debug("error while closing resource", e);
}
}
public static void close(final Socket closeable) {
logger.debug("close {}", closeable);
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException e) {
logger.debug("error while closing socket", e);
}
}
public static void sleep(final int delay) {
try {
Thread.sleep(delay);
} catch (final InterruptedException e1) {
Thread.currentThread().interrupt();
}
}
public static byte[] copyOf(final byte[] bytes) {
final byte[] copy = new byte[bytes.length];
System.arraycopy(bytes, 0, copy, 0, bytes.length);
return copy;
}
public static byte[] copyOfRange(final byte[] original, final int from, final int to) {
final int newLength = to - from;
if (newLength < 0) {
throw new IllegalArgumentException(from + " > " + to);
}
final byte[] copy = new byte[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
public static void wrapAndThrowAsRuntimeException(final Exception e) throws NetworkIOException {
if (e instanceof IOException) {
throw new NetworkIOException((IOException)e);
} else if (e instanceof NetworkIOException) {
throw (NetworkIOException)e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"PointlessArithmeticExpression", "PointlessBitwiseExpression"})
public static int parseBytes(final int b1, final int b2, final int b3, final int b4) {
return ((b1 << 3 * 8) & 0xFF000000)
| ((b2 << 2 * 8) & 0x00FF0000)
| ((b3 << 1 * 8) & 0x0000FF00)
| ((b4 << 0 * 8) & 0x000000FF);
}
// @see http://stackoverflow.com/questions/119328/how-do-i-truncate-a-java-string-to-fit-in-a-given-number-of-bytes-once-utf-8-enc
public static String truncateWhenUTF8(final String s, final int maxBytes) {
int b = 0;
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
// ranges from http://en.wikipedia.org/wiki/UTF-8
int skip = 0;
int more;
if (c <= 0x007f) {
more = 1;
}
else if (c <= 0x07FF) {
more = 2;
} else if (c <= 0xd7ff) {
more = 3;
} else if (c <= 0xDFFF) {
// surrogate area, consume next char as well
more = 4;
skip = 1;
} else {
more = 3;
}
if (b + more > maxBytes) {
return s.substring(0, i);
}
b += more;
i += skip;
}
return s;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.
*/
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.notnoop.exceptions;
import com.notnoop.apns.DeliveryError;
/**
*
* @author kkirch
*/
public class ApnsDeliveryErrorException extends ApnsException {
private final DeliveryError deliveryError;
public ApnsDeliveryErrorException(DeliveryError error) {
this.deliveryError = error;
}
@Override
public String getMessage() {
return "Failed to deliver notification with error code " + deliveryError.code();
}
public DeliveryError getDeliveryError() {
return deliveryError;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.exceptions;
/**
* Base class for all the exceptions thrown in Apns Library
*/
public abstract class ApnsException extends RuntimeException {
private static final long serialVersionUID = -4756693306121825229L;
public ApnsException() { super(); }
public ApnsException(String message) { super(message); }
public ApnsException(Throwable cause) { super(cause); }
public ApnsException(String m, Throwable c) { super(m, c); }
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.exceptions;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
/**
* Signals that the the provided SSL context settings (e.g.
* keystore path, password, encryption type, etc) are invalid
*
* This Exception can be caused by any of the following:
*
* <ol>
* <li>{@link KeyStoreException}</li>
* <li>{@link NoSuchAlgorithmException}</li>
* <li>{@link CertificateException}</li>
* <li>{@link IOException}</li>
* <li>{@link UnrecoverableKeyException}</li>
* <li>{@link KeyManagementException}</li>
* </ol>
*
*/
public class InvalidSSLConfig extends ApnsException {
private static final long serialVersionUID = -7283168775864517167L;
public InvalidSSLConfig() { super(); }
public InvalidSSLConfig(String message) { super(message); }
public InvalidSSLConfig(Throwable cause) { super(cause); }
public InvalidSSLConfig(String m, Throwable c) { super(m, c); }
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.exceptions;
import java.io.IOException;
/**
* Thrown to indicate that that a network operation has failed:
* (e.g. connectivity problems, domain cannot be found, network
* dropped).
*/
public class NetworkIOException extends ApnsException {
private static final long serialVersionUID = 3353516625486306533L;
private boolean resend;
public NetworkIOException() { super(); }
public NetworkIOException(String message) { super(message); }
public NetworkIOException(IOException cause) { super(cause); }
public NetworkIOException(String m, IOException c) { super(m, c); }
public NetworkIOException(IOException cause, boolean resend) {
super(cause);
this.resend = resend;
}
/**
* Identifies whether an exception was thrown during a resend of a
* message or not. In this case a resend refers to whether the
* message is being resent from the buffer of messages internal.
* This would occur if we sent 5 messages quickly to APNS:
* 1,2,3,4,5 and the 3 message was rejected. We would
* then need to resend 4 and 5. If a network exception was
* triggered when doing this, then the resend flag will be
* {@code true}.
* @return {@code true} for an exception trigger during a resend, otherwise {@code false}.
*/
public boolean isResend() {
return resend;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2009, Mahmood Ali.
* 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 Mahmood Ali. 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
* OWNER 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.notnoop.exceptions;
import java.io.IOException;
/**
* Signals that an I/O exception of some sort has occurred. This
* class is the general class of exceptions produced by failed or
* interrupted I/O operations.
*
* This is a RuntimeException, unlike the java.io.IOException
*/
public class RuntimeIOException extends ApnsException {
private static final long serialVersionUID = 8665285084049041306L;
public RuntimeIOException() { super(); }
public RuntimeIOException(String message) { super(message); }
public RuntimeIOException(IOException cause) { super(cause); }
public RuntimeIOException(String m, IOException c) { super(m, c); }
}