Skip to content

Commit 087ebf1

Browse files
kelaompachairoginjonbartelstonygermano
authored and
kayyagari
committed
Update client notifications to pull from github Release API
- Added library java-semver-0.10.2.jar for version parsing and comparison. - Refactored to use streams and added tests - No longer calls a self-hosted service, so active extensions are no longer provided as part of the request. Co-authored-by: Richard Ogin <rogin@users.noreply.github.com> Co-authored-by: Jon Bartels <jon.bartels@teladochealth.com> Co-authored-by: Tony Germano <tony@germano.name> Signed-off-by: kelaompachai <141376761+kelaompachai@users.noreply.github.com> Signed-off-by: Richard Ogin <rogin@users.noreply.github.com> Signed-off-by: Tony Germano <tony@germano.name> Issue: #24
1 parent 7247f5b commit 087ebf1

File tree

9 files changed

+1498
-98
lines changed

9 files changed

+1498
-98
lines changed

client/.classpath

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,6 @@
213213
<classpathentry kind="lib" path="lib/libphonenumber-8.12.50.jar"/>
214214
<classpathentry kind="lib" path="lib/commons-pool2-2.3.jar"/>
215215
<classpathentry kind="lib" path="lib/xml-apis-1.4.01.jar"/>
216+
<classpathentry kind="lib" path="lib/java-semver-0.10.2.jar"/>
216217
<classpathentry kind="output" path="bin"/>
217218
</classpath>

client/lib/java-semver-0.10.2.jar

50.8 KB
Binary file not shown.

server/.classpath

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
<classpathentry kind="src" path="conf"/>
66
<classpathentry kind="src" path="dbconf"/>
77
<classpathentry kind="src" path="build"/>
8+
<classpathentry kind="lib" path="lib/java-semver-0.10.2.jar"/>
89
<classpathentry kind="lib" path="lib/extensions/dimse/jai_imageio.jar"/>
910
<classpathentry kind="lib" path="lib/extensions/doc/flying-saucer-core-9.0.1.jar"/>
1011
<classpathentry kind="lib" path="lib/extensions/doc/flying-saucer-pdf-9.0.1.jar"/>
11-
<classpathentry kind="lib" path="lib/extensions/file/jcifs-ng-2.1.10.jar"/>
12+
<classpathentry kind="lib" path="lib/extensions/file/jcifs-ng-2.1.10.jar"/>
1213
<classpathentry kind="lib" path="lib/extensions/doc/itext-2.1.7.jar"/>
1314
<classpathentry kind="lib" path="lib/extensions/doc/itext-rtf-2.1.7.jar"/>
1415
<classpathentry kind="lib" path="lib/commons/commons-beanutils-1.9.4.jar"/>
@@ -190,9 +191,9 @@
190191
<classpathentry kind="lib" path="/Donkey/lib/guava/j2objc-annotations-1.3.jar"/>
191192
<classpathentry kind="lib" path="/Donkey/lib/guava/jsr305-3.0.2.jar"/>
192193
<classpathentry kind="lib" path="/Donkey/lib/guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"/>
193-
<classpathentry kind="lib" path="lib/bcpkix-jdk18on-1.78.1.jar"/>
194-
<classpathentry kind="lib" path="lib/bcprov-jdk18on-1.78.1.jar"/>
195-
<classpathentry kind="lib" path="lib/bcutil-jdk18on-1.78.1.jar"/>
194+
<classpathentry kind="lib" path="lib/bcpkix-jdk18on-1.78.1.jar"/>
195+
<classpathentry kind="lib" path="lib/bcprov-jdk18on-1.78.1.jar"/>
196+
<classpathentry kind="lib" path="lib/bcutil-jdk18on-1.78.1.jar"/>
196197
<classpathentry kind="lib" path="lib/commons/commons-vfs2-2.9.0.jar">
197198
<attributes>
198199
<attribute name="javadoc_location" value="http://commons.apache.org/proper/commons-vfs/apidocs"/>

server/build.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,7 @@
12741274
<copy todir="${test_classes}">
12751275
<fileset dir="${test}">
12761276
<include name="**/*.xml" />
1277+
<include name="**/*.json" />
12771278
</fileset>
12781279
</copy>
12791280

server/lib/java-semver-0.10.2.jar

50.8 KB
Binary file not shown.

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

Lines changed: 120 additions & 94 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;
13+
import java.io.InputStreamReader;
1214
import java.net.URI;
1315
import java.nio.charset.Charset;
14-
import java.util.ArrayList;
16+
import java.nio.charset.StandardCharsets;
1517
import java.util.Arrays;
18+
import java.util.Collections;
1619
import java.util.List;
1720
import java.util.Map;
21+
import java.util.Optional;
1822
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;
1927

2028
import org.apache.commons.httpclient.HttpStatus;
21-
import org.apache.commons.io.IOUtils;
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,10 +46,12 @@
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

42-
import com.fasterxml.jackson.core.type.TypeReference;
51+
import com.fasterxml.jackson.databind.JsonMappingException;
4352
import com.fasterxml.jackson.databind.JsonNode;
4453
import com.fasterxml.jackson.databind.ObjectMapper;
54+
import com.github.zafarkhaja.semver.Version;
4555
import com.mirth.connect.model.User;
4656
import com.mirth.connect.model.converters.ObjectXMLSerializer;
4757
import com.mirth.connect.model.notification.Notification;
@@ -51,9 +61,7 @@ public class ConnectServiceUtil {
5161
private final static String URL_CONNECT_SERVER = "https://connect.mirthcorp.com";
5262
private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet";
5363
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";
5765
private final static int TIMEOUT = 10000;
5866
public final static Integer MILLIS_PER_DAY = 86400000;
5967

@@ -66,7 +74,7 @@ public static void registerUser(String serverId, String mirthVersion, User user,
6674

6775
HttpPost post = new HttpPost();
6876
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));
7078
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
7179

7280
try {
@@ -87,112 +95,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
8795
}
8896
}
8997

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+
*/
90109
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+
}
96115

116+
CloseableHttpClient httpClient = null;
117+
CloseableHttpResponse httpResponse = null;
118+
HttpEntity responseEntity = null;
97119
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) };
104120
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);
105128

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();
108132

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());
135137
} else {
136138
throw new ClientException("Status code: " + statusCode);
137139
}
138-
} catch (Exception e) {
139-
throw e;
140140
} finally {
141-
HttpClientUtils.closeQuietly(response);
142-
HttpClientUtils.closeQuietly(client);
141+
EntityUtils.consumeQuietly(responseEntity);
142+
HttpClientUtils.closeQuietly(httpResponse);
143+
HttpClientUtils.closeQuietly(httpClient);
143144
}
144145

145-
return allNotifications;
146+
return validNotifications;
146147
}
147148

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+
}
152162

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+
}
154176

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;
155185
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+
}
163194

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+
}
166209

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);
194220
}
195-
return notificationCount;
221+
return notificationCount.intValue();
196222
}
197223

198224
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
212238
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
213239

214240
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));
216242

217243
try {
218244
HttpClientContext postContext = HttpClientContext.create();

0 commit comments

Comments
 (0)