Skip to content

Commit 346e1a4

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/branding-connectservice
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
2 parents 306a86a + 59889f6 commit 346e1a4

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,11 +46,13 @@
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;
4554
import com.mirth.connect.client.core.BrandingConstants;
55+
import com.github.zafarkhaja.semver.Version;
4656
import com.mirth.connect.model.User;
4757
import com.mirth.connect.model.converters.ObjectXMLSerializer;
4858
import com.mirth.connect.model.notification.Notification;
@@ -52,9 +62,7 @@ public class ConnectServiceUtil {
5262
private final static String URL_CONNECT_SERVER = BrandingConstants.CONNECT_SERVER_URL;
5363
private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet";
5464
private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet";
55-
private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet";
56-
private static String NOTIFICATION_GET = "getNotifications";
57-
private static String NOTIFICATION_COUNT_GET = "getNotificationCount";
65+
private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases";
5866
private final static int TIMEOUT = 10000;
5967
public final static Integer MILLIS_PER_DAY = 86400000;
6068

@@ -67,7 +75,7 @@ public static void registerUser(String serverId, String mirthVersion, User user,
6775

6876
HttpPost post = new HttpPost();
6977
post.setURI(URI.create(URL_CONNECT_SERVER + URL_REGISTRATION_SERVLET));
70-
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
78+
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), StandardCharsets.UTF_8));
7179
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
7280

7381
try {
@@ -88,112 +96,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
8896
}
8997
}
9098

