commit push server
This commit is contained in:
11
src/main/java/cn/wildfirechat/push/PushApplication.java
Normal file
11
src/main/java/cn/wildfirechat/push/PushApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
28
src/main/java/cn/wildfirechat/push/PushController.java
Normal file
28
src/main/java/cn/wildfirechat/push/PushController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
161
src/main/java/cn/wildfirechat/push/PushMessage.java
Normal file
161
src/main/java/cn/wildfirechat/push/PushMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
7
src/main/java/cn/wildfirechat/push/PushMessageType.java
Normal file
7
src/main/java/cn/wildfirechat/push/PushMessageType.java
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package cn.wildfirechat.push.android;
|
||||
|
||||
import cn.wildfirechat.push.PushMessage;
|
||||
|
||||
public interface AndroidPushService {
|
||||
Object push(PushMessage pushMessage);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
136
src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java
Normal file
136
src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
86
src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java
Normal file
86
src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
187
src/main/java/cn/wildfirechat/push/ios/ApnsServer.java
Normal file
187
src/main/java/cn/wildfirechat/push/ios/ApnsServer.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package cn.wildfirechat.push.ios;
|
||||
|
||||
import cn.wildfirechat.push.PushMessage;
|
||||
|
||||
public interface IOSPushService {
|
||||
Object push(PushMessage pushMessage);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
6
src/main/java/cn/wildfirechat/push/ios/IOSPushType.java
Normal file
6
src/main/java/cn/wildfirechat/push/ios/IOSPushType.java
Normal 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;
|
||||
}
|
||||
57
src/main/java/com/notnoop/apns/APNS.java
Executable file
57
src/main/java/com/notnoop/apns/APNS.java
Executable 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();
|
||||
}
|
||||
}
|
||||
92
src/main/java/com/notnoop/apns/ApnsDelegate.java
Executable file
92
src/main/java/com/notnoop/apns/ApnsDelegate.java
Executable 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();
|
||||
}
|
||||
52
src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java
Executable file
52
src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java
Executable 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) {
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/notnoop/apns/ApnsNotification.java
Executable file
76
src/main/java/com/notnoop/apns/ApnsNotification.java
Executable 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();
|
||||
}
|
||||
140
src/main/java/com/notnoop/apns/ApnsService.java
Executable file
140
src/main/java/com/notnoop/apns/ApnsService.java
Executable 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;
|
||||
|
||||
}
|
||||
760
src/main/java/com/notnoop/apns/ApnsServiceBuilder.java
Executable file
760
src/main/java/com/notnoop/apns/ApnsServiceBuilder.java
Executable 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().");
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/notnoop/apns/DeliveryError.java
Executable file
81
src/main/java/com/notnoop/apns/DeliveryError.java
Executable 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;
|
||||
}
|
||||
}
|
||||
191
src/main/java/com/notnoop/apns/EnhancedApnsNotification.java
Executable file
191
src/main/java/com/notnoop/apns/EnhancedApnsNotification.java
Executable 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+")";
|
||||
}
|
||||
}
|
||||
535
src/main/java/com/notnoop/apns/PayloadBuilder.java
Executable file
535
src/main/java/com/notnoop/apns/PayloadBuilder.java
Executable 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();
|
||||
}
|
||||
}
|
||||
118
src/main/java/com/notnoop/apns/ReconnectPolicy.java
Executable file
118
src/main/java/com/notnoop/apns/ReconnectPolicy.java
Executable 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();
|
||||
}
|
||||
}
|
||||
172
src/main/java/com/notnoop/apns/SimpleApnsNotification.java
Executable file
172
src/main/java/com/notnoop/apns/SimpleApnsNotification.java
Executable 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;
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java
Executable file
47
src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java
Executable 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);
|
||||
|
||||
}
|
||||
90
src/main/java/com/notnoop/apns/internal/AbstractApnsService.java
Executable file
90
src/main/java/com/notnoop/apns/internal/AbstractApnsService.java
Executable 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();
|
||||
}
|
||||
}
|
||||
52
src/main/java/com/notnoop/apns/internal/ApnsConnection.java
Executable file
52
src/main/java/com/notnoop/apns/internal/ApnsConnection.java
Executable 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();
|
||||
}
|
||||
412
src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java
Executable file
412
src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java
Executable 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;
|
||||
}
|
||||
}
|
||||
121
src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java
Executable file
121
src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java
Executable 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
121
src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java
Executable file
121
src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java
Executable 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();
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java
Executable file
59
src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java
Executable 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();
|
||||
}
|
||||
}
|
||||
143
src/main/java/com/notnoop/apns/internal/BatchApnsService.java
Executable file
143
src/main/java/com/notnoop/apns/internal/BatchApnsService.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/main/java/com/notnoop/apns/internal/QueuedApnsService.java
Executable file
126
src/main/java/com/notnoop/apns/internal/QueuedApnsService.java
Executable 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();
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java
Executable file
67
src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java
Executable file
179
src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java
Executable file
147
src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
296
src/main/java/com/notnoop/apns/internal/Utilities.java
Executable file
296
src/main/java/com/notnoop/apns/internal/Utilities.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
61
src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java
Executable file
61
src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java
Executable 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
44
src/main/java/com/notnoop/exceptions/ApnsException.java
Executable file
44
src/main/java/com/notnoop/exceptions/ApnsException.java
Executable 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); }
|
||||
|
||||
}
|
||||
64
src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java
Executable file
64
src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java
Executable 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); }
|
||||
|
||||
}
|
||||
69
src/main/java/com/notnoop/exceptions/NetworkIOException.java
Executable file
69
src/main/java/com/notnoop/exceptions/NetworkIOException.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
50
src/main/java/com/notnoop/exceptions/RuntimeIOException.java
Executable file
50
src/main/java/com/notnoop/exceptions/RuntimeIOException.java
Executable 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); }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user