diff options
Diffstat (limited to 'juick-notifications/src')
10 files changed, 257 insertions, 140 deletions
diff --git a/juick-notifications/src/main/java/com/juick/components/MPNSClient.java b/juick-notifications/src/main/java/com/juick/components/MPNSClient.java new file mode 100644 index 00000000..165bb94c --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/MPNSClient.java @@ -0,0 +1,103 @@ +package com.juick.components; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.components.mpns.MPNSError; +import com.juick.components.mpns.MPNSToken; +import org.apache.http.*; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by vital on 29.03.2017. + */ +public class MPNSClient { + + private static Logger logger = LoggerFactory.getLogger(MPNSClient.class); + + private String accessToken; + + private NotificationClientListener listener; + + @Inject + private ObjectMapper jsonMapper; + @Value("${wns_application_sip}") + private String applicationSip; + @Value("${wns_client_secret}") + private String applicationSecret; + + @PostConstruct + public void authenticate() throws IOException { + HttpClient client = HttpClientBuilder.create().build(); + String url = "https://login.live.com/accesstoken.srf"; + List<NameValuePair> formParams = new ArrayList<>(); + formParams.add(new BasicNameValuePair("grant_type", "client_credentials")); + formParams.add(new BasicNameValuePair("client_id", applicationSip)); + formParams.add(new BasicNameValuePair("client_secret", applicationSecret)); + formParams.add(new BasicNameValuePair("scope", "notify.windows.com")); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); + HttpPost httppost = new HttpPost(url); + httppost.setEntity(entity); + HttpResponse response = client.execute(httppost); + int statusCode = response.getStatusLine().getStatusCode(); + String responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8); + if (statusCode != HttpStatus.SC_OK) { + MPNSError error = jsonMapper.readValue(responseContent, MPNSError.class); + throw new IOException(error.getError() + ": " + error.getErrorDescription()); + } + MPNSToken token = jsonMapper.readValue(responseContent, MPNSToken.class); + if (token.getTokenType().length() >= 1) { + token.setTokenType(Character.toUpperCase(token.getTokenType().charAt(0)) + token.getTokenType().substring(1)); + } + accessToken = token.getTokenType() + " " + token.getAccessToken(); + logger.info("MPNS authenticated"); + } + + void sendNotification(final String url, final String xml) throws IOException { + HttpClient client = HttpClientBuilder.create().build(); + StringEntity entity = new StringEntity(xml, Consts.UTF_8); + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("Content-Type", "text/xml"); + httpPost.setHeader("Authorization", accessToken); + httpPost.setHeader("X-WNS-Type", "wns/toast"); + httpPost.setEntity(entity); + HttpResponse response = client.execute(httpPost); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + if (statusCode == HttpStatus.SC_GONE) { + // expired + logger.info("{} is scheduled to remove", url); + listener.invalidToken("mpns", url); + } else { + String headersContent = stringifyWnsHttpHeaders(response.getAllHeaders()); + throw new IOException(headersContent); + } + } + } + + private String stringifyWnsHttpHeaders(final Header[] allHeaders) { + return Arrays.stream(allHeaders) + .filter(x -> x.getName().startsWith("X-WNS-") || x.getName().startsWith("WWW-")) + .map(x -> x.getName() + ": " + x.getValue()) + .collect(Collectors.joining("\n")); + } + + public void setListener(NotificationClientListener listener) { + this.listener = listener; + } +} diff --git a/juick-notifications/src/main/java/com/juick/components/NotificationClientListener.java b/juick-notifications/src/main/java/com/juick/components/NotificationClientListener.java new file mode 100644 index 00000000..46bd683f --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/NotificationClientListener.java @@ -0,0 +1,8 @@ +package com.juick.components; + +/** + * Created by vital on 29.03.2017. + */ +public interface NotificationClientListener { + void invalidToken(String type, String token); +} diff --git a/juick-notifications/src/main/java/com/juick/components/Notifications.java b/juick-notifications/src/main/java/com/juick/components/Notifications.java index 00f63136..673ea402 100644 --- a/juick-notifications/src/main/java/com/juick/components/Notifications.java +++ b/juick-notifications/src/main/java/com/juick/components/Notifications.java @@ -17,30 +17,17 @@ */ package com.juick.components; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.android.gcm.server.*; import com.juick.TokensList; -import com.juick.components.mpns.MPNSError; -import com.juick.components.mpns.MPNSToken; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsService; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; -import org.apache.http.*; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; -import org.apache.http.util.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.web.client.RestTemplate; import rocks.xmpp.core.XmppException; @@ -57,52 +44,31 @@ import java.util.stream.Collectors; /** * @author Ugnich Anton */ -public class Notifications implements AutoCloseable { +public class Notifications implements NotificationClientListener, AutoCloseable { private static Logger logger = LoggerFactory.getLogger(Notifications.class); - private final RestTemplate rest; + @Inject + private RestTemplate rest; + @Inject private ExternalComponent xmpp; - private final Sender GCMSender; - - private final String wns_application_sip; - private final String wns_client_secret; - private final String pushJid; - private final String xmppHost; - private final int xmppPort; - private final String xmppPushPassword; + @Inject + private Sender GCMSender; - private final ObjectMapper mapper; + @Inject + private ObjectMapper jsonMapper; - private final Set<String> invalidGCMTokens; - private final Set<String> invalidMPNSTokens; + private final Set<String> invalidGCMTokens = Collections.synchronizedSet(new HashSet<>()); + private final Set<String> invalidMPNSTokens = Collections.synchronizedSet(new HashSet<>()); @Inject + private MPNSClient mpnsClient; + @Inject private ApnsService apns; - - public Notifications(final Environment env, final RestTemplate rest) { - this.rest = rest; - wns_application_sip = env.getProperty("wns_application_sip", StringUtils.EMPTY); - wns_client_secret = env.getProperty("wns_client_secret", StringUtils.EMPTY); - GCMSender = new Sender(env.getProperty("gcm_key", StringUtils.EMPTY), Endpoint.GCM); - pushJid = env.getProperty("push_jid"); - xmppHost = env.getProperty("xmpp_host", "localhost"); - xmppPort = NumberUtils.toInt(env.getProperty("xmpp_port"), 5347); - xmppPushPassword = env.getProperty("push_xmpp_password", StringUtils.EMPTY); - mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); - invalidGCMTokens = Collections.synchronizedSet(new HashSet<>()); - invalidMPNSTokens = Collections.synchronizedSet(new HashSet<>()); - } - @PostConstruct - public void init() { - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.Message.class)) - .build(); - xmpp = ExternalComponent.create(pushJid, xmppPushPassword, configuration, xmppHost, xmppPort); + public void init() throws IOException { + mpnsClient.authenticate(); + mpnsClient.setListener(this); xmpp.addInboundMessageListener(e -> { rocks.xmpp.core.stanza.model.Message msg = e.getMessage(); com.juick.Message jmsg = msg.getExtension(com.juick.Message.class); @@ -135,11 +101,7 @@ public class Notifications implements AutoCloseable { .flatMap(t -> t.getTokens().stream()).collect(Collectors.toList()); if (!regids.isEmpty()) { try { - ObjectMapper messageSerializer = new ObjectMapper(); - messageSerializer.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - messageSerializer.setSerializationInclusion(JsonInclude.Include.NON_NULL); - messageSerializer.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); - String json = messageSerializer.writeValueAsString(jmsg); + String json = jsonMapper.writeValueAsString(jmsg); logger.info(json); Message message = new Message.Builder().addData("message", json).build(); MulticastResult result = GCMSender.send(message, regids, 3); @@ -173,7 +135,6 @@ public class Notifications implements AutoCloseable { logger.info("WNS: no recipients"); } else { try { - String wnsToken = getWnsAccessToken(); String text1 = "@" + jmsg.getUser().getName(); if (!jmsg.getTags().isEmpty()) { text1 += ":" + StringEscapeUtils.escapeXml11(jmsg.getTagsString()); @@ -195,7 +156,7 @@ public class Notifications implements AutoCloseable { logger.trace(xml); for (String url : urls) { logger.info("WNS: {}", url); - sendWNS(wnsToken, url, xml); + mpnsClient.sendNotification(url, xml); } } catch (IOException | IllegalStateException ex) { logger.error("WNS: ", ex); @@ -230,66 +191,6 @@ public class Notifications implements AutoCloseable { logger.info("ExternalComponent on notifications destroyed"); } - String getWnsAccessToken() throws IOException, IllegalStateException { - if (TextUtils.isEmpty(wns_application_sip)) { - throw new IllegalStateException("'wns_application_sip' is not initialized"); - } - if (TextUtils.isEmpty(wns_client_secret)) { - throw new IllegalStateException("'wns_client_secret' is not initialized"); - } - HttpClient client = HttpClientBuilder.create().build(); - String url = "https://login.live.com/accesstoken.srf"; - List<NameValuePair> formParams = new ArrayList<>(); - formParams.add(new BasicNameValuePair("grant_type", "client_credentials")); - formParams.add(new BasicNameValuePair("client_id", wns_application_sip)); - formParams.add(new BasicNameValuePair("client_secret", wns_client_secret)); - formParams.add(new BasicNameValuePair("scope", "notify.windows.com")); - UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); - HttpPost httppost = new HttpPost(url); - httppost.setEntity(entity); - HttpResponse response = client.execute(httppost); - int statusCode = response.getStatusLine().getStatusCode(); - String responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8); - if (statusCode != HttpStatus.SC_OK) { - MPNSError error = mapper.readValue(responseContent, MPNSError.class); - throw new IOException(error.getError() + ": " + error.getErrorDescription()); - } - MPNSToken token = mapper.readValue(responseContent, MPNSToken.class); - if (token.getTokenType().length() >= 1) { - token.setTokenType(Character.toUpperCase(token.getTokenType().charAt(0)) + token.getTokenType().substring(1)); - } - return token.getTokenType() + " " + token.getAccessToken(); - } - - void sendWNS(final String wnsToken, final String url, final String xml) throws IOException { - HttpClient client = HttpClientBuilder.create().build(); - StringEntity entity = new StringEntity(xml, Consts.UTF_8); - HttpPost httpPost = new HttpPost(url); - httpPost.setHeader("Content-Type", "text/xml"); - httpPost.setHeader("Authorization", wnsToken); - httpPost.setHeader("X-WNS-Type", "wns/toast"); - httpPost.setEntity(entity); - HttpResponse response = client.execute(httpPost); - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode != HttpStatus.SC_OK) { - if (statusCode == HttpStatus.SC_GONE) { - // expired - logger.info("{} is scheduled to remove", url); - addInvalidMPNSToken(url); - } else { - String headersContent = stringifyWnsHttpHeaders(response.getAllHeaders()); - throw new IOException(headersContent); - } - } - } - - static String stringifyWnsHttpHeaders(final Header[] allHeaders) { - return Arrays.stream(allHeaders) - .filter(x -> x.getName().startsWith("X-WNS-") || x.getName().startsWith("WWW-")) - .map(x -> x.getName() + ": " + x.getValue()) - .collect(Collectors.joining("\n")); - } - public void addInvalidGCMToken(String token) { synchronized (invalidGCMTokens) { invalidGCMTokens.add(token); @@ -318,4 +219,15 @@ public class Notifications implements AutoCloseable { invalidMPNSTokens.clear(); } } + + @Override + public void invalidToken(String type, String token) { + switch (type) { + case "mpns": + addInvalidMPNSToken(token); + break; + default: + break; + } + } } diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/APNSConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/APNSConfiguration.java new file mode 100644 index 00000000..a4959497 --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/configuration/APNSConfiguration.java @@ -0,0 +1,23 @@ +package com.juick.components.configuration; + +import com.notnoop.apns.APNS; +import com.notnoop.apns.ApnsService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Created by vital on 28.03.2017. + */ +@Configuration +public class APNSConfiguration { + @Value("${ios_pkcs12_file}") + private String pkcs12File; + @Value("${ios_pkcs12_password}") + private String pkcs12secret; + @Bean + public ApnsService apns() { + return APNS.newService().withCert(pkcs12File, pkcs12secret) + .withProductionDestination().build(); + } +} diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/GCMConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/GCMConfiguration.java new file mode 100644 index 00000000..96c7716f --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/configuration/GCMConfiguration.java @@ -0,0 +1,21 @@ +package com.juick.components.configuration; + +import com.google.android.gcm.server.Endpoint; +import com.google.android.gcm.server.Sender; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Created by vital on 29.03.2017. + */ +@Configuration +public class GCMConfiguration { + @Value("${gcm_key}") + private String gcmKey; + + @Bean + public Sender GCMSender() { + return new Sender(gcmKey, Endpoint.GCM); + } +} diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/MPNSConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/MPNSConfiguration.java new file mode 100644 index 00000000..f849b159 --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/configuration/MPNSConfiguration.java @@ -0,0 +1,16 @@ +package com.juick.components.configuration; + +import com.juick.components.MPNSClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Created by vital on 29.03.2017. + */ +@Configuration +public class MPNSConfiguration { + @Bean + public MPNSClient mpnsClient() { + return new MPNSClient(); + } +} diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java index 9c76b889..d6b4fa1e 100644 --- a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java +++ b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java @@ -1,21 +1,21 @@ package com.juick.components.configuration; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.juick.components.CleanUp; import com.juick.components.Notifications; -import com.notnoop.apns.APNS; -import com.notnoop.apns.ApnsService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import org.springframework.core.env.Environment; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.InterceptingClientHttpRequestFactory; import org.springframework.http.client.support.BasicAuthorizationInterceptor; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; -import javax.inject.Inject; import java.util.Collections; import java.util.List; @@ -27,16 +27,16 @@ import java.util.List; @PropertySource("classpath:juick.conf") @ComponentScan(basePackages = {"com.juick.components.service"}) public class NotificationsAppConfiguration { - @Inject - private Environment env; + @Value("${api_user:'juick'}") + private String apiUser; + @Value("${api_password:'secret'}") + private String apiSecret; @Bean public RestTemplate rest() { RestTemplate rest = new RestTemplate(); List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList( - new BasicAuthorizationInterceptor( - env.getProperty("api_user", "juick"), - env.getProperty("api_password", "secret"))); + new BasicAuthorizationInterceptor(apiUser, apiSecret)); rest.setRequestFactory(new InterceptingClientHttpRequestFactory(rest.getRequestFactory(), interceptors)); return rest; @@ -44,14 +44,19 @@ public class NotificationsAppConfiguration { @Bean public Notifications push() { - return new Notifications(env, rest()); + return new Notifications(); } @Bean - public ApnsService apns() { - return APNS.newService().withCert(env.getProperty("ios_pkcs12_file"), env.getProperty("ios_pkcs12_password")) - .withProductionDestination().build(); + public ObjectMapper jsonMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); + mapper.registerModule(new Jdk8Module()); + return mapper; } + @Bean public CleanUp cleanUp() { return new CleanUp(); diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsInitializer.java b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsInitializer.java index 617f8686..2421f0e9 100644 --- a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsInitializer.java +++ b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsInitializer.java @@ -13,7 +13,10 @@ public class NotificationsInitializer extends AbstractAnnotationConfigDispatcher @Override protected Class<?>[] getRootConfigClasses() { - return new Class<?>[]{NotificationsAppConfiguration.class}; + return new Class<?>[]{ + NotificationsAppConfiguration.class, APNSConfiguration.class, MPNSConfiguration.class, + GCMConfiguration.class, XMPPConfiguration.class + }; } @Override diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsMvcConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsMvcConfiguration.java index f92b6a19..3d828832 100644 --- a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsMvcConfiguration.java +++ b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsMvcConfiguration.java @@ -1,14 +1,13 @@ package com.juick.components.configuration; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import javax.inject.Inject; import java.util.List; /** @@ -17,16 +16,12 @@ import java.util.List; @Configuration @ComponentScan(basePackages = {"com.juick.components.controllers"}) public class NotificationsMvcConfiguration extends WebMvcConfigurationSupport { + @Inject + ObjectMapper jsonMapper; @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { - Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() - .serializationInclusion(JsonInclude.Include.NON_DEFAULT) - .serializationInclusion(JsonInclude.Include.NON_NULL) - .serializationInclusion(JsonInclude.Include.NON_ABSENT) - .serializationInclusion(JsonInclude.Include.NON_EMPTY); - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(builder.build()); - converter.getObjectMapper().registerModule(new Jdk8Module()); + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(jsonMapper); converters.add(converter); super.configureMessageConverters(converters); } diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/XMPPConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/XMPPConfiguration.java new file mode 100644 index 00000000..22b3d561 --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/configuration/XMPPConfiguration.java @@ -0,0 +1,31 @@ +package com.juick.components.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.extensions.component.accept.ExternalComponent; + +/** + * Created by vital on 29.03.2017. + */ +@Configuration +public class XMPPConfiguration { + @Value("${push_jid}") + private String pushJid; + @Value("${xmpp_host:'localhost'}") + private String xmppHost; + @Value("${xmpp_port:5347}") + private int xmppPort; + @Value("${push_xmpp_password:'secret'}") + private String xmppPushPassword; + + @Bean + public ExternalComponent xmpp() { + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.Message.class)) + .build(); + return ExternalComponent.create(pushJid, xmppPushPassword, configuration, xmppHost, xmppPort); + } +} |