Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,6 @@
<classpathentry kind="lib" path="lib/libphonenumber-8.12.50.jar"/>
<classpathentry kind="lib" path="lib/commons-pool2-2.3.jar"/>
<classpathentry kind="lib" path="lib/xml-apis-1.4.01.jar"/>
<classpathentry kind="lib" path="lib/java-semver-0.10.2.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
Binary file added client/lib/java-semver-0.10.2.jar
Binary file not shown.
9 changes: 5 additions & 4 deletions server/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
<classpathentry kind="src" path="conf"/>
<classpathentry kind="src" path="dbconf"/>
<classpathentry kind="src" path="build"/>
<classpathentry kind="lib" path="lib/java-semver-0.10.2.jar"/>
<classpathentry kind="lib" path="lib/extensions/dimse/jai_imageio.jar"/>
<classpathentry kind="lib" path="lib/extensions/doc/flying-saucer-core-9.0.1.jar"/>
<classpathentry kind="lib" path="lib/extensions/doc/flying-saucer-pdf-9.0.1.jar"/>
<classpathentry kind="lib" path="lib/extensions/file/jcifs-ng-2.1.10.jar"/>
<classpathentry kind="lib" path="lib/extensions/file/jcifs-ng-2.1.10.jar"/>
<classpathentry kind="lib" path="lib/extensions/doc/itext-2.1.7.jar"/>
<classpathentry kind="lib" path="lib/extensions/doc/itext-rtf-2.1.7.jar"/>
<classpathentry kind="lib" path="lib/commons/commons-beanutils-1.9.4.jar"/>
Expand Down Expand Up @@ -190,9 +191,9 @@
<classpathentry kind="lib" path="/Donkey/lib/guava/j2objc-annotations-1.3.jar"/>
<classpathentry kind="lib" path="/Donkey/lib/guava/jsr305-3.0.2.jar"/>
<classpathentry kind="lib" path="/Donkey/lib/guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"/>
<classpathentry kind="lib" path="lib/bcpkix-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/bcprov-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/bcutil-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/bcpkix-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/bcprov-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/bcutil-jdk18on-1.78.1.jar"/>
<classpathentry kind="lib" path="lib/commons/commons-vfs2-2.9.0.jar">
<attributes>
<attribute name="javadoc_location" value="http://commons.apache.org/proper/commons-vfs/apidocs"/>
Expand Down
1 change: 1 addition & 0 deletions server/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,7 @@
<copy todir="${test_classes}">
<fileset dir="${test}">
<include name="**/*.xml" />
<include name="**/*.json" />
</fileset>
</copy>

Expand Down
Binary file added server/lib/java-semver-0.10.2.jar
Binary file not shown.
214 changes: 120 additions & 94 deletions server/src/com/mirth/connect/client/core/ConnectServiceUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,30 @@

package com.mirth.connect.client.core;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
Expand All @@ -38,10 +46,12 @@
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.zafarkhaja.semver.Version;
import com.mirth.connect.model.User;
import com.mirth.connect.model.converters.ObjectXMLSerializer;
import com.mirth.connect.model.notification.Notification;
Expand All @@ -51,9 +61,7 @@ public class ConnectServiceUtil {
private final static String URL_CONNECT_SERVER = "https://connect.mirthcorp.com";
private final static String URL_REGISTRATION_SERVLET = "/RegistrationServlet";
private final static String URL_USAGE_SERVLET = "/UsageStatisticsServlet";
private final static String URL_NOTIFICATION_SERVLET = "/NotificationServlet";
private static String NOTIFICATION_GET = "getNotifications";
private static String NOTIFICATION_COUNT_GET = "getNotificationCount";
private static String URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases";
private final static int TIMEOUT = 10000;
public final static Integer MILLIS_PER_DAY = 86400000;

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

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

try {
Expand All @@ -87,112 +95,130 @@ public static void registerUser(String serverId, String mirthVersion, User user,
}
}

/**
* Query an external source for new releases. Return notifications for each release that's greater than the current version.
*
* @param serverId
* @param mirthVersion
* @param extensionVersions
* @param protocols
* @param cipherSuites
* @return a non-null list
* @throws Exception should anything fail dealing with the web request and the handling of its response
*/
public static List<Notification> getNotifications(String serverId, String mirthVersion, Map<String, String> extensionVersions, String[] protocols, String[] cipherSuites) throws Exception {
CloseableHttpClient client = null;
HttpPost post = new HttpPost();
CloseableHttpResponse response = null;

List<Notification> allNotifications = new ArrayList<Notification>();
List<Notification> validNotifications = Collections.emptyList();
Optional<Version> parsedMirthVersion = Version.tryParse(mirthVersion);
if (!parsedMirthVersion.isPresent()) {
return validNotifications;
}

CloseableHttpClient httpClient = null;
CloseableHttpResponse httpResponse = null;
HttpEntity responseEntity = null;
try {
ObjectMapper mapper = new ObjectMapper();
String extensionVersionsJson = mapper.writeValueAsString(extensionVersions);
NameValuePair[] params = { new BasicNameValuePair("op", NOTIFICATION_GET),
new BasicNameValuePair("serverId", serverId),
new BasicNameValuePair("version", mirthVersion),
new BasicNameValuePair("extensionVersions", extensionVersionsJson) };
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
HttpClientContext getContext = HttpClientContext.create();
getContext.setRequestConfig(requestConfig);
httpClient = getClient(protocols, cipherSuites);
HttpGet httpget = new HttpGet(URL_NOTIFICATIONS);
// adding header makes github send back body as rendered html for the "body_html" field
httpget.addHeader("Accept", "application/vnd.github.html+json");
httpResponse = httpClient.execute(httpget, getContext);

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

HttpClientContext postContext = HttpClientContext.create();
postContext.setRequestConfig(requestConfig);
client = getClient(protocols, cipherSuites);
response = client.execute(post, postContext);
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
if ((statusCode == HttpStatus.SC_OK)) {
HttpEntity responseEntity = response.getEntity();
Charset responseCharset = null;
try {
responseCharset = ContentType.getOrDefault(responseEntity).getCharset();
} catch (Exception e) {
responseCharset = ContentType.TEXT_PLAIN.getCharset();
}

String responseContent = IOUtils.toString(responseEntity.getContent(), responseCharset).trim();
JsonNode rootNode = mapper.readTree(responseContent);

for (JsonNode childNode : rootNode) {
Notification notification = new Notification();
notification.setId(childNode.get("id").asInt());
notification.setName(childNode.get("name").asText());
notification.setDate(childNode.get("date").asText());
notification.setContent(childNode.get("content").asText());
allNotifications.add(notification);
}
validNotifications = toJsonStream(responseEntity)
.filter(dropOlderThan(parsedMirthVersion.get()))
.map(ConnectServiceUtil::toNotification)
.collect(Collectors.toList());
} else {
throw new ClientException("Status code: " + statusCode);
}
} catch (Exception e) {
throw e;
} finally {
HttpClientUtils.closeQuietly(response);
HttpClientUtils.closeQuietly(client);
EntityUtils.consumeQuietly(responseEntity);
HttpClientUtils.closeQuietly(httpResponse);
HttpClientUtils.closeQuietly(httpClient);
}

return allNotifications;
return validNotifications;
}

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

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

/**
* Try pulling a charset from the given response. Default to UTF-8.
*
* @param responseEntity
* @return
*/
protected static Charset getCharset(HttpEntity responseEntity) {
Charset charset = StandardCharsets.UTF_8;
try {
ObjectMapper mapper = new ObjectMapper();
String extensionVersionsJson = mapper.writeValueAsString(extensionVersions);
NameValuePair[] params = { new BasicNameValuePair("op", NOTIFICATION_COUNT_GET),
new BasicNameValuePair("serverId", serverId),
new BasicNameValuePair("version", mirthVersion),
new BasicNameValuePair("extensionVersions", extensionVersionsJson) };
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
ContentType ct = ContentType.get(responseEntity);
Charset fromHeader = ct.getCharset();
if (fromHeader != null) {
charset = fromHeader;
}
} catch (Exception ignore) {}
return charset;
}

post.setURI(URI.create(URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET));
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
/**
* Given a JSON node with HTML content from a GitHub release feed, convert it to a notification.
*
* @param node
* @return a notification
*/
protected static Notification toNotification(JsonNode node) {
Notification notification = new Notification();
notification.setId(node.get("id").asInt());
notification.setName(node.get("name").asText());
notification.setDate(node.get("published_at").asText());
notification.setContent(node.get("body_html").asText());
return notification;
}

HttpClientContext postContext = HttpClientContext.create();
postContext.setRequestConfig(requestConfig);
client = getClient(protocols, cipherSuites);
response = client.execute(post, postContext);
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
if ((statusCode == HttpStatus.SC_OK)) {
HttpEntity responseEntity = response.getEntity();
Charset responseCharset = null;
try {
responseCharset = ContentType.getOrDefault(responseEntity).getCharset();
} catch (Exception e) {
responseCharset = ContentType.TEXT_PLAIN.getCharset();
}

List<Integer> notificationIds = mapper.readValue(IOUtils.toString(responseEntity.getContent(), responseCharset).trim(), new TypeReference<List<Integer>>() {
});
for (int id : notificationIds) {
if (!archivedNotifications.contains(id)) {
notificationCount++;
}
}
}
} catch (Exception e) {
} finally {
HttpClientUtils.closeQuietly(response);
HttpClientUtils.closeQuietly(client);
public static int getNotificationCount(String serverId, String mirthVersion, Map<String, String> extensionVersions, Set<Integer> archivedNotifications, String[] protocols, String[] cipherSuites) {
Long notificationCount = 0L;
try {
notificationCount = getNotifications(serverId, mirthVersion, extensionVersions, protocols, cipherSuites)
.stream()
.map(Notification::getId)
.filter(id -> !archivedNotifications.contains(id))
.count();
} catch (Exception ignore) {
System.err.println("Failed to get notification count, defaulting to zero: " + ignore);
}
return notificationCount;
return notificationCount.intValue();
}

public static boolean sendStatistics(String serverId, String mirthVersion, boolean server, String data, String[] protocols, String[] cipherSuites) {
Expand All @@ -212,7 +238,7 @@ public static boolean sendStatistics(String serverId, String mirthVersion, boole
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();

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

try {
HttpClientContext postContext = HttpClientContext.create();
Expand Down
Loading