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 ;
2632import org .apache .http .client .config .RequestConfig ;
2733import org .apache .http .client .entity .UrlEncodedFormEntity ;
2834import org .apache .http .client .methods .CloseableHttpResponse ;
29-
3035import org .apache .http .client .methods .HttpGet ;
31-
3236import org .apache .http .client .methods .HttpPost ;
3337import org .apache .http .client .protocol .HttpClientContext ;
3438import org .apache .http .client .utils .HttpClientUtils ;
4549import org .apache .http .util .EntityUtils ;
4650
4751import com .fasterxml .jackson .core .type .TypeReference ;
52+ import com .fasterxml .jackson .databind .JsonMappingException ;
4853import com .fasterxml .jackson .databind .JsonNode ;
4954import com .fasterxml .jackson .databind .ObjectMapper ;
5055import com .mirth .connect .model .User ;
@@ -57,7 +62,7 @@ public class ConnectServiceUtil {
5762 private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet" ;
5863 private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet" ;
5964 private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet" ;
60- private static String NOTIFICATION_GET = "getNotifications " ;
65+ private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases " ;
6166 private static String NOTIFICATION_COUNT_GET = "getNotificationCount" ;
6267 private final static int TIMEOUT = 10000 ;
6368 public final static Integer MILLIS_PER_DAY = 86400000 ;
@@ -92,190 +97,181 @@ public static void registerUser(String serverId, String mirthVersion, User user,
9297 }
9398 }
9499
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+ */
95111 public static List <Notification > getNotifications (String serverId , String mirthVersion , Map <String , String > extensionVersions , String [] protocols , String [] cipherSuites ) throws Exception {
96- List <Notification > allNotifications = new ArrayList <Notification >();
97-
98- System .out .println ("Entered getNotifications" );
112+ List <Notification > validNotifications = Collections .emptyList ();
99113
100- // make an api call
101-
102- // CloseableHttpClient client = HttpClients.createDefault();
103- CloseableHttpClient client = null ;
104- CloseableHttpResponse response = null ;
114+ CloseableHttpClient httpClient = null ;
115+ CloseableHttpResponse httpResponse = null ;
116+ HttpEntity responseEntity = null ;
105117
106- HttpGet get = new HttpGet ("https://api.github.com/repos/nextgenhealthcare/connect/releases" );
107-
108118 try {
109- ObjectMapper mapper = new ObjectMapper ();
110-
111- client = getClient (protocols , cipherSuites );
112- response = client .execute (get );
113- System .out .println (response .getStatusLine ());
114- HttpEntity entity1 = response .getEntity ();
115- String responseContent = IOUtils .toString (entity1 .getContent (), "utf8" );
116-
117-
118- JsonNode rootNode = mapper .readTree (responseContent );
119-
120- String [] mirthVersionStringArr = mirthVersion .split ("[\\ .]" );
121- int [] mirthVersionIntArr = new int [mirthVersionStringArr .length ];
122- for (int i = 0 ; i < mirthVersionStringArr .length ; i ++) {
123- mirthVersionIntArr [i ] = Integer .parseInt (mirthVersionStringArr [i ]);
124- }
125-
126-
127-
128-
129- for (JsonNode childNode : rootNode ) {
130-
131-
132- if (!checkNotificationVersion (mirthVersionIntArr , childNode .get ("tag_name" ).asText ())) break ;
133-
134- Notification notification = new Notification ();
135-
136- notification .setId (childNode .get ("id" ).asInt ());
137- notification .setName (childNode .get ("name" ).asText ());
138- notification .setDate (childNode .get ("published_at" ).asText ());
119+ HttpClientContext getContext = HttpClientContext .create ();
120+ getContext .setRequestConfig (createRequestConfig ());
121+ httpClient = getClient (protocols , cipherSuites );
122+ httpResponse = httpClient .execute (new HttpGet (URL_NOTIFICATIONS ), getContext );
139123
140- // create the content html
141- String content = createNotificationHtml ( childNode . get ( "name" ). asText (), childNode . get ( "html_url" ). asText ());
142- notification . setContent ( content );
124+ int statusCode = httpResponse . getStatusLine (). getStatusCode ();
125+ if ( statusCode == HttpStatus . SC_OK ) {
126+ responseEntity = httpResponse . getEntity ( );
143127
144- allNotifications .add (notification );
128+ var newerOnly = filterForNewerVersions (toJsonStream (responseEntity ), mirthVersion );
129+ validNotifications = newerOnly .map (node -> toNotification (node )).collect (Collectors .toList ());
130+ } else {
131+ throw new ClientException ("Status code: " + statusCode );
145132 }
146-
147- // and ensure it is fully consumed
148- EntityUtils .consume (entity1 );
149-
150-
151- } catch (Exception e ) {
152- System .out .print (e );
153- e .printStackTrace ();
154- throw e ;
155133 } finally {
156- HttpClientUtils .closeQuietly (client );
157- HttpClientUtils .closeQuietly (response );
134+ EntityUtils .consumeQuietly (responseEntity );
135+ HttpClientUtils .closeQuietly (httpResponse );
136+ HttpClientUtils .closeQuietly (httpClient );
158137 }
159138
160- return allNotifications ;
139+ return validNotifications ;
161140 }
162141
163- private static String createNotificationHtml (String name , String releaseUrl ) {
164- // System.out.println("Entered createNotificationHtml");
165-
166- // should arguments be sanitized?
167- // apache string escape utils??
168-
169- String escapedName = StringEscapeUtils .escapeHtml4 (name );
170- String escapedReleaseUrl = StringEscapeUtils .escapeHtml4 (releaseUrl );
171-
172- StringBuilder content = new StringBuilder ();
173-
174- // create header with name
175- content .append ("<h2>" );
176- content .append (escapedName );
177- content .append ("</h2>" );
178-
179- // announce there is a new version with a p element
180- content .append ("<h3>" );
181- content .append ("A new version of Mirth Connect is available!" );
182- content .append ("</h3>" );
183-
184- // create a link to the release webpage
185- content .append ("<a href=\" " + escapedReleaseUrl + "\" >" );
186- content .append ("Release Webpage" );
187- content .append ("</a>" );
188-
189- return content .toString ();
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 ();
190149 }
191150
192- private static boolean checkNotificationVersion (int [] mirthVersionArr , String notificationVersion ) {
193- // checks release notifications to see whether they are unnecessary because user has already fully updated
194-
195-
196- String [] notificationVersionStringArr = notificationVersion .split ("[\\ .]" );
197- int [] notificationVersionIntArr = new int [notificationVersionStringArr .length ];
198-
199-
200- for (int i = 0 ; i < notificationVersionStringArr .length ; i += 1 ) {
201- notificationVersionIntArr [i ] = Integer .parseInt (notificationVersionStringArr [i ]);
202- }
203-
204- int lengthOfShorter = Math .min (mirthVersionArr .length , notificationVersionIntArr .length );
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+ }
205162
206- for (int i = 0 ; i < lengthOfShorter ; i ++) {
207- if (mirthVersionArr [i ] > notificationVersionIntArr [i ]) return false ;
208- }
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+ }
209180
210- // if the user's mirth version is a subpatch of the patch this notification is for, return false
211- if (mirthVersionArr .length > notificationVersionIntArr .length ) return false ;
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+ }
212191
213- // if the user is using the version that this notification is for, return false
214- if (mirthVersionArr [mirthVersionArr .length - 1 ] == notificationVersionIntArr [notificationVersionIntArr .length - 1 ]) return false ;
215-
216- return true ;
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 ;
217204 }
218205
219- public static List <Notification > getNotifications2 (String serverId , String mirthVersion , Map <String , String > extensionVersions , String [] protocols , String [] cipherSuites ) throws Exception {
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+ }
220215
221- System .out .println ("Entered getNotifications2" );
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+ }
222226
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 ());
223239
224- CloseableHttpClient client = null ;
225- HttpPost post = new HttpPost ( );
226- CloseableHttpResponse response = null ;
240+ // create the content html
241+ String content = toNotificationContent ( node );
242+ notification . setContent ( content ) ;
227243
228- List <Notification > allNotifications = new ArrayList <Notification >();
244+ return notification ;
245+ }
229246
230- try {
231- ObjectMapper mapper = new ObjectMapper ();
232- String extensionVersionsJson = mapper .writeValueAsString (extensionVersions );
233- NameValuePair [] params = { new BasicNameValuePair ("op" , NOTIFICATION_GET ),
234- new BasicNameValuePair ("serverId" , serverId ),
235- new BasicNameValuePair ("version" , mirthVersion ),
236- new BasicNameValuePair ("extensionVersions" , extensionVersionsJson ) };
237- RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
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 ());
238256
239- post .setURI (URI .create (URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET ));
240- post .setEntity (new UrlEncodedFormEntity (Arrays .asList (params ), Charset .forName ("UTF-8" )));
257+ StringBuilder content = new StringBuilder (256 );
241258
242- HttpClientContext postContext = HttpClientContext .create ();
243- postContext .setRequestConfig (requestConfig );
244- client = getClient (protocols , cipherSuites );
245- response = client .execute (post , postContext );
246- StatusLine statusLine = response .getStatusLine ();
247- int statusCode = statusLine .getStatusCode ();
248- if ((statusCode == HttpStatus .SC_OK )) {
249- HttpEntity responseEntity = response .getEntity ();
250- Charset responseCharset = null ;
251- try {
252- responseCharset = ContentType .getOrDefault (responseEntity ).getCharset ();
253- } catch (Exception e ) {
254- responseCharset = ContentType .TEXT_PLAIN .getCharset ();
255- }
259+ // create header with name
260+ content .append ("<h2>" )
261+ .append (escapedName )
262+ .append ("</h2>" );
256263
257- String responseContent = IOUtils .toString (responseEntity .getContent (), responseCharset ).trim ();
258- JsonNode rootNode = mapper .readTree (responseContent );
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>" );
259268
260- for (JsonNode childNode : rootNode ) {
261- Notification notification = new Notification ();
262- notification .setId (childNode .get ("id" ).asInt ());
263- notification .setName (childNode .get ("name" ).asText ());
264- notification .setDate (childNode .get ("date" ).asText ());
265- notification .setContent (childNode .get ("content" ).asText ());
266- allNotifications .add (notification );
267- }
268- } else {
269- throw new ClientException ("Status code: " + statusCode );
270- }
271- } catch (Exception e ) {
272- throw e ;
273- } finally {
274- HttpClientUtils .closeQuietly (response );
275- HttpClientUtils .closeQuietly (client );
276- }
269+ // create a link to the release webpage
270+ content .append ("<a href=\" " + escapedReleaseUrl + "\" >" )
271+ .append ("Release Webpage" )
272+ .append ("</a>" );
277273
278- return allNotifications ;
274+ return content . toString () ;
279275 }
280276
281277 public static int getNotificationCount (String serverId , String mirthVersion , Map <String , String > extensionVersions , Set <Integer > archivedNotifications , String [] protocols , String [] cipherSuites ) {
0 commit comments