Skip to content

Commit 90c82b1

Browse files
kelaompachairogin
authored andcommitted
query OIE's GH releases API
refactor and include tests Signed-off-by: Richard Ogin <rogin@users.noreply.github.com>
1 parent 8175b47 commit 90c82b1

5 files changed

Lines changed: 1524 additions & 46 deletions

File tree

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: 175 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,30 @@
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;
28+
import org.apache.commons.text.StringEscapeUtils;
2229
import org.apache.http.HttpEntity;
2330
import org.apache.http.NameValuePair;
2431
import org.apache.http.StatusLine;
2532
import org.apache.http.client.config.RequestConfig;
2633
import org.apache.http.client.entity.UrlEncodedFormEntity;
2734
import org.apache.http.client.methods.CloseableHttpResponse;
35+
import org.apache.http.client.methods.HttpGet;
2836
import org.apache.http.client.methods.HttpPost;
2937
import org.apache.http.client.protocol.HttpClientContext;
3038
import org.apache.http.client.utils.HttpClientUtils;
@@ -38,8 +46,10 @@
3846
import org.apache.http.impl.client.HttpClients;
3947
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
4048
import org.apache.http.message.BasicNameValuePair;
49+
import org.apache.http.util.EntityUtils;
4150

4251
import com.fasterxml.jackson.core.type.TypeReference;
52+
import com.fasterxml.jackson.databind.JsonMappingException;
4353
import com.fasterxml.jackson.databind.JsonNode;
4454
import com.fasterxml.jackson.databind.ObjectMapper;
4555
import 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

Comments
 (0)