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 ;
54+ import com .github .zafarkhaja .semver .Version ;
4555import com .mirth .connect .model .User ;
4656import com .mirth .connect .model .converters .ObjectXMLSerializer ;
4757import com .mirth .connect .model .notification .Notification ;
@@ -51,9 +61,7 @@ public class ConnectServiceUtil {
5161 private final static String URL_CONNECT_SERVER = "https://connect.mirthcorp.com" ;
5262 private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet" ;
5363 private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet" ;
54- private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet" ;
55- private static String NOTIFICATION_GET = "getNotifications" ;
56- private static String NOTIFICATION_COUNT_GET = "getNotificationCount" ;
64+ private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases" ;
5765 private final static int TIMEOUT = 10000 ;
5866 public final static Integer MILLIS_PER_DAY = 86400000 ;
5967
@@ -66,7 +74,7 @@ public static void registerUser(String serverId, String mirthVersion, User user,
6674
6775 HttpPost post = new HttpPost ();
6876 post .setURI (URI .create (URL_CONNECT_SERVER + URL_REGISTRATION_SERVLET ));
69- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset . forName ( "UTF-8" ) ));
77+ post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), StandardCharsets . UTF_8 ));
7078 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
7179
7280 try {
@@ -87,112 +95,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
8795 }
8896 }
8997
98+ /**
99+ * Query an external source for new releases. Return notifications for each release that's greater than the current version.
100+ *
101+ * @param serverId
102+ * @param mirthVersion
103+ * @param extensionVersions
104+ * @param protocols
105+ * @param cipherSuites
106+ * @return a non-null list
107+ * @throws Exception should anything fail dealing with the web request and the handling of its response
108+ */
90109 public static List <Notification > getNotifications (String serverId , String mirthVersion , Map <String , String > extensionVersions , String [] protocols , String [] cipherSuites ) throws Exception {
91- CloseableHttpClient client = null ;
92- HttpPost post = new HttpPost ( );
93- CloseableHttpResponse response = null ;
94-
95- List < Notification > allNotifications = new ArrayList < Notification >();
110+ List < Notification > validNotifications = Collections . emptyList () ;
111+ Optional < Version > parsedMirthVersion = Version . tryParse ( mirthVersion );
112+ if (! parsedMirthVersion . isPresent ()) {
113+ return validNotifications ;
114+ }
96115
116+ CloseableHttpClient httpClient = null ;
117+ CloseableHttpResponse httpResponse = null ;
118+ HttpEntity responseEntity = null ;
97119 try {
98- ObjectMapper mapper = new ObjectMapper ();
99- String extensionVersionsJson = mapper .writeValueAsString (extensionVersions );
100- NameValuePair [] params = { new BasicNameValuePair ("op" , NOTIFICATION_GET ),
101- new BasicNameValuePair ("serverId" , serverId ),
102- new BasicNameValuePair ("version" , mirthVersion ),
103- new BasicNameValuePair ("extensionVersions" , extensionVersionsJson ) };
104120 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
121+ HttpClientContext getContext = HttpClientContext .create ();
122+ getContext .setRequestConfig (requestConfig );
123+ httpClient = getClient (protocols , cipherSuites );
124+ HttpGet httpget = new HttpGet (URL_NOTIFICATIONS );
125+ // adding header makes github send back body as rendered html for the "body_html" field
126+ httpget .addHeader ("Accept" , "application/vnd.github.html+json" );
127+ httpResponse = httpClient .execute (httpget , getContext );
105128
106- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
107- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
129+ int statusCode = httpResponse .getStatusLine ().getStatusCode ();
130+ if (statusCode == HttpStatus .SC_OK ) {
131+ responseEntity = httpResponse .getEntity ();
108132
109- HttpClientContext postContext = HttpClientContext .create ();
110- postContext .setRequestConfig (requestConfig );
111- client = getClient (protocols , cipherSuites );
112- response = client .execute (post , postContext );
113- StatusLine statusLine = response .getStatusLine ();
114- int statusCode = statusLine .getStatusCode ();
115- if ((statusCode == HttpStatus .SC_OK )) {
116- HttpEntity responseEntity = response .getEntity ();
117- Charset responseCharset = null ;
118- try {
119- responseCharset = ContentType .getOrDefault (responseEntity ).getCharset ();
120- } catch (Exception e ) {
121- responseCharset = ContentType .TEXT_PLAIN .getCharset ();
122- }
123-
124- String responseContent = IOUtils .toString (responseEntity .getContent (), responseCharset ).trim ();
125- JsonNode rootNode = mapper .readTree (responseContent );
126-
127- for (JsonNode childNode : rootNode ) {
128- Notification notification = new Notification ();
129- notification .setId (childNode .get ("id" ).asInt ());
130- notification .setName (childNode .get ("name" ).asText ());
131- notification .setDate (childNode .get ("date" ).asText ());
132- notification .setContent (childNode .get ("content" ).asText ());
133- allNotifications .add (notification );
134- }
133+ validNotifications = toJsonStream (responseEntity )
134+ .filter (dropOlderThan (parsedMirthVersion .get ()))
135+ .map (ConnectServiceUtil ::toNotification )
136+ .collect (Collectors .toList ());
135137 } else {
136138 throw new ClientException ("Status code: " + statusCode );
137139 }
138- } catch (Exception e ) {
139- throw e ;
140140 } finally {
141- HttpClientUtils .closeQuietly (response );
142- HttpClientUtils .closeQuietly (client );
141+ EntityUtils .consumeQuietly (responseEntity );
142+ HttpClientUtils .closeQuietly (httpResponse );
143+ HttpClientUtils .closeQuietly (httpClient );
143144 }
144145
145- return allNotifications ;
146+ return validNotifications ;
146147 }
147148
148- public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
149- CloseableHttpClient client = null ;
150- HttpPost post = new HttpPost ();
151- CloseableHttpResponse response = null ;
149+ /**
150+ * Creates a predicate to filter JSON nodes representing releases.
151+ * The predicate returns true if the "tag_name" of the JSON node, when parsed as a semantic version,
152+ * is newer than the provided reference version.
153+ *
154+ * @param version The reference {@link Version} to compare against
155+ * @return A {@link Predicate} for {@link JsonNode}s that evaluates to true for newer versions.
156+ */
157+ protected static Predicate <JsonNode > dropOlderThan (Version version ) {
158+ return node -> Version .tryParse (node .get ("tag_name" ).asText ())
159+ .filter (version ::isLowerThan )
160+ .isPresent ();
161+ }
152162
153- int notificationCount = 0 ;
163+ /**
164+ * Converts an HTTP response entity containing a JSON array into a stream of {@link JsonNode} objects.
165+ * Each element in the JSON array becomes a {@link JsonNode} in the stream.
166+ *
167+ * @param responseEntity The {@link HttpEntity} from the HTTP response, expected to contain a JSON array.
168+ * @return A stream of {@link JsonNode} objects.
169+ * @throws IOException If an I/O error occurs while reading the response entity.
170+ * @throws JsonMappingException If an error occurs during JSON parsing.
171+ */
172+ protected static Stream <JsonNode > toJsonStream (HttpEntity responseEntity ) throws IOException , JsonMappingException {
173+ JsonNode rootNode = new ObjectMapper ().readTree (new InputStreamReader (responseEntity .getContent (), getCharset (responseEntity )));
174+ return StreamSupport .stream (rootNode .spliterator (), false );
175+ }
154176
177+ /**
178+ * Try pulling a charset from the given response. Default to UTF-8.
179+ *
180+ * @param responseEntity
181+ * @return
182+ */
183+ protected static Charset getCharset (HttpEntity responseEntity ) {
184+ Charset charset = StandardCharsets .UTF_8 ;
155185 try {
156- ObjectMapper mapper = new ObjectMapper ();
157- String extensionVersionsJson = mapper .writeValueAsString (extensionVersions );
158- NameValuePair [] params = { new BasicNameValuePair ("op" , NOTIFICATION_COUNT_GET ),
159- new BasicNameValuePair ("serverId" , serverId ),
160- new BasicNameValuePair ("version" , mirthVersion ),
161- new BasicNameValuePair ("extensionVersions" , extensionVersionsJson ) };
162- RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
186+ ContentType ct = ContentType .get (responseEntity );
187+ Charset fromHeader = ct .getCharset ();
188+ if (fromHeader != null ) {
189+ charset = fromHeader ;
190+ }
191+ } catch (Exception ignore ) {}
192+ return charset ;
193+ }
163194
164- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
165- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
195+ /**
196+ * Given a JSON node with HTML content from a GitHub release feed, convert it to a notification.
197+ *
198+ * @param node
199+ * @return a notification
200+ */
201+ protected static Notification toNotification (JsonNode node ) {
202+ Notification notification = new Notification ();
203+ notification .setId (node .get ("id" ).asInt ());
204+ notification .setName (node .get ("name" ).asText ());
205+ notification .setDate (node .get ("published_at" ).asText ());
206+ notification .setContent (node .get ("body_html" ).asText ());
207+ return notification ;
208+ }
166209
167- HttpClientContext postContext = HttpClientContext .create ();
168- postContext .setRequestConfig (requestConfig );
169- client = getClient (protocols , cipherSuites );
170- response = client .execute (post , postContext );
171- StatusLine statusLine = response .getStatusLine ();
172- int statusCode = statusLine .getStatusCode ();
173- if ((statusCode == HttpStatus .SC_OK )) {
174- HttpEntity responseEntity = response .getEntity ();
175- Charset responseCharset = null ;
176- try {
177- responseCharset = ContentType .getOrDefault (responseEntity ).getCharset ();
178- } catch (Exception e ) {
179- responseCharset = ContentType .TEXT_PLAIN .getCharset ();
180- }
181-
182- List <Integer > notificationIds = mapper .readValue (IOUtils .toString (responseEntity .getContent (), responseCharset ).trim (), new TypeReference <List <Integer >>() {
183- });
184- for (int id : notificationIds ) {
185- if (!archivedNotifications .contains (id )) {
186- notificationCount ++;
187- }
188- }
189- }
190- } catch (Exception e ) {
191- } finally {
192- HttpClientUtils .closeQuietly (response );
193- HttpClientUtils .closeQuietly (client );
210+ public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
211+ Long notificationCount = 0L ;
212+ try {
213+ notificationCount = getNotifications (serverId , mirthVersion , extensionVersions , protocols , cipherSuites )
214+ .stream ()
215+ .map (Notification ::getId )
216+ .filter (id -> !archivedNotifications .contains (id ))
217+ .count ();
218+ } catch (Exception ignore ) {
219+ System .err .println ("Failed to get notification count, defaulting to zero: " + ignore );
194220 }
195- return notificationCount ;
221+ return notificationCount . intValue () ;
196222 }
197223
198224 public static boolean sendStatistics (String serverId , String mirthVersion , boolean server , String data , String [] protocols , String [] cipherSuites ) {
@@ -212,7 +238,7 @@ public static boolean sendStatistics(String serverId, String mirthVersion, boole
212238 RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
213239
214240 post .setURI (URI .create (URL_CONNECT_SERVER + URL_USAGE_SERVLET ));
215- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset . forName ( "UTF-8" ) ));
241+ post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), StandardCharsets . UTF_8 ));
216242
217243 try {
218244 HttpClientContext postContext = HttpClientContext .create ();
0 commit comments