Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,5 @@ donkey/donkey-test

# /webadmin/src/com/mirth/connect/webadmin/utils/

.DS_Store
.java-version
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.
227 changes: 182 additions & 45 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,31 @@

package com.mirth.connect.client.core;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
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.Spliterator;
import java.util.Spliterators;
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.commons.text.StringEscapeUtils;
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 +47,13 @@
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 @@ -52,7 +64,7 @@ public class ConnectServiceUtil {
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 URL_NOTIFICATIONS = "https://api.github.com/repos/openintegrationengine/engine/releases";
private static String NOTIFICATION_COUNT_GET = "getNotificationCount";
private final static int TIMEOUT = 10000;
public final static Integer MILLIS_PER_DAY = 86400000;
Expand Down Expand Up @@ -87,62 +99,187 @@ 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> validNotifications = Collections.emptyList();

List<Notification> allNotifications = new ArrayList<Notification>();
Optional<Version> parsedMirthVersion = Version.tryParse(mirthVersion);

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();
if(!parsedMirthVersion.isPresent()) return validNotifications;

post.setURI(URI.create(URL_CONNECT_SERVER + URL_NOTIFICATION_SERVLET));
post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params), Charset.forName("UTF-8")));
CloseableHttpClient httpClient = null;
CloseableHttpResponse httpResponse = null;
HttpEntity responseEntity = null;

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();
}
try {
HttpClientContext getContext = HttpClientContext.create();
getContext.setRequestConfig(createRequestConfig());
httpClient = getClient(protocols, cipherSuites);
httpResponse = httpClient.execute(new HttpGet(URL_NOTIFICATIONS), getContext);

String responseContent = IOUtils.toString(responseEntity.getContent(), responseCharset).trim();
JsonNode rootNode = mapper.readTree(responseContent);
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
responseEntity = httpResponse.getEntity();

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);
}
Stream<JsonNode> newerOnly = filterForNewerVersions(toJsonStream(responseEntity), parsedMirthVersion.get());
validNotifications = newerOnly.map(node -> toNotification(node)).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;
}

/**
* Create a request config with appropriate network timeouts.
*
* @return
*/
private static RequestConfig createRequestConfig() {
return RequestConfig.custom().setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
}

/**
* Filter the stream to only new versions, then create a notification for each.
*
* @param nodes
* @param currentVersion
* @return a non-null List
*/
protected static Stream<JsonNode> filterForNewerVersions(Stream<JsonNode> nodes, Version currentVersion) {
return nodes.filter(node -> isCurrentOlderThan(currentVersion, node.get("tag_name").asText()));
}

/**
* Convert a JSON response to a stream of {@link JsonNode}.
*
* @param responseEntity
* @return a stream
* @throws IOException
* @throws JsonMappingException
*/
protected static Stream<JsonNode> toJsonStream(HttpEntity responseEntity) throws IOException, JsonMappingException {
String responseContent = getResponseContent(responseEntity);

JsonNode rootNode = new ObjectMapper().readTree(responseContent);

//convert to stream to simplify
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(rootNode.elements(), Spliterator.ORDERED),false);
}

/**
* Extract the reponse content. It attempts to use the charset found in the response, if any.
*
* @param responseEntity
* @return
* @throws IOException
*/
protected static String getResponseContent(HttpEntity responseEntity) throws IOException {
return IOUtils.toString(responseEntity.getContent(), getCharset(responseEntity));
}

/**
* Try pulling a charset from the given response. Default to {@link ContentType#TEXT_PLAIN}.
*
* @param responseEntity
* @return
*/
protected static Charset getCharset(HttpEntity responseEntity) {
Charset charset = ContentType.TEXT_PLAIN.getCharset();
try {
charset = ContentType.getOrDefault(responseEntity).getCharset();
} catch (Exception ignore) {}
return charset;
}

/**
* Split a given version string into an int[].
*
* @param version
* @return
*/
protected static int[] toVersionArray(String version) {
return Arrays.stream(version.split("[\\.]")).mapToInt(Integer::parseInt).toArray();
}

/**
* Compare the current version with another. If current is less than other, return true. All others,
* including a malformed other, returns false.
*
* @param currentVersion
* @param anotherVersion
* @return true if the current version is less than than the other
*/
protected static boolean isCurrentOlderThan (Version currentVersion, String anotherVersion) {
Optional<Version> parsedOther = Version.tryParse(anotherVersion);

return parsedOther.isPresent() && currentVersion.isLowerThan(parsedOther.get());
}

/**
* Given a JSON node from a GitHub release feed, convert it to a HTML notification.
*
* @param node
* @return a notification with HTML content
*/
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());

// create the content html
String content = toNotificationContent(node);
notification.setContent(content);

return notification;
}

/**
* Create the HTML content for a notification.
*
* @param node
* @return an HTML String
*/
protected static String toNotificationContent (JsonNode node) {
String escapedName = StringEscapeUtils.escapeHtml4(node.get("name").asText());
String escapedReleaseUrl = StringEscapeUtils.escapeHtml4(node.get("html_url").asText());

StringBuilder content = new StringBuilder(256);

// create header with name
content.append("<h2>")
.append(escapedName)
.append("</h2>");

// announce there is a new version
content.append("<h3>")
.append("A new version of Open Integration Engine is available!")
.append("</h3>");

// create a link to the release webpage
content.append("<a href=\"" + escapedReleaseUrl + "\">")
.append("Release Webpage")
.append("</a>");

return content.toString();
}

public static int getNotificationCount(String serverId, String mirthVersion, Map<String, String> extensionVersions, Set<Integer> archivedNotifications, String[] protocols, String[] cipherSuites) {
Expand Down
Loading