Skip to content

Commit b4e2169

Browse files
authored
Merge pull request #1 from rogin/kelaompachai/main
query OIE's GH releases API
2 parents 9b3f78d + 472cd52 commit b4e2169

File tree

5 files changed

+1504
-159
lines changed

5 files changed

+1504
-159
lines changed

server/build.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,7 @@
12691269
<copy todir="${test_classes}">
12701270
<fileset dir="${test}">
12711271
<include name="**/*.xml" />
1272+
<include name="**/*.json" />
12721273
</fileset>
12731274
</copy>
12741275

server/src/com/mirth/connect/client/core/ConnectServiceUtil.java

Lines changed: 155 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99

1010
package com.mirth.connect.client.core;
1111

12+
import java.io.IOException;
1213
import java.net.URI;
1314
import java.nio.charset.Charset;
14-
import java.util.ArrayList;
1515
import java.util.Arrays;
16+
import java.util.Collections;
1617
import java.util.List;
1718
import java.util.Map;
1819
import 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

2026
import org.apache.commons.httpclient.HttpStatus;
2127
import org.apache.commons.io.IOUtils;
@@ -26,9 +32,7 @@
2632
import org.apache.http.client.config.RequestConfig;
2733
import org.apache.http.client.entity.UrlEncodedFormEntity;
2834
import org.apache.http.client.methods.CloseableHttpResponse;
29-
3035
import org.apache.http.client.methods.HttpGet;
31-
3236
import org.apache.http.client.methods.HttpPost;
3337
import org.apache.http.client.protocol.HttpClientContext;
3438
import org.apache.http.client.utils.HttpClientUtils;
@@ -45,6 +49,7 @@
4549
import org.apache.http.util.EntityUtils;
4650

4751
import com.fasterxml.jackson.core.type.TypeReference;
52+
import com.fasterxml.jackson.databind.JsonMappingException;
4853
import com.fasterxml.jackson.databind.JsonNode;
4954
import com.fasterxml.jackson.databind.ObjectMapper;
5055
import 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

Comments
 (0)