99+
/**
100+
* Query an external source for new releases. Return notifications for each release that's greater than the current version.
101+
*
102+
* @param serverId
103+
* @param mirthVersion
104+
* @param extensionVersions
105+
* @param protocols
106+
* @param cipherSuites
107+
* @return a non-null list
108+
* @throws Exception should anything fail dealing with the web request and the handling of its response
109+
*/
91110
public static List<Notification> getNotifications(String serverId, String mirthVersion, Map<String, String> extensionVersions, String[] protocols, String[] cipherSuites) throws Exception {
92-
CloseableHttpClient client = null;
93-
HttpPost post = new HttpPost();
94-
CloseableHttpResponse response = null;
95-
96-
List<Notification> allNotifications = new ArrayList<Notification>();
111+
List<Notification> validNotifications = Collections.emptyList();
112+
Optional<Version> parsedMirthVersion = Version.tryParse(mirthVersion);
113+
if (!parsedMirthVersion.isPresent()) {
114+
return validNotifications;
115+
}
97116

117+
CloseableHttpClient httpClient = null;
118+
CloseableHttpResponse httpResponse = null;
119+
HttpEntity responseEntity = null;
98120
try {
99-
ObjectMapper mapper = new ObjectMapper();
100-
String extensionVersionsJson = mapper.writeValueAsString(extensionVersions);
101-
NameValuePair[] params = { new BasicNameValuePair("op", NOTIFICATION_GET),
102-
new BasicNameValuePair("serverId", serverId),
103-
new BasicNameValuePair("version", mirthVersion),
104-
new BasicNameValuePair("extensionVersions", extensionVersionsJson) };
105121
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
122+
HttpClientContext getContext = HttpClientContext.create();
123+
getContext.setRequestConfig(requestConfig);
124+
httpClient = getClient(protocols, cipherSuites);
125+
HttpGet httpget = new HttpGet(URL_NOTIFICATIONS);
126+
// adding header makes github send back body as rendered html for the "body_html" field
127+
httpget.addHeader("Accept", "application/vnd.github.html+json");
128+
httpResponse = httpClient.execute(httpget, getContext);
106129

107-
post.setURI(URI.create(URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET));
108-
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
130+
int statusCode = httpResponse.getStatusLine().getStatusCode();
131+
if (statusCode == HttpStatus.SC_OK) {
132+
responseEntity = httpResponse.getEntity();
109133

110-
HttpClientContext postContext = HttpClientContext.create();
111-
postContext.setRequestConfig(requestConfig);
112-
client = getClient(protocols, cipherSuites);
113-
response = client.execute(post, postContext);
114-
StatusLine statusLine = response.getStatusLine();
115-
int statusCode = statusLine.getStatusCode();
116-
if ((statusCode == HttpStatus.SC_OK)) {
117-
HttpEntity responseEntity = response.getEntity();
118-
Charset responseCharset = null;
119-
try {
120-
responseCharset = ContentType.getOrDefault(responseEntity).getCharset();
121-
} catch (Exception e) {
122-
responseCharset = ContentType.TEXT_PLAIN.getCharset();
123-
}
124-
125-
String responseContent = IOUtils.toString(responseEntity.getContent(), responseCharset).trim();
126-
JsonNode rootNode = mapper.readTree(responseContent);
127-
128-
for (JsonNode childNode : rootNode) {
129-
Notification notification = new Notification();
130-
notification.setId(childNode.get("id").asInt());
131-
notification.setName(childNode.get("name").asText());
132-
notification.setDate(childNode.get("date").asText());
133-
notification.setContent(childNode.get("content").asText());
134-
allNotifications.add(notification);
135-
}
134+
validNotifications = toJsonStream(responseEntity)
135+
.filter(dropOlderThan(parsedMirthVersion.get()))
136+
.map(ConnectServiceUtil::toNotification)
137+
.collect(Collectors.toList());
136138
} else {
137139
throw new ClientException("Status code: " + statusCode);
138140
}
139-
} catch (Exception e) {
140-
throw e;
141141
} finally {
142-
HttpClientUtils.closeQuietly(response);
143-
HttpClientUtils.closeQuietly(client);
142+
EntityUtils.consumeQuietly(responseEntity);
143+
HttpClientUtils.closeQuietly(httpResponse);
144+
HttpClientUtils.closeQuietly(httpClient);
144145
}
145146

146-
return allNotifications;
147+
return validNotifications;
147148
}
148149

149-
public static int getNotificationCount(String serverId, String mirthVersion, Map<String, String> extensionVersions, Set<Integer> archivedNotifications, String[] protocols, String[] cipherSuites) {
150-
CloseableHttpClient client = null;
151-
HttpPost post = new HttpPost();
152-
CloseableHttpResponse response = null;
150+
/**
151+
* Creates a predicate to filter JSON nodes representing releases.
152+
* The predicate returns true if the "tag_name" of the JSON node, when parsed as a semantic version,
153+
* is newer than the provided reference version.
154+
*
155+
* @param version The reference {@link Version} to compare against
156+
* @return A {@link Predicate} for {@link JsonNode}s that evaluates to true for newer versions.
157+
*/
158+
protected static Predicate<JsonNode> dropOlderThan(Version version) {
159+
return node -> Version.tryParse(node.get("tag_name").asText())
160+
.filter(version::isLowerThan)
161+
.isPresent();
162+
}
153163

154-
int notificationCount = 0;
164+
/**
165+
* Converts an HTTP response entity containing a JSON array into a stream of {@link JsonNode} objects.
166+
* Each element in the JSON array becomes a {@link JsonNode} in the stream.
167+
*
168+
* @param responseEntity The {@link HttpEntity} from the HTTP response, expected to contain a JSON array.
169+
* @return A stream of {@link JsonNode} objects.
170+
* @throws IOException If an I/O error occurs while reading the response entity.
171+
* @throws JsonMappingException If an error occurs during JSON parsing.
172+
*/
173+
protected static Stream<JsonNode> toJsonStream(HttpEntity responseEntity) throws IOException, JsonMappingException {
174+
JsonNode rootNode = new ObjectMapper().readTree(new InputStreamReader(responseEntity.getContent(), getCharset(responseEntity)));
175+
return StreamSupport.stream(rootNode.spliterator(), false);
176+
}
155177

178+
/**
179+
* Try pulling a charset from the given response. Default to UTF-8.
180+
*
181+
* @param responseEntity
182+
* @return
183+
*/
184+
protected static Charset getCharset(HttpEntity responseEntity) {
185+
Charset charset = StandardCharsets.UTF_8;
156186
try {
157-
ObjectMapper mapper = new ObjectMapper();
158-
String extensionVersionsJson = mapper.writeValueAsString(extensionVersions);
159-
NameValuePair[] params = { new BasicNameValuePair("op", NOTIFICATION_COUNT_GET),
160-
new BasicNameValuePair("serverId", serverId),
161-
new BasicNameValuePair("version", mirthVersion),
162-
new BasicNameValuePair("extensionVersions", extensionVersionsJson) };
163-
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
187+
ContentType ct = ContentType.get(responseEntity);
188+
Charset fromHeader = ct.getCharset();
189+
if (fromHeader != null) {
190+
charset = fromHeader;
191+
}
192+
} catch (Exception ignore) {}
193+
return charset;
194+
}
164195

165-
post.setURI(URI.create(URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET));
166-
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
196+
/**
197+
* Given a JSON node with HTML content from a GitHub release feed, convert it to a notification.
198+
*
199+
* @param node
200+
* @return a notification
201+
*/
202+
protected static Notification toNotification(JsonNode node) {
203+
Notification notification = new Notification();
204+
notification.setId(node.get("id").asInt());
205+
notification.setName(node.get("name").asText());
206+
notification.setDate(node.get("published_at").asText());
207+
notification.setContent(node.get("body_html").asText());
208+
return notification;
209+
}
167210

168-
HttpClientContext postContext = HttpClientContext.create();
169-
postContext.setRequestConfig(requestConfig);
170-
client = getClient(protocols, cipherSuites);
171-
response = client.execute(post, postContext);
172-
StatusLine statusLine = response.getStatusLine();
173-
int statusCode = statusLine.getStatusCode();
174-
if ((statusCode == HttpStatus.SC_OK)) {
175-
HttpEntity responseEntity = response.getEntity();
176-
Charset responseCharset = null;
177-
try {
178-
responseCharset = ContentType.getOrDefault(responseEntity).getCharset();
179-
} catch (Exception e) {
180-
responseCharset = ContentType.TEXT_PLAIN.getCharset();
181-
}
182-
183-
List<Integer> notificationIds = mapper.readValue(IOUtils.toString(responseEntity.getContent(), responseCharset).trim(), new TypeReference<List<Integer>>() {
184-
});
185-
for (int id : notificationIds) {
186-
if (!archivedNotifications.contains(id)) {
187-
notificationCount++;
188-
}
189-
}
190-
}
191-
} catch (Exception e) {
192-
} finally {
193-
HttpClientUtils.closeQuietly(response);
194-
HttpClientUtils.closeQuietly(client);
211+
public static int getNotificationCount(String serverId, String mirthVersion, Map<String, String> extensionVersions, Set<Integer> archivedNotifications, String[] protocols, String[] cipherSuites) {
212+
Long notificationCount = 0L;
213+
try {
214+
notificationCount = getNotifications(serverId, mirthVersion, extensionVersions, protocols, cipherSuites)
215+
.stream()
216+
.map(Notification::getId)
217+
.filter(id -> !archivedNotifications.contains(id))
218+
.count();
219+
} catch (Exception ignore) {
220+
System.err.println("Failed to get notification count, defaulting to zero: " + ignore);
195221
}
196-
return notificationCount;
222+
return notificationCount.intValue();
197223
}
198224

199225
public static boolean sendStatistics(String serverId, String mirthVersion, boolean server, String data, String[] protocols, String[] cipherSuites) {
@@ -213,7 +239,7 @@ public static boolean sendStatistics(String serverId, String mirthVersion, boole
213239
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
214240

215241
post.setURI(URI.create(URL_CONNECT_SERVER + URL_USAGE_SERVLET));
216-
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
242+
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), StandardCharsets.UTF_8));
217243

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

0 commit comments

Comments
 (0)