9
9
10
10
package com .mirth .connect .client .core ;
11
11
12
+ import java .io .IOException ;
13
+ import java .io .InputStreamReader ;
12
14
import java .net .URI ;
13
15
import java .nio .charset .Charset ;
14
- import java .util . ArrayList ;
16
+ import java .nio . charset . StandardCharsets ;
15
17
import java .util .Arrays ;
18
+ import java .util .Collections ;
16
19
import java .util .List ;
17
20
import java .util .Map ;
21
+ import java .util .Optional ;
18
22
import 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 ;
19
27
20
28
import org .apache .commons .httpclient .HttpStatus ;
21
- import org .apache .commons .io .IOUtils ;
22
29
import org .apache .http .HttpEntity ;
23
30
import org .apache .http .NameValuePair ;
24
31
import org .apache .http .StatusLine ;
25
32
import org .apache .http .client .config .RequestConfig ;
26
33
import org .apache .http .client .entity .UrlEncodedFormEntity ;
27
34
import org .apache .http .client .methods .CloseableHttpResponse ;
35
+ import org .apache .http .client .methods .HttpGet ;
28
36
import org .apache .http .client .methods .HttpPost ;
29
37
import org .apache .http .client .protocol .HttpClientContext ;
30
38
import org .apache .http .client .utils .HttpClientUtils ;
38
46
import org .apache .http .impl .client .HttpClients ;
39
47
import org .apache .http .impl .conn .BasicHttpClientConnectionManager ;
40
48
import org .apache .http .message .BasicNameValuePair ;
49
+ import org .apache .http .util .EntityUtils ;
41
50
42
- import com .fasterxml .jackson .core . type . TypeReference ;
51
+ import com .fasterxml .jackson .databind . JsonMappingException ;
43
52
import com .fasterxml .jackson .databind .JsonNode ;
44
53
import com .fasterxml .jackson .databind .ObjectMapper ;
54
+ import com .github .zafarkhaja .semver .Version ;
45
55
import com .mirth .connect .model .User ;
46
56
import com .mirth .connect .model .converters .ObjectXMLSerializer ;
47
57
import com .mirth .connect .model .notification .Notification ;
@@ -51,9 +61,7 @@ public class ConnectServiceUtil {
51
61
private final static String URL_CONNECT_SERVER = "https://connect.mirthcorp.com" ;
52
62
private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet" ;
53
63
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" ;
57
65
private final static int TIMEOUT = 10000 ;
58
66
public final static Integer MILLIS_PER_DAY = 86400000 ;
59
67
@@ -66,7 +74,7 @@ public static void registerUser(String serverId, String mirthVersion, User user,
66
74
67
75
HttpPost post = new HttpPost ();
68
76
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 ));
70
78
RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
71
79
72
80
try {
@@ -87,112 +95,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
87
95
}
88
96
}
89
97
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
+ */
90
109
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
+ }
96
115
116
+ CloseableHttpClient httpClient = null ;
117
+ CloseableHttpResponse httpResponse = null ;
118
+ HttpEntity responseEntity = null ;
97
119
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
120
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 );
105
128
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 ();
108
132
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 ());
135
137
} else {
136
138
throw new ClientException ("Status code: " + statusCode );
137
139
}
138
- } catch (Exception e ) {
139
- throw e ;
140
140
} finally {
141
- HttpClientUtils .closeQuietly (response );
142
- HttpClientUtils .closeQuietly (client );
141
+ EntityUtils .consumeQuietly (responseEntity );
142
+ HttpClientUtils .closeQuietly (httpResponse );
143
+ HttpClientUtils .closeQuietly (httpClient );
143
144
}
144
145
145
- return allNotifications ;
146
+ return validNotifications ;
146
147
}
147
148
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
+ }
152
162
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
+ }
154
176
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 ;
155
185
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
+ }
163
194
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
+ }
166
209
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 );
194
220
}
195
- return notificationCount ;
221
+ return notificationCount . intValue () ;
196
222
}
197
223
198
224
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
212
238
RequestConfig requestConfig = RequestConfig .custom ().setConnectTimeout (TIMEOUT ).setConnectionRequestTimeout (TIMEOUT ).setSocketTimeout (TIMEOUT ).build ();
213
239
214
240
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 ));
216
242
217
243
try {
218
244
HttpClientContext postContext = HttpClientContext .create ();
0 commit comments