99
1010package com .mirth .connect .client .core ;
1111
12+ import java .io .IOException ;
1213import java .net .URI ;
1314import java .nio .charset .Charset ;
14- import java .util .ArrayList ;
1515import java .util .Arrays ;
16+ import java .util .Collections ;
1617import java .util .List ;
1718import java .util .Map ;
1819import java .util .Set ;
20+ import java .util .Spliterator ;
21+ import java .util .Spliterators ;
22+ import java .util .stream .Collectors ;
23+ import java .util .stream .Stream ;
24+ import java .util .stream .StreamSupport ;
1925
2026import org .apache .commons .httpclient .HttpStatus ;
2127import org .apache .commons .io .IOUtils ;
28+ import org .apache .commons .text .StringEscapeUtils ;
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
4251import com .fasterxml .jackson .core .type .TypeReference ;
52+ import com .fasterxml .jackson .databind .JsonMappingException ;
4353import com .fasterxml .jackson .databind .JsonNode ;
4454import com .fasterxml .jackson .databind .ObjectMapper ;
4555import com .mirth .connect .model .User ;
@@ -52,7 +62,7 @@ public class ConnectServiceUtil {
5262 private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet" ;
5363 private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet" ;
5464 private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet" ;
55- private static String NOTIFICATION_GET = "getNotifications " ;
65+ private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases " ;
5666 private static String NOTIFICATION_COUNT_GET = "getNotificationCount" ;
5767 private final static int TIMEOUT = 10000 ;
5868 public final static Integer MILLIS_PER_DAY = 86400000 ;
@@ -87,62 +97,181 @@ public static void registerUser(String serverId, String mirthVersion, User user,
8797 }
8898 }
8999
100+ /**
101+ * Query an external source for new releases. Return notifications for each release that's greater than the current version.
102+ *
103+ * @param serverId
104+ * @param mirthVersion
105+ * @param extensionVersions
106+ * @param protocols
107+ * @param cipherSuites
108+ * @return a non-null list
109+ * @throws Exception should anything fail dealing with the web request and the handling of its response
110+ */
90111 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 ;
112+ List <Notification > validNotifications = Collections .emptyList ();
94113
95- List <Notification > allNotifications = new ArrayList <Notification >();
114+ CloseableHttpClient httpClient = null ;
115+ CloseableHttpResponse httpResponse = null ;
116+ HttpEntity responseEntity = null ;
96117
97118 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 ) };
104- RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
105-
106- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
107- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
108-
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- }
119+ HttpClientContext getContext = HttpClientContext .create ();
120+ getContext .setRequestConfig (createRequestConfig ());
121+ httpClient = getClient (protocols , cipherSuites );
122+ httpResponse = httpClient .execute (new HttpGet (URL_NOTIFICATIONS ), getContext );
123123
124- String responseContent = IOUtils .toString (responseEntity .getContent (), responseCharset ).trim ();
125- JsonNode rootNode = mapper .readTree (responseContent );
124+ int statusCode = httpResponse .getStatusLine ().getStatusCode ();
125+ if (statusCode == HttpStatus .SC_OK ) {
126+ responseEntity = httpResponse .getEntity ();
126127
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- }
128+ var newerOnly = filterForNewerVersions (toJsonStream (responseEntity ), mirthVersion );
129+ validNotifications = newerOnly .map (node -> toNotification (node )).collect (Collectors .toList ());
135130 } else {
136131 throw new ClientException ("Status code: " + statusCode );
137132 }
138- } catch (Exception e ) {
139- throw e ;
140133 } finally {
141- HttpClientUtils .closeQuietly (response );
142- HttpClientUtils .closeQuietly (client );
134+ EntityUtils .consumeQuietly (responseEntity );
135+ HttpClientUtils .closeQuietly (httpResponse );
136+ HttpClientUtils .closeQuietly (httpClient );
143137 }
144138
145- return allNotifications ;
139+ return validNotifications ;
140+ }
141+
142+ /**
143+ * Create a request config with appropriate network timeouts.
144+ *
145+ * @return
146+ */
147+ private static RequestConfig createRequestConfig () {
148+ return RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
149+ }
150+
151+ /**
152+ * Filter the stream to only new versions, then create a notification for each.
153+ *
154+ * @param nodes
155+ * @param currentVersion
156+ * @return a non-null List
157+ */
158+ protected static Stream <JsonNode > filterForNewerVersions (Stream <JsonNode > nodes , String currentVersion ) {
159+ int [] curVersion = toVersionArray (currentVersion );
160+ return nodes .takeWhile (node -> isCurrentOlderThan (curVersion , node .get ("tag_name" ).asText ()));
161+ }
162+
163+ /**
164+ * Convert a JSON response to a stream of {@link JsonNode}.
165+ *
166+ * @param responseEntity
167+ * @return a stream
168+ * @throws IOException
169+ * @throws JsonMappingException
170+ */
171+ protected static Stream <JsonNode > toJsonStream (HttpEntity responseEntity ) throws IOException , JsonMappingException {
172+ String responseContent = getResponseContent (responseEntity );
173+
174+ JsonNode rootNode = new ObjectMapper ().readTree (responseContent );
175+
176+ //convert to stream to simplify
177+ return StreamSupport .stream (
178+ Spliterators .spliteratorUnknownSize (rootNode .elements (), Spliterator .ORDERED ),false );
179+ }
180+
181+ /**
182+ * Extract the reponse content. It attempts to use the charset found in the response, if any.
183+ *
184+ * @param responseEntity
185+ * @return
186+ * @throws IOException
187+ */
188+ protected static String getResponseContent (HttpEntity responseEntity ) throws IOException {
189+ return IOUtils .toString (responseEntity .getContent (), getCharset (responseEntity ));
190+ }
191+
192+ /**
193+ * Try pulling a charset from the given response. Default to {@link ContentType#TEXT_PLAIN}.
194+ *
195+ * @param responseEntity
196+ * @return
197+ */
198+ protected static Charset getCharset (HttpEntity responseEntity ) {
199+ Charset charset = ContentType .TEXT_PLAIN .getCharset ();
200+ try {
201+ charset = ContentType .getOrDefault (responseEntity ).getCharset ();
202+ } catch (Exception ignore ) {}
203+ return charset ;
204+ }
205+
206+ /**
207+ * Split a given version string into an int[].
208+ *
209+ * @param version
210+ * @return
211+ */
212+ protected static int [] toVersionArray (String version ) {
213+ return Arrays .stream (version .split ("[\\ .]" )).mapToInt (Integer ::parseInt ).toArray ();
214+ }
215+
216+ /**
217+ * Compare the current version with another. If current is greater than or equal, this returns false.
218+ *
219+ * @param currentVersion
220+ * @param anotherVersion
221+ * @return true if the current version is less than than the other
222+ */
223+ protected static boolean isCurrentOlderThan (int [] currentVersion , String anotherVersion ) {
224+ return Arrays .compare (currentVersion , toVersionArray (anotherVersion )) < 0 ;
225+ }
226+
227+ /**
228+ * Given a JSON node from a GitHub release feed, convert it to a HTML notification.
229+ *
230+ * @param node
231+ * @return a notification with HTML content
232+ */
233+ protected static Notification toNotification (JsonNode node ) {
234+ Notification notification = new Notification ();
235+
236+ notification .setId (node .get ("id" ).asInt ());
237+ notification .setName (node .get ("name" ).asText ());
238+ notification .setDate (node .get ("published_at" ).asText ());
239+
240+ // create the content html
241+ String content = toNotificationContent (node );
242+ notification .setContent (content );
243+
244+ return notification ;
245+ }
246+
247+ /**
248+ * Create the HTML content for a notification.
249+ *
250+ * @param node
251+ * @return an HTML String
252+ */
253+ protected static String toNotificationContent (JsonNode node ) {
254+ String escapedName = StringEscapeUtils .escapeHtml4 (node .get ("name" ).asText ());
255+ String escapedReleaseUrl = StringEscapeUtils .escapeHtml4 (node .get ("html_url" ).asText ());
256+
257+ StringBuilder content = new StringBuilder (256 );
258+
259+ // create header with name
260+ content .append ("<h2>" )
261+ .append (escapedName )
262+ .append ("</h2>" );
263+
264+ // announce there is a new version
265+ content .append ("<h3>" )
266+ .append ("A new version of Open Integration Engine is available!" )
267+ .append ("</h3>" );
268+
269+ // create a link to the release webpage
270+ content .append ("<a href=\" " + escapedReleaseUrl + "\" >" )
271+ .append ("Release Webpage" )
272+ .append ("</a>" );
273+
274+ return content .toString ();
146275 }
147276
148277 public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
0 commit comments