Java tutorial
/* * Copyright (c) 2018. David Feng * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.bzcentre.dapiPush; import com.bzcentre.dapiPush.fcm.processors.MessageProcessor; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import com.turo.pushy.apns.PushNotificationResponse; import com.turo.pushy.apns.util.SimpleApnsPushNotification; import nginx.clojure.AppEventListenerManager.*; import nginx.clojure.NginxClojureRT; import nginx.clojure.NginxHttpServerChannel; import nginx.clojure.java.ArrayMap; import nginx.clojure.java.NginxJavaRingHandler; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.codec.digest.DigestUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.security.cert.CertificateException; import java.sql.*; import java.util.*; import java.util.Date; import java.util.concurrent.ConcurrentHashMap; import static com.bzcentre.dapiPush.dapiSecrets.*; import static nginx.clojure.MiniConstants.*; public class DapiReceiver extends TimerTask implements NginxJavaRingHandler, Listener { private final ApnsProxy apnsProxy = ApnsProxy.prepareClient(); private FcmProxy fcmProxy; public static final int SERVER_SENT_EVENTS = POST_EVENT_TYPE_COMPLEX_EVENT_IDX_START + 1; public static final String DISCONNECT = "shutdown!"; public static final String DISCONNECT_QUIET = "shutdownQuiet!"; final static String TAG = "[DapiReceiver][Worker:" + NginxClojureRT.processId + "] "; Connection dbconn = null; Statement stmt = null; Statement BLKCheck = null; String errMsg = null; public static Set<NginxHttpServerChannel> serverSentEventSubscribers; public DapiReceiver() { try { fcmProxy = FcmProxy.prepareClient(); dbconn = connectMySql(); stmt = dbconn.createStatement(); BLKCheck = dbconn.createStatement(); serverSentEventSubscribers = Collections.newSetFromMap(new ConcurrentHashMap<>()); NginxClojureRT.getAppEventListenerManager().addListener(this); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException e1) { NginxClojureRT.log.info(TAG + "Database connecting failed..." + e1.getMessage()); } catch (CertificateException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private int rebuildDBConnection(String statement, String query) { int rows = 0; NginxClojureRT.log.info(TAG + "Re-connecting jdbc mySQL... ...", statement); try { dbconn = connectMySql(); stmt = dbconn.createStatement(); rows = stmt.executeUpdate(query); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException e) { NginxClojureRT.log.info(TAG + "Database re-connecting failed..."); e.printStackTrace(); } return rows; } private ResultSet rebuildDBConnection(Map<String, String> target) { ResultSet rows = null; NginxClojureRT.log.info(TAG + "Re-connecting jdbc mySQL...", target.get("statement")); try { dbconn = connectMySql(); stmt = dbconn.createStatement(); BLKCheck = dbconn.createStatement(); if (target.get("statement").equals("stmt")) { rows = stmt.executeQuery(target.get("query")); } else if (target.get("statement").equals("BLKCheck")) { rows = BLKCheck.executeQuery(target.get("query")); } } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException e) { NginxClojureRT.log.info(TAG + "Database re-connecting failed..."); e.printStackTrace(); } return rows; } @Override public Object[] invoke(Map<String, Object> request) { NginxClojureRT.log.info(TAG + " is invoked..."); String chk_token; String user_id; String invitations; String return_code = ""; String dummy_header = "http://www.dummy.com/dummy?"; // full url for URLEncodedUtils String payload; String provider; MsgCounter msgCounter = new MsgCounter(); @SuppressWarnings("unused") Integer isModerator; String query; String dapiToken = newBzToken(service_seed); int push_status = NGX_HTTP_FORBIDDEN; GsonBuilder gBuilder = new GsonBuilder(); gBuilder.registerTypeAdapter(new TypeToken<Receipient>() { }.getType(), new ReceipientTypeAdapter()); Gson g = gBuilder.disableHtmlEscaping().serializeNulls().create(); List<String[]> undeliverables = new ArrayList<>(); Set<String> deliverables = new HashSet<>(); String msg = ""; errMsg = null; String requestMethod; // Supported request map constants can be find in the MiniConstants file requestMethod = request.get(REQUEST_METHOD).toString(); if (requestMethod.equals(GET) && request.containsKey(QUERY_STRING)) { try { msg = dummy_header + request.get(QUERY_STRING).toString(); } catch (NullPointerException e) { errMsg = "NullPointerException" + e.getMessage(); } } else if (requestMethod.equals(POST)) { if (request.containsKey(BODY)) { InputStream body = (InputStream) request.get(BODY); BufferedReader bReader = new BufferedReader(new InputStreamReader(body)); StringBuilder sbfFileContents = new StringBuilder(); //read file line by line try { while ((msg = bReader.readLine()) != null) { sbfFileContents.append(msg); } msg = dummy_header + sbfFileContents.toString(); } catch (IOException e) { errMsg = "IOException" + e.getMessage(); } catch (NullPointerException e) { errMsg = "Null Content, Error :" + e.getMessage(); } } else { errMsg = "NO BODY"; } } if (errMsg != null) { NginxClojureRT.log.info(TAG + "http parse error:" + errMsg); return new Object[] { NGX_HTTP_BAD_REQUEST, ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map "{\"method\":\"" + requestMethod + " \", \"message\":\"" + errMsg + "\"}" //response body can be string, File or Array/Collection of string or File }; } // invitations is a base64+URLencoded string try { NginxClojureRT.log.debug(TAG + "msg get from body:\n" + msg); final Map<String, Object> queryMap = convertQueryStringToMap(msg); PushNotificationResponse<SimpleApnsPushNotification> apnsProxyResponse; chk_token = queryMap.get("dapiToken").toString(); user_id = queryMap.get("user_id").toString(); invitations = queryMap.get("invitations").toString(); invitations = StringUtils.newStringUtf8(Base64.decodeBase64(invitations)); NginxClojureRT.log.debug(TAG + "after base64 decode:\n" + invitations); if (chk_token.equals(dapiToken)) { // Hoicoi Token validation List<Receipient> invitees; NginxClojureRT.log.info(TAG + "Parsing invitees from json..." + invitations); invitees = g.fromJson(invitations, new TypeToken<ArrayList<Receipient>>() { }.getType()); NginxClojureRT.log.info(TAG + "user " + user_id + " is sending " + invitees.size() + " push token(s) to user(s) " + g.toJson(invitees.get(0).getPayload().getAcme7())); // receipient={"fcm_token","apns_token","payload"} // payload class is as APNS message payload defined, FCM needs to map to it. msgCounter.countdown = invitees.size(); NginxClojureRT.log.info(TAG + "msgCounter[countdown,apns,fcm]:" + msgCounter.list()); for (Receipient receipient : invitees) { return_code = ""; payload = g.toJson(receipient.getPayload()); // isModerator= receipient.getIsMdr(); // default state sent_request, ApnsProxy will validate the result and make state update if (receipient.getApns_Token() != null && !receipient.getApns_Token().isEmpty() && payload != null) { query = "INSERT INTO `notification_push_blacklist` (`provider`,`user_id`,`to_token`) VALUES ('apns'," + receipient.getPayload().getAcme8() + ",'" + receipient.getApns_Token() + "')"; try { stmt.executeUpdate(query); } catch (SQLException e) { if (e.getErrorCode() != 1062) { // code 1062=duplicate entry NginxClojureRT.log.info(TAG + "apns query exception near line 186: " + e.getMessage() + " when\n" + query); } } provider = "apns"; switch (inBlackList(receipient.getPayload().getAcme8(), receipient.getApns_Token())) { case "sent_request": case "false": apnsProxyResponse = apnsProxy.apnsPush(receipient.getApns_Token(), payload); if (apnsProxyResponse.isAccepted()) { NginxClojureRT.log.info(TAG + "Pushing notification to user " + receipient.getPayload().getAcme8() + " through APNS."); MessageProcessor.pushBlackList(receipient.getApns_Token(), "whiteList", null); deliverables.add(receipient.getPayload().getAcme8()); push_status = (push_status == NGX_HTTP_FORBIDDEN ? NGX_HTTP_NO_CONTENT : push_status); //status 204 return_code = "apns_pushOK"; msgCounter.countdown--; msgCounter.apns++; } else { String reason = apnsProxyResponse.getRejectionReason(); Date timestamp = apnsProxyResponse.getTokenInvalidationTimestamp(); push_status = NGX_HTTP_NOT_FOUND; if (reason.equals("BadDeviceToken") || reason.equals("Unregistered")) { MessageProcessor.pushBlackList(receipient.getApns_Token(), reason, timestamp); } else { MessageProcessor.pushBlackList(receipient.getApns_Token(), "whiteList", null); } String[] undeliverable = { provider, receipient.getApns_Token(), receipient.getPayload().getAcme8() }; undeliverables.add(undeliverable); msgCounter.countdown--; } break; case "inactive": push_status = NGX_HTTP_NOT_FOUND;// status 404, to indicate that the user removes the app. return_code = "Unregistered"; String[] undeliverable = { provider, receipient.getApns_Token(), receipient.getPayload().getAcme8() }; undeliverables.add(undeliverable); msgCounter.countdown--; NginxClojureRT.log.info(TAG + "Already in blacklist:" + receipient.getApns_Token()); break; default: msgCounter.countdown--; return_code = "apns_blacklist_null_exception"; NginxClojureRT.log.info(TAG + "APNS BlackList check return null!"); break; } } if (receipient.getFcm_Token() != null && receipient.getFcm_Token().isEmpty() && payload != null) { // Timestamp timestamp = new Timestamp(System.currentTimeMillis()); query = "INSERT INTO `notification_push_blacklist` (`provider`,`user_id`,`to_token`) VALUES ('fcm'," + receipient.getPayload().getAcme8() + ",'" + receipient.getFcm_Token() + "')"; try { stmt.executeUpdate(query); } catch (SQLException e) { if (e.getClass().getName().equals("com.mysql.jdbc.CommunicationsException")) { rebuildDBConnection("stmt", query); } if (e.getErrorCode() != 1062) { // code 1062=duplicate entry NginxClojureRT.log.info(TAG + "odbc query exception near line 223 => Code:" + e.getErrorCode() + " : " + e.getMessage() + "\n" + query); } } provider = "fcm"; String responseType = inBlackList(receipient.getPayload().getAcme8(), receipient.getFcm_Token()); switch (responseType) { case "sent_request": case "false": msgCounter.countdown--; if (fcmProxy.fcmPush(receipient.getFcm_Token(), payload)) { deliverables.add(receipient.getPayload().getAcme8()); push_status = (push_status == NGX_HTTP_FORBIDDEN ? NGX_HTTP_NO_CONTENT : push_status); //status 204 return_code = "fcm_pushOK"; msgCounter.fcm++; break; } else { String response = inBlackList(receipient.getPayload().getAcme8(), receipient.getFcm_Token()); if (!response.equals("inactive")) { NginxClojureRT.log.info("TAG" + "Some thing wrong with the fcmPush. Expecting inactive but ... ->" + response); break; } else { msgCounter.countdown++; // if is inactive, continue inactive block, so add the counter back. } } case "inactive": push_status = NGX_HTTP_NOT_FOUND;// status 404, to indicate that the user removes the app. return_code = "Unregistered"; String[] undeliverable = { provider, receipient.getFcm_Token(), receipient.getPayload().getAcme8() }; undeliverables.add(undeliverable); msgCounter.countdown--; if (responseType.equals("inactive")) NginxClojureRT.log.info(TAG + "Already in blacklist:" + receipient.getFcm_Token()); break; default: msgCounter.countdown--; return_code = "fcm_blacklist_null_exception"; NginxClojureRT.log.info(TAG + "FCM BlackList nullException!"); break; } } NginxClojureRT.log.info(TAG + "msgCounter[countdown,apns,fcm]:" + msgCounter.list()); if (msgCounter.countdown == 0) { NginxClojureRT.log.info(TAG + "There are " + (msgCounter.apns + msgCounter.fcm) + " notification(s) ha(s)(ve) been successfully pushed to user(s) " + g.toJson(deliverables) + " for => " + invitees.get(0).getPayload().getAps().getAlert().getBody()); return wrapupPushResult(receipient.getPayload().getAcme8(), push_status, return_code, deliverables, msgCounter, undeliverables); } } } else { return_code = "InvalidToken"; errMsg = "HoiCoi Token is not valid<br>" + chk_token + "<br>" + dapiToken; } } catch (IllegalArgumentException | JsonParseException | IllegalStateException | NullPointerException | ClassCastException | URISyntaxException e) { return_code = e.getClass().getName(); errMsg = e.getMessage(); e.printStackTrace(); } return new Object[] { NGX_HTTP_FORBIDDEN, ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map "{\"code\":\"" + (return_code.isEmpty() ? "future_not_response" : return_code) + "\", \"message\":\"Should return from the Future response.\"}" //response body can be string, File or Array/Collection of string or File }; } private Object[] wrapupPushResult(String user_id, int push_status, String return_code, Set<String> deliverables, MsgCounter msgCounter, List<String[]> undeliverables) { GsonBuilder gBuilder = new GsonBuilder(); Gson g = gBuilder.disableHtmlEscaping().serializeNulls().create(); String query = ""; int rows; if (deliverables.size() > 0) { //something has been pushed try { stmt = dbconn.createStatement(); if (msgCounter.apns > 0) { query = "UPDATE `Statistics` SET `counter`=`counter`+" + msgCounter.apns + " WHERE `user_id`=" + user_id + " AND `provider`='apns'"; rows = stmt.executeUpdate(query); if (rows == 0) { query = "INSERT INTO `Statistics` (`provider`,`user_id`,`counter`) VALUES ('apns'," + user_id + "," + deliverables.size() + ")"; stmt.executeUpdate(query); } } if (msgCounter.fcm > 0) { query = "UPDATE `Statistics` SET `counter`=`counter`+" + msgCounter.fcm + " WHERE `user_id`=" + user_id + " AND `provider`='fcm'"; rows = stmt.executeUpdate(query); if (rows == 0) { query = "INSERT INTO `Statistics` (`provider`,`user_id`,`counter`) VALUES ('fcm'," + user_id + "," + deliverables.size() + ")"; stmt.executeUpdate(query); } } } catch (SQLException | NullPointerException e) { if (e.getClass().getName().equals("com.mysql.jdbc.CommunicationsException")) { if (msgCounter.apns > 0) { query = "UPDATE `Statistics` SET `counter`=`counter`+" + msgCounter.apns + " WHERE `user_id`=" + user_id + " AND `provider`='apns'"; rows = rebuildDBConnection("stmt", query); if (rows == 0) { query = "INSERT INTO `Statistics` (`provider`,`user_id`,`counter`) VALUES ('apns'," + user_id + "," + msgCounter.apns + ")"; try { stmt.executeUpdate(query); } catch (SQLException e1) { e1.printStackTrace(); } } } if (msgCounter.fcm > 0) { query = "UPDATE `Statistics` SET `counter`=`counter`+" + msgCounter.fcm + " WHERE `user_id`=" + user_id + " AND `provider`='fcm'"; rows = rebuildDBConnection("stmt", query); if (rows == 0) { query = "INSERT INTO `Statistics` (`provider`,`user_id`,`counter`) VALUES ('apns'," + user_id + "," + msgCounter.fcm + ")"; try { stmt.executeUpdate(query); } catch (SQLException e1) { e1.printStackTrace(); } } } } NginxClojureRT.log.info(TAG + e.getMessage() + " \nquery=" + query); } finally { NginxClojureRT.log.info( TAG + "Statistics updated with total msg count:" + (msgCounter.apns + msgCounter.fcm)); } } // remove push succeeded items from undeliverables for (String deliverable : deliverables) { for (int i = 0; i < undeliverables.size(); i++) { if (deliverable.equals(undeliverables.get(i)[2])) { undeliverables.remove(i); } } } NginxClojureRT.log.info(TAG + " return " + push_status + " and " + undeliverables.size() + " undeliverables. errorCode:" + return_code + " errMeg:" + errMsg); // 1. There are tokens that may have been expired. dapiAir will call unregisterTokens if (!undeliverables.isEmpty()) { return new Object[] { push_status, // this should be NGX_HTTP_NOT_FOUND ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map "{\"code\":\"" + return_code + "\", \"message\":" + g.toJson(undeliverables.toArray()) + "}" //response body can be string, File or Array/Collection of string or File }; } // 2. something wrong if ((return_code != null && return_code.indexOf("pushOK") == -1) || push_status == NGX_HTTP_NOT_FOUND) { return new Object[] { push_status, ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map "{\"code\":\"" + return_code + "\", \"message\":\"" + errMsg + "\"}" //response body can be string, File or Array/Collection of string or File }; } // 3. everyghing fine return new Object[] { push_status, // 204 OK null, null }; } private static String newBzToken(String seed) { return DigestUtils.sha1Hex(seed + hoicoi_token); } /** * @param user_id * @param to_token * @return String inactive (is in black list), sent_request (unknow, still in progress), false (not in black list) */ private String inBlackList(String user_id, String to_token) { String check_sql; ResultSet result; check_sql = "SELECT `user_id`, UNIX_TIMESTAMP(timestamp) as timestamp, `state` FROM `notification_push_blacklist` " + "WHERE `to_token`='" + to_token + "'"; try { result = BLKCheck.executeQuery(check_sql); if (result.next()) { if (user_id.equals(result.getString("user_id"))) { return result.getString("state"); // sent_request, inactive } else { return null; } } } catch (SQLException e) { if (e.getClass().getName().equals("com.mysql.jdbc.CommunicationsException")) { Map<String, String> target = new HashMap<>(); target.put("BLKCheck", check_sql); result = this.rebuildDBConnection(target); try { if (result.next()) { if (user_id.equals(result.getString("user_id"))) { return result.getString("state"); // sent_request, inactive } else { return null; } } } catch (SQLException se) { NginxClojureRT.log.info(TAG + check_sql); e.printStackTrace(); return null; } } else { NginxClojureRT.log.info(TAG + check_sql); e.printStackTrace(); } return null; } return "false"; } // parse query string, the url needs to be encoded or it will throw exceptions. public static Map<String, Object> convertQueryStringToMap(String url) throws URISyntaxException { List<NameValuePair> params = URLEncodedUtils.parse(new URI(url), Charset.defaultCharset()); Map<String, Object> queryStringMap = new HashMap<>(); for (NameValuePair param : params) { queryStringMap.put(param.getName(), handleMultiValuedQueryParam(queryStringMap, param.getName(), param.getValue())); } return queryStringMap; } private static Object handleMultiValuedQueryParam(Map<String, Object> responseMap, String key, String value) { if (!responseMap.containsKey(key)) { // haven't been handled yet, simply return it to the map either a multiple value hashSet or a single value // return value.contains(",") ? new HashSet<String>(Arrays.asList(value.split(","))) : value; return value; } else { // else if already processed, add it to the existing one. @SuppressWarnings("unchecked") Set<String> queryValueSet = responseMap.get(key) instanceof Set ? (Set<String>) responseMap.get(key) : new HashSet<>(); // if (value.contains(",")) { // this is for array parameter, we don't use it here. // queryValueSet.addAll(Arrays.asList(value.split(","))); // } else { queryValueSet.add(value); // } return queryValueSet; } } // https://dev.mysql.com/doc/refman/5.7/en/secure-connections.html private static Connection connectMySql() throws ClassNotFoundException, SQLException, InstantiationException, IllegalAccessException { // Load the JDBC driver String driver = "com.mysql.jdbc.Driver"; //String driver = "org.gjt.mm.mysql.Driver"; Class.forName(driver).newInstance(); // Create a connection to the database String url = "jdbc:mysql://localhost/DapiPush?autoReconnect=true&failOverReadOnly=false&useSSL=false&requireSSL=false"; return DriverManager.getConnection(url, dbUsername, dbPassword); } /** * Implements TimerTask's abstract run method. */ @Override public void run() { // payloadBuilder.setAlertTitle("Dapi Push says: "+counter); // String payload = (counter % 2 == 0) ? payloadBuilder.buildWithDefaultMaximumLength(): testNote; // if(apnsProxy.getReadyStatus() == true){ // final SimpleApnsPushNotification pushNotification; // { // pushNotification = new SimpleApnsPushNotification(testToken, topic, payload); // counter++; // if(counter > 6) notifyMe.cancel(); // } // final Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture = // apnsProxy.sendNotification(pushNotification); // } else { // NginxClojureRT.log.debug(TAG+"Notify Me failed! dapiPush not connected!"); // } } @Override public void onEvent(PostedEvent event) { //pub/sub management String message = new String((byte[]) event.data, event.offset, event.length, DEFAULT_ENCODING); if (event.tag == SERVER_SENT_EVENTS) { try { if (DISCONNECT.equals(message) || DISCONNECT_QUIET.equals(message)) { try { dbconn.close(); } catch (SQLException e) { e.printStackTrace(); } if (apnsProxy != null && apnsProxy.isConnected()) { apnsProxy.disconnect(); NginxClojureRT.log.info(TAG + "Nginx stop/reload, disconnect with apns push servers"); } else { NginxClojureRT.log.info(TAG + "Apns is already disconnected!"); } if (fcmProxy != null && fcmProxy.isConnected()) { fcmProxy.disconnect(); NginxClojureRT.log.info(TAG + "Nginx stop/reload, disconnect with fcm push servers"); } else { NginxClojureRT.log.info(TAG + "fcm is already disconnected!"); } } for (NginxHttpServerChannel channel : serverSentEventSubscribers) { if (DISCONNECT.equals(message)) { channel.send("data: " + message + "\r\n\r\n", true, true); channel.close(); } else if (DISCONNECT_QUIET.equals(message)) { channel.close(); } else { NginxClojureRT.log.info(TAG + "Pub/Sub Message received: " + message); channel.send("data: " + message + "\r\n\r\n", true, false); } NginxClojureRT.getAppEventListenerManager().removeListener(this); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } private class MsgCounter { int apns = 0; int fcm = 0; int countdown; public String list() { return "[" + countdown + "," + apns + "," + fcm + "]"; } } }