"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
Creating a Facebook Clone - Part XLV - Transcript.pdf
1. Creating a Facebook Clone - Part XLV
The changes to the NotificationService are significant and in effect that's the class that implements push notification calls from the server.
2. @Service
public class NotificationService {
private static final String PUSHURL =
"https://push.codenameone.com/push/push";
private final static Logger logger = LoggerFactory.
getLogger(NotificationService.class);
@Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
NotificationService
Before we start we need to add a few fields to the class, these will be used by the code we introduce soon. These are besides the keys field I mentioned before.
This is the URL of the Codename One push server, we'll use that to send out push messages
3. @Service
public class NotificationService {
private static final String PUSHURL =
"https://push.codenameone.com/push/push";
private final static Logger logger = LoggerFactory.
getLogger(NotificationService.class);
@Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
NotificationService
Logging is used for errors in the body of the class
4. @Service
public class NotificationService {
private static final String PUSHURL =
"https://push.codenameone.com/push/push";
private final static Logger logger = LoggerFactory.
getLogger(NotificationService.class);
@Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
NotificationService
We need to inject the users repository now as we need to find the user there
5. @Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
public void removePushKey(String key) {
List<User> userList = users.findByPushKey(key);
if(!userList.isEmpty()) {
User u = userList.get(0);
u.setPushKey(null);
users.save(u);
}
}
public void sendNotification(User u, NotificationDAO nd) {
NotificationService
The first step is trivial, it's about mapping the push key to the service class which is exactly what we do here
6. @Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
public void removePushKey(String key) {
List<User> userList = users.findByPushKey(key);
if(!userList.isEmpty()) {
User u = userList.get(0);
u.setPushKey(null);
users.save(u);
}
}
public void sendNotification(User u, NotificationDAO nd) {
NotificationService
On push registration we invoke this method to update the push key in the user object
7. @Autowired
private NotificationRepository notifications;
@Autowired
private APIKeys keys;
@Autowired
private UserRepository users;
public void updatePushKey(String auth, String key) {
User u = users.findByAuthtoken(auth).get(0);
u.setPushKey(key);
users.save(u);
}
public void removePushKey(String key) {
List<User> userList = users.findByPushKey(key);
if(!userList.isEmpty()) {
User u = userList.get(0);
u.setPushKey(null);
users.save(u);
}
}
public void sendNotification(User u, NotificationDAO nd) {
NotificationService
If the push server indicates that a push key is invalid or expired we delete that key. A fresh key will be created the next time the user runs the app.
The second method is internal to the server and isn't exposed as a WebService but the former method is.
8. users.save(u);
}
}
public void sendNotification(User u, NotificationDAO nd) {
Notification n = new Notification();
n.setDate(System.currentTimeMillis());
n.setReaction(nd.getReaction());
n.setReactionColor(nd.getReactionColor());
n.setText(nd.getText());
n.setUser(u);
n.setWasRead(nd.isWasRead());
notifications.save(n);
if(u.getPushKey() != null) {
sendPushNotification(u.getPushKey(), nd.getText(), 1);
}
}
public List<NotificationDAO> listNotifications(String authtoken,
int page, int amount) {
Page<Notification> notificationsPage = notifications.
findNotificationsForUser(
authtoken, PageRequest.of(page, amount,
Sort.by(Sort.Direction.DESC, "date")));
List<NotificationDAO> resp = new ArrayList<>();
NotificationService
The next stage is the push notification itself, we add these lines to the sendNotification method. Notice that a push key can be null since it can expire or might not be
registered yet… This leads us to the sendPushNotification method but we’ll get there via a detour
9. resp.add(c.getDAO());
}
return resp;
}
private String sendPushImpl(String deviceId,
String messageBody, int type) throws IOException {
HttpURLConnection connection = (HttpURLConnection)
new URL(PUSHURL).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
String cert;
String pass;
boolean production = keys.get("push.itunes_production",
"false").equals("true");
if(production) {
cert = keys.get("push.itunes_prodcert");
pass = keys.get("push.itunes_prodpass");
} else {
cert = keys.get("push.itunes_devcert");
pass = keys.get("push.itunes_devpass");
}
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
NotificationService
This method is based on the server push code from the developer guide. Notice that this is the impl method not the method we invoked before. We'll go through this first
and then reach that code
10. resp.add(c.getDAO());
}
return resp;
}
private String sendPushImpl(String deviceId,
String messageBody, int type) throws IOException {
HttpURLConnection connection = (HttpURLConnection)
new URL(PUSHURL).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
String cert;
String pass;
boolean production = keys.get("push.itunes_production",
"false").equals("true");
if(production) {
cert = keys.get("push.itunes_prodcert");
pass = keys.get("push.itunes_prodpass");
} else {
cert = keys.get("push.itunes_devcert");
pass = keys.get("push.itunes_devpass");
}
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
NotificationService
We connect to the push server URL to send the push message
11. resp.add(c.getDAO());
}
return resp;
}
private String sendPushImpl(String deviceId,
String messageBody, int type) throws IOException {
HttpURLConnection connection = (HttpURLConnection)
new URL(PUSHURL).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
String cert;
String pass;
boolean production = keys.get("push.itunes_production",
"false").equals("true");
if(production) {
cert = keys.get("push.itunes_prodcert");
pass = keys.get("push.itunes_prodpass");
} else {
cert = keys.get("push.itunes_devcert");
pass = keys.get("push.itunes_devpass");
}
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
NotificationService
A standard form POST submission request is used
12. connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
String cert;
String pass;
boolean production = keys.get("push.itunes_production",
"false").equals("true");
if(production) {
cert = keys.get("push.itunes_prodcert");
pass = keys.get("push.itunes_prodpass");
} else {
cert = keys.get("push.itunes_devcert");
pass = keys.get("push.itunes_devpass");
}
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
"&type=" + type + "&auth=" +
URLEncoder.encode(keys.get("push.gcm_key"), "UTF-8") +
"&certPassword=" + URLEncoder.encode(pass, "UTF-8") +
"&cert=" + URLEncoder.encode(cert, "UTF-8") +
"&body=" + URLEncoder.encode(messageBody, "UTF-8") +
"&production=" + production;
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
NotificationService
We fetch the values for the fields that can differ specifically the certificates & passwords which vary between production and development
13. if(production) {
cert = keys.get("push.itunes_prodcert");
pass = keys.get("push.itunes_prodpass");
} else {
cert = keys.get("push.itunes_devcert");
pass = keys.get("push.itunes_devpass");
}
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
"&type=" + type + "&auth=" +
URLEncoder.encode(keys.get("push.gcm_key"), "UTF-8") +
"&certPassword=" + URLEncoder.encode(pass, "UTF-8") +
"&cert=" + URLEncoder.encode(cert, "UTF-8") +
"&body=" + URLEncoder.encode(messageBody, "UTF-8") +
"&production=" + production;
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
if(c == 200) {
try (InputStream i = connection.getInputStream()) {
return new String(readInputStream(i));
}
}
logger.error("Error response code from push server: " + c);
NotificationService
We send all the details as a POST request to the Codename One push server which then issues a push to the given device type
14. }
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
"&type=" + type + "&auth=" +
URLEncoder.encode(keys.get("push.gcm_key"), "UTF-8") +
"&certPassword=" + URLEncoder.encode(pass, "UTF-8") +
"&cert=" + URLEncoder.encode(cert, "UTF-8") +
"&body=" + URLEncoder.encode(messageBody, "UTF-8") +
"&production=" + production;
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
if(c == 200) {
try (InputStream i = connection.getInputStream()) {
return new String(readInputStream(i));
}
}
logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
NotificationService
The server can return 200 in case of an error but if the response isn't 200 then it's surely an error
15. }
String query = "token=" + keys.get("push.token") +
"&device=" + URLEncoder.encode(deviceId, "UTF-8") +
"&type=" + type + "&auth=" +
URLEncoder.encode(keys.get("push.gcm_key"), "UTF-8") +
"&certPassword=" + URLEncoder.encode(pass, "UTF-8") +
"&cert=" + URLEncoder.encode(cert, "UTF-8") +
"&body=" + URLEncoder.encode(messageBody, "UTF-8") +
"&production=" + production;
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
if(c == 200) {
try (InputStream i = connection.getInputStream()) {
return new String(readInputStream(i));
}
}
logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
NotificationService
The server response is transformed to a String for parsing on next method, I'll cover the readInputStream method soon
16. logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
NotificationService
We need to go over the responses from the push server. These responses would include information such as push key expiration and we would need to purge that key
from our database.
That's all done in the actual method for sending push messages
17. logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
NotificationService
The @Async annotation indicates this method should execute on a separate thread, this is handled by Spring. We'll discuss that soon
18. logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
NotificationService
sendPushImpl return JSON with messages which we need to process
19. logger.error("Error response code from push server: " + c);
return null;
}
@Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
NotificationService
The Spring JSON parsing API has different forms for Map/List but we can get both in the response from the server so we need to check
20. @Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
}
} else {
Map<String, Object> m = parser.parseMap(json);
NotificationService
If it's a list then it's a device list with either acknowledgement of sending (Android only) or error messages
21. @Async
public void sendPushNotification(String deviceId,
String messageBody, int type) {
try {
String json = sendPushImpl(deviceId, messageBody, type);
if(json != null) {
JsonParser parser = JsonParserFactory.getJsonParser();
if(json.startsWith("[")) {
List<Object> lst = parser.parseList(json);
for(Object o : lst) {
if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
}
} else {
Map<String, Object> m = parser.parseMap(json);
NotificationService
If we have an error message for a specific device key we need to remove it to prevent future problems with the push servers
22. if(o instanceof Map) {
Map entry = (Map)o;
String status = (String)entry.get("status");
if(status != null) {
if(status.equals("error") ||
status.equals("inactive")) {
removePushKey((String)
entry.get("id"));
}
}
}
}
} else {
Map<String, Object> m = parser.parseMap(json);
if(m.containsKey("error")) {
logger.error("Error message from server: " +
m.get("error"));
}
}
}
} catch(IOException err) {
logger.error("Error during connection to push server", err);
}
}
private static byte[] readInputStream(InputStream i)
NotificationService
If we got a Map in response it could indicate an error which we currently don't really handle other than through logging.
If push doesn't work the app would still work fine you'll just need to refresh manually. A better implementation would also use a fallback for cases of push failure e.g.
WebSocket. But it's not essential at least not at first.
23. }
}
} catch(IOException err) {
logger.error("Error during connection to push server", err);
}
}
private static byte[] readInputStream(InputStream i)
throws IOException {
try(ByteArrayOutputStream b = new ByteArrayOutputStream()) {
copy(i, b);
return b.toByteArray();
}
}
private static void copy(InputStream i, OutputStream o)
throws IOException {
byte[] buffer = new byte[8192];
int size = i.read(buffer);
while(size > -1) {
o.write(buffer, 0, size);
size = i.read(buffer);
}
}
}
NotificationService
We referenced readInputStream in the previous code blocks it's defined in the code as such. I could have written more modern code using nio but I was running out of
time and I had this code handy. It does the job well…
24. @RequestMapping(method=RequestMethod.GET, value="/notifications")
public List<NotificationDAO> listNotifications(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="page") int page,
@RequestParam(name="size") int size) {
return notifications.listNotifications(auth, page, size);
}
@RequestMapping(method=RequestMethod.POST, value="/contacts")
public String uploadContacts(
@RequestHeader(name="auth", required=true) String auth,
@RequestBody List<ShadowUserDAO> contacts) {
users.uploadContacts(auth, contacts);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value="/updatePush")
public String updatePushKey(
@RequestHeader(name="auth", required=true) String auth,
String key) {
notifications.updatePushKey(auth, key);
return "OK";
}
}
UserWebService
Next, we expose the push registration method as a restful webservice.
This is a direct mapping to the update method so there isn't much to say about that…
25. @SpringBootApplication
@EnableAsync
public class FacebookCloneServerApplication {
public static void main(String[] args) {
SpringApplication.run(FacebookCloneServerApplication.class, args);
}
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(5000);
executor.setThreadNamePrefix("PushThread-");
executor.initialize();
return executor;
}
}
FacebookCloneServerApplication
The last piece of the server code is the changes we need to make to the FacebookCloneServerApplication class. We didn't touch that class at all when we started off but
now we need some changes to support the asynchronous sendPushNotification method.
First we need the @EnableAsync annotation so @Async will work in the app
26. @SpringBootApplication
@EnableAsync
public class FacebookCloneServerApplication {
public static void main(String[] args) {
SpringApplication.run(FacebookCloneServerApplication.class, args);
}
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(5000);
executor.setThreadNamePrefix("PushThread-");
executor.initialize();
return executor;
}
}
FacebookCloneServerApplication
The @Bean for the asyncExecutor creates the thread pool used when we invoke an @Async method. We define reasonable capacities, we don't want an infinite queue as
we can run out of RAM & it can be used in a Denial of Service attack.
We also don't want too many threads in the pool as our server might overcrowd the push servers and receive rate limits. This level should work fine.
With that the server side portion of push support is done.