99
1010package com .mirth .connect .client .core ;
1111
12+ import java .io .IOException ;
13+ import java .io .InputStreamReader ;
1214import java .net .URI ;
1315import java .nio .charset .Charset ;
14- import java .util . ArrayList ;
16+ import java .nio . charset . StandardCharsets ;
1517import java .util .Arrays ;
18+ import java .util .Collections ;
1619import java .util .List ;
1720import java .util .Map ;
21+ import java .util .Optional ;
1822import java .util .Set ;
23+ import java .util .function .Predicate ;
24+ import java .util .stream .Collectors ;
25+ import java .util .stream .Stream ;
26+ import java .util .stream .StreamSupport ;
1927
2028import org .apache .commons .httpclient .HttpStatus ;
21- import org .apache .commons .io .IOUtils ;
2229import org .apache .http .HttpEntity ;
2330import org .apache .http .NameValuePair ;
2431import org .apache .http .StatusLine ;
2532import org .apache .http .client .config .RequestConfig ;
2633import org .apache .http .client .entity .UrlEncodedFormEntity ;
2734import org .apache .http .client .methods .CloseableHttpResponse ;
35+ import org .apache .http .client .methods .HttpGet ;
2836import org .apache .http .client .methods .HttpPost ;
2937import org .apache .http .client .protocol .HttpClientContext ;
3038import org .apache .http .client .utils .HttpClientUtils ;
3846import org .apache .http .impl .client .HttpClients ;
3947import org .apache .http .impl .conn .BasicHttpClientConnectionManager ;
4048import org .apache .http .message .BasicNameValuePair ;
49+ import org .apache .http .util .EntityUtils ;
4150
42- import com .fasterxml .jackson .core . type . TypeReference ;
51+ import com .fasterxml .jackson .databind . JsonMappingException ;
4352import com .fasterxml .jackson .databind .JsonNode ;
4453import com .fasterxml .jackson .databind .ObjectMapper ;
4554import com .mirth .connect .client .core .BrandingConstants ;
55+ import com .github .zafarkhaja .semver .Version ;
4656import com .mirth .connect .model .User ;
4757import com .mirth .connect .model .converters .ObjectXMLSerializer ;
4858import com .mirth .connect .model .notification .Notification ;
@@ -52,9 +62,7 @@ public class ConnectServiceUtil {
5262 private final static String URL_CONNECT_SERVER = BrandingConstants .CONNECT_SERVER_URL ;
5363 private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet" ;
5464 private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet" ;
55- private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet" ;
56- private static String NOTIFICATION_GET = "getNotifications" ;
57- private static String NOTIFICATION_COUNT_GET = "getNotificationCount" ;
65+ private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases" ;
5866 private final static int TIMEOUT = 10000 ;
5967 public final static Integer MILLIS_PER_DAY = 86400000 ;
6068
@@ -67,7 +75,7 @@ public static void registerUser(String serverId, String mirthVersion, User user,
6775
6876 HttpPost post = new HttpPost ();
6977 post .setURI (URI .create (URL_CONNECT_SERVER + URL_REGISTRATION_SERVLET ));
70- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset . forName ( "UTF-8" ) ));
78+ post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), StandardCharsets . UTF_8 ));
7179 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
7280
7381 try {
@@ -88,112 +96,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
8896 }
8997 }
9098
99+ /**
100+ * Query an external source for new releases. Return notifications for each release that's greater than the current version.
101+ *
102+ * @param serverId
103+ * @param mirthVersion
104+ * @param extensionVersions
105+ * @param protocols
106+ * @param cipherSuites
107+ * @return a non-null list
108+ * @throws Exception should anything fail dealing with the web request and the handling of its response
109+ */
91110 public static List <Notification > getNotifications (String serverId , String mirthVersion , Map <String , String > extensionVersions , String [] protocols , String [] cipherSuites ) throws Exception {
92- CloseableHttpClient client = null ;
93- HttpPost post = new HttpPost ( );
94- CloseableHttpResponse response = null ;
95-
96- List < Notification > allNotifications = new ArrayList < Notification >();
111+ List < Notification > validNotifications = Collections . emptyList () ;
112+ Optional < Version > parsedMirthVersion = Version . tryParse ( mirthVersion );
113+ if (! parsedMirthVersion . isPresent ()) {
114+ return validNotifications ;
115+ }
97116
117+ CloseableHttpClient httpClient = null ;
118+ CloseableHttpResponse httpResponse = null ;
119+ HttpEntity responseEntity = null ;
98120 try {
99- ObjectMapper mapper = new ObjectMapper ();
100- String extensionVersionsJson = mapper .writeValueAsString (extensionVersions );
101- NameValuePair [] params = { new BasicNameValuePair ("op" , NOTIFICATION_GET ),
102- new BasicNameValuePair ("serverId" , serverId ),
103- new BasicNameValuePair ("version" , mirthVersion ),
104- new BasicNameValuePair ("extensionVersions" , extensionVersionsJson ) };
105121 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
122+ HttpClientContext getContext = HttpClientContext .create ();
123+ getContext .setRequestConfig (requestConfig );
124+ httpClient = getClient (protocols , cipherSuites );
125+ HttpGet httpget = new HttpGet (URL_NOTIFICATIONS );
126+ // adding header makes github send back body as rendered html for the "body_html" field
127+ httpget .addHeader ("Accept" , "application/vnd.github.html+json" );
128+ httpResponse = httpClient .execute (httpget , getContext );
106129
107- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
108- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
130+ int statusCode = httpResponse .getStatusLine ().getStatusCode ();
131+ if (statusCode == HttpStatus .SC_OK ) {
132+ responseEntity = httpResponse .getEntity ();
109133
110- HttpClientContext postContext = HttpClientContext .create ();
111- postContext .setRequestConfig (requestConfig );
112- client = getClient (protocols , cipherSuites );
113- response = client .execute (post , postContext );
114- StatusLine statusLine = response .getStatusLine ();
115- int statusCode = statusLine .getStatusCode ();
116- if ((statusCode == HttpStatus .SC_OK )) {
117- HttpEntity responseEntity = response .getEntity ();
118- Charset responseCharset = null ;
119- try {
120- responseCharset = ContentType .getOrDefault (responseEntity ).getCharset ();
121- } catch (Exception e ) {
122- responseCharset = ContentType .TEXT_PLAIN .getCharset ();
123- }
124-
125- String responseContent = IOUtils .toString (responseEntity .getContent (), responseCharset ).trim ();
126- JsonNode rootNode = mapper .readTree (responseContent );
127-
128- for (JsonNode childNode : rootNode ) {
129- Notification notification = new Notification ();
130- notification .setId (childNode .get ("id" ).asInt ());
131- notification .setName (childNode .get ("name" ).asText ());
132- notification .setDate (childNode .get ("date" ).asText ());
133- notification .setContent (childNode .get ("content" ).asText ());
134- allNotifications .add (notification );
135- }
134+ validNotifications = toJsonStream (responseEntity )
135+ .filter (dropOlderThan (parsedMirthVersion .get ()))
136+ .map (ConnectServiceUtil ::toNotification )
137+ .collect (Collectors .toList ());
136138 } else {
137139 throw new ClientException ("Status code: " + statusCode );
138140 }
139- } catch (Exception e ) {
140- throw e ;
141141 } finally {
142- HttpClientUtils .closeQuietly (response );
143- HttpClientUtils .closeQuietly (client );
142+ EntityUtils .consumeQuietly (responseEntity );
143+ HttpClientUtils .closeQuietly (httpResponse );
144+ HttpClientUtils .closeQuietly (httpClient );
144145 }
145146
146- return allNotifications ;
147+ return validNotifications ;
147148 }
148149
149- public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
150- CloseableHttpClient client = null ;
151- HttpPost post = new HttpPost ();
152- CloseableHttpResponse response = null ;
150+ /**
151+ * Creates a predicate to filter JSON nodes representing releases.
152+ * The predicate returns true if the "tag_name" of the JSON node, when parsed as a semantic version,
153+ * is newer than the provided reference version.
154+ *
155+ * @param version The reference {@link Version} to compare against
156+ * @return A {@link Predicate} for {@link JsonNode}s that evaluates to true for newer versions.
157+ */
158+ protected static Predicate <JsonNode > dropOlderThan (Version version ) {
159+ return node -> Version .tryParse (node .get ("tag_name" ).asText ())
160+ .filter (version ::isLowerThan )
161+ .isPresent ();
162+ }
153163
154- int notificationCount = 0 ;
164+ /**
165+ * Converts an HTTP response entity containing a JSON array into a stream of {@link JsonNode} objects.
166+ * Each element in the JSON array becomes a {@link JsonNode} in the stream.
167+ *
168+ * @param responseEntity The {@link HttpEntity} from the HTTP response, expected to contain a JSON array.
169+ * @return A stream of {@link JsonNode} objects.
170+ * @throws IOException If an I/O error occurs while reading the response entity.
171+ * @throws JsonMappingException If an error occurs during JSON parsing.
172+ */
173+ protected static Stream <JsonNode > toJsonStream (HttpEntity responseEntity ) throws IOException , JsonMappingException {
174+ JsonNode rootNode = new ObjectMapper ().readTree (new InputStreamReader (responseEntity .getContent (), getCharset (responseEntity )));
175+ return StreamSupport .stream (rootNode .spliterator (), false );
176+ }
155177
178+ /**
179+ * Try pulling a charset from the given response. Default to UTF-8.
180+ *
181+ * @param responseEntity
182+ * @return
183+ */
184+ protected static Charset getCharset (HttpEntity responseEntity ) {
185+ Charset charset = StandardCharsets .UTF_8 ;
156186 try {
157- ObjectMapper mapper = new ObjectMapper ();
158- String extensionVersionsJson = mapper .writeValueAsString (extensionVersions );
159- NameValuePair [] params = { new BasicNameValuePair ("op" , NOTIFICATION_COUNT_GET ),
160- new BasicNameValuePair ("serverId" , serverId ),
161- new BasicNameValuePair ("version" , mirthVersion ),
162- new BasicNameValuePair ("extensionVersions" , extensionVersionsJson ) };
163- RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
187+ ContentType ct = ContentType .get (responseEntity );
188+ Charset fromHeader = ct .getCharset ();
189+ if (fromHeader != null ) {
190+ charset = fromHeader ;
191+ }
192+ } catch (Exception ignore ) {}
193+ return charset ;
194+ }
164195
165- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
166- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
196+ /**
197+ * Given a JSON node with HTML content from a GitHub release feed, convert it to a notification.
198+ *
199+ * @param node
200+ * @return a notification
201+ */
202+ protected static Notification toNotification (JsonNode node ) {
203+ Notification notification = new Notification ();
204+ notification .setId (node .get ("id" ).asInt ());
205+ notification .setName (node .get ("name" ).asText ());
206+ notification .setDate (node .get ("published_at" ).asText ());
207+ notification .setContent (node .get ("body_html" ).asText ());
208+ return notification ;
209+ }
167210
168- HttpClientContext postContext = HttpClientContext .create ();
169- postContext .setRequestConfig (requestConfig );
170- client = getClient (protocols , cipherSuites );
171- response = client .execute (post , postContext );
172- StatusLine statusLine = response .getStatusLine ();
173- int statusCode = statusLine .getStatusCode ();
174- if ((statusCode == HttpStatus .SC_OK )) {
175- HttpEntity responseEntity = response .getEntity ();
176- Charset responseCharset = null ;
177- try {
178- responseCharset = ContentType .getOrDefault (responseEntity ).getCharset ();
179- } catch (Exception e ) {
180- responseCharset = ContentType .TEXT_PLAIN .getCharset ();
181- }
182-
183- List <Integer > notificationIds = mapper .readValue (IOUtils .toString (responseEntity .getContent (), responseCharset ).trim (), new TypeReference <List <Integer >>() {
184- });
185- for (int id : notificationIds ) {
186- if (!archivedNotifications .contains (id )) {
187- notificationCount ++;
188- }
189- }
190- }
191- } catch (Exception e ) {
192- } finally {
193- HttpClientUtils .closeQuietly (response );
194- HttpClientUtils .closeQuietly (client );
211+ public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
212+ Long notificationCount = 0L ;
213+ try {
214+ notificationCount = getNotifications (serverId , mirthVersion , extensionVersions , protocols , cipherSuites )
215+ .stream ()
216+ .map (Notification ::getId )
217+ .filter (id -> !archivedNotifications .contains (id ))
218+ .count ();
219+ } catch (Exception ignore ) {
220+ System .err .println ("Failed to get notification count, defaulting to zero: " + ignore );
195221 }
196- return notificationCount ;
222+ return notificationCount . intValue () ;
197223 }
198224
199225 public static boolean sendStatistics (String serverId , String mirthVersion , boolean server , String data , String [] protocols , String [] cipherSuites ) {
@@ -213,7 +239,7 @@ public static boolean sendStatistics(String serverId, String mirthVersion, boole
213239 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
214240
215241 post .setURI (URI .create (URL_CONNECT_SERVER + URL_USAGE_SERVLET ));
216- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset . forName ( "UTF-8" ) ));
242+ post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), StandardCharsets . UTF_8 ));
217243
218244 try {
219245 HttpClientContext postContext = HttpClientContext .create ();
0 commit comments