org.phenotips.data.push.internal.DefaultPushPatientData.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.data.push.internal.DefaultPushPatientData.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.data.push.internal;

import org.phenotips.Constants;
import org.phenotips.data.Patient;
import org.phenotips.data.internal.controller.VersionsController;
import org.phenotips.data.push.PushPatientData;
import org.phenotips.data.push.PushServerConfigurationResponse;
import org.phenotips.data.push.PushServerGetPatientIDResponse;
import org.phenotips.data.push.PushServerSendPatientResponse;
import org.phenotips.data.shareprotocol.ShareProtocol;
import org.phenotips.data.shareprotocol.ShareProtocol.Incompatibility;

import org.xwiki.component.annotation.Component;
import org.xwiki.context.Execution;
import org.xwiki.model.reference.DocumentReference;

import java.net.URLEncoder;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONObject;
import org.slf4j.Logger;

import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;

/**
 * Default implementation for the {@link PushPatientData} component.
 *
 * @version $Id: 3de6cc241b368bb382e10471a8bed6f86eb61dce $
 * @since 1.0M11
 */
@Component
@Singleton
public class DefaultPushPatientData implements PushPatientData {
    /** Server configuration ID property name within the PushPatientServer class. */
    public static final String PUSH_SERVER_CONFIG_ID_PROPERTY_NAME = "name";

    /** Server configuration URL property name within the PushPatientServer class. */
    public static final String PUSH_SERVER_CONFIG_URL_PROPERTY_NAME = "url";

    /** Server configuration registration URL property name within the PushPatientServer class. */
    public static final String PUSH_SERVER_CONFIG_REGISTRATION_URL_PROPERTY_NAME = "registration_url";

    /** Server configuration Description property name within the PushPatientServer class. */
    public static final String PUSH_SERVER_CONFIG_DESC_PROPERTY_NAME = "description";

    /** Destination page. */
    private static final String PATIENT_DATA_SHARING_PAGE = "/bin/receivePatientData";

    /**
     * Key and value which should be added to POST/GET requests to force XWiki to display raw output without any
     * additional HTML E.g. http://localhost:8080/bin/receivePatientData?xpage=plain&...
     */
    private static final String XWIKI_RAW_OUTPUT_KEY = "xpage";

    private static final String XWIKI_RAW_OUTPUT_VALUE = "plain";

    /** Logging helper object. */
    @Inject
    private Logger logger;

    /** Provides access to the current request context. */
    @Inject
    private Execution execution;

    /** HTTP client used for communicating with the remote server. */
    private final CloseableHttpClient client = HttpClients.createSystem();

    /** A cache of known protocol versions for various server */
    private Map<String, String> protocolVersionsCache = new HashMap<>();

    /**
     * Helper method for obtaining a valid xcontext from the execution context.
     *
     * @return the current request context
     */
    private XWikiContext getXContext() {
        return (XWikiContext) this.execution.getContext().getProperty(XWikiContext.EXECUTIONCONTEXT_KEY);
    }

    /**
     * Return the the URL of the specified remote PhenoTips instance.
     *
     * @param serverConfiguration the XObject holding the remote server configuration
     * @return the configured URL, in the format {@code http://remote.host.name/bin/}, or {@code null} if the
     *         configuration isn't valid
     */
    private String getBaseURL(BaseObject serverConfiguration) {
        if (serverConfiguration != null) {
            String result = serverConfiguration.getStringValue(PUSH_SERVER_CONFIG_URL_PROPERTY_NAME);
            if (StringUtils.isBlank(result)) {
                return null;
            }
            if (!result.startsWith("http")) {
                result = "http://" + result;
            }
            return StringUtils.stripEnd(result, "/") + PATIENT_DATA_SHARING_PAGE;
        }
        return null;
    }

    private HttpPost generateRequest(String remoteServerIdentifier, List<NameValuePair> data) {
        BaseObject serverConfiguration = this.getPushServerConfiguration(remoteServerIdentifier);

        String submitURL = getBaseURL(serverConfiguration);
        if (submitURL == null) {
            return null;
        }

        this.logger.trace("POST URL: {}", submitURL);

        HttpPost method = new HttpPost(submitURL);

        method.setEntity(new UrlEncodedFormEntity(data, Consts.UTF_8));

        return method;
    }

    private List<NameValuePair> generateRequestData(String actionName, String userName, String password,
            String userToken, String protocolVersion) {
        List<NameValuePair> result = new LinkedList<>();
        result.add(new BasicNameValuePair(XWIKI_RAW_OUTPUT_KEY, XWIKI_RAW_OUTPUT_VALUE));
        result.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_PROTOCOLVER, protocolVersion));
        result.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_ACTION, actionName));
        result.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_USERNAME, userName));
        if (StringUtils.isNotBlank(userToken)) {
            result.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_USER_TOKEN, userToken));
        } else {
            result.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_PASSWORD, password));
        }
        return result;
    }

    /**
     * Get the push server configuration given its name.
     *
     * @param serverName name of the server
     * @return {@link BaseObject XObjects} with server configuration, may be {@code null}
     */
    private BaseObject getPushServerConfiguration(String serverName) {
        try {
            XWikiContext context = getXContext();
            XWiki xwiki = context.getWiki();
            XWikiDocument prefsDoc = xwiki
                    .getDocument(new DocumentReference(context.getWikiId(), "XWiki", "XWikiPreferences"), context);
            return prefsDoc.getXObject(
                    new DocumentReference(context.getWikiId(), Constants.CODE_SPACE, "PushPatientServer"),
                    PUSH_SERVER_CONFIG_ID_PROPERTY_NAME, serverName);
        } catch (XWikiException ex) {
            this.logger.warn("Failed to get server info: {}", ex.getMessage(), ex);
            return null;
        }
    }

    @Override
    public PushServerConfigurationResponse getRemoteConfiguration(String remoteServerIdentifier, String userName,
            String password, String userToken) {
        // try to connect to server using current push protocol version
        PushServerConfigurationResponse serverResponse = this.sendRemoteConfigurationRequest(remoteServerIdentifier,
                userName, password, userToken, ShareProtocol.CURRENT_PUSH_PROTOCOL_VERSION);
        if (serverResponse == null) {
            return null;
        }

        // compatibility check: if selected server is using a no longer supported push protocol version
        // a corresponding error should be returned
        String serverProtocolVersion = serverResponse.getServerProtocolVersion();
        if (serverProtocolVersion == null
                || ShareProtocol.OLD_INCOMPATIBLE_VERSIONS.contains(serverProtocolVersion)) {
            return new UnsupportedOldServerProtocolResponse();
        }

        if (serverResponse.isServerDoesNotAcceptClientProtocolVersion()
                && ShareProtocol.COMPATIBLE_OLD_SERVER_PROTOCOL_VERSIONS.contains(serverProtocolVersion)) {
            // server does not accept our initial protocol version, and it is one of the older supported verisons
            // => fall back to the same version that server uses - and retry the connection
            serverResponse = this.sendRemoteConfigurationRequest(remoteServerIdentifier, userName, password,
                    userToken, serverProtocolVersion);
        }

        // store the last known version of push protocol used by the server, so that without client code
        // worrying about that a proper serializer (when possible) is used when pushing data to that server
        this.protocolVersionsCache.put(remoteServerIdentifier, serverProtocolVersion);

        return serverResponse;
    }

    private PushServerConfigurationResponse sendRemoteConfigurationRequest(String remoteServerIdentifier,
            String userName, String password, String userToken, String useProtocolVersion) {
        this.logger.debug("===> Getting server configuration for: [{}]", remoteServerIdentifier);

        HttpPost method = null;

        try {
            method = generateRequest(remoteServerIdentifier,
                    generateRequestData(ShareProtocol.CLIENT_POST_ACTIONKEY_VALUE_INFO, userName, password,
                            userToken, useProtocolVersion));
            if (method == null) {
                return null;
            }

            try (CloseableHttpResponse httpResponse = this.client.execute(method)) {
                int returnCode = httpResponse.getStatusLine().getStatusCode();
                this.logger.trace("GetConfig HTTP return code: {}", returnCode);

                String response = IOUtils.toString(httpResponse.getEntity().getContent(), Consts.UTF_8);

                this.logger.debug("===> Push server response: [{}]", response);

                // Can't be valid JSON with less than 2 characters: most likely empty response from an un-accepting
                // server
                if (response.length() < 2) {
                    return null;
                }

                try {
                    JSONObject responseJSON = new JSONObject(response);

                    return new DefaultPushServerConfigurationResponse(responseJSON);
                } catch (Exception ex) {
                    this.logger.error("Received invalid JSON reply from remote server: {}...",
                            response.substring(0, 50));
                    return null;
                }
            }
        } catch (Exception ex) {
            this.logger.error("Failed to login: {}", ex.getMessage(), ex);
        } finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
        return null;
    }

    @Override
    public PushServerSendPatientResponse sendPatient(Patient patient, Set<String> exportFields,
            JSONObject patientState, String groupName, String remoteGUID, String remoteServerIdentifier,
            String userName, String password, String userToken) {
        this.logger.info("Pushing data to server: [{}]", remoteServerIdentifier);

        HttpPost method = null;

        try {
            String serverProtocolVersion = this.getProtocolVersionForPushingToServer(remoteServerIdentifier);

            List<NameValuePair> data = generateRequestData(ShareProtocol.CLIENT_POST_ACTIONKEY_VALUE_PUSH, userName,
                    password, userToken, serverProtocolVersion);
            if (exportFields != null) {
                // Version information is required in the JSON; when exportFields is null everything is included anyway
                exportFields.add(VersionsController.getEnablingFieldName());
            }

            // for compatibility with servers running older versions of PhenoTips:
            //
            // if the target server is known to support only old versions of push protocol, replace
            // those fields which are not compatible with compatible alternatives (to trigger old serializers)
            if (this.protocolVersionsCache.containsKey(remoteServerIdentifier)) {
                if (ShareProtocol.INCOMPATIBILITIES_IN_OLD_PROTOCOL_VERSIONS.containsKey(serverProtocolVersion)) {
                    this.logger.warn("Using old serializers for protocol version [{}] to push data to server [{}]",
                            serverProtocolVersion, remoteServerIdentifier);
                    List<ShareProtocol.Incompatibility> incompatibilitiesList = ShareProtocol.INCOMPATIBILITIES_IN_OLD_PROTOCOL_VERSIONS
                            .get(serverProtocolVersion);
                    for (Incompatibility incompat : incompatibilitiesList) {
                        if (exportFields.contains(incompat.getCurrentFieldName())) {
                            exportFields.remove(incompat.getCurrentFieldName());
                            if (!StringUtils.isEmpty(incompat.getDeprecatedFieldName())) {
                                exportFields.add(incompat.getDeprecatedFieldName());
                            }
                        }
                    }
                }
            }

            String patientJSON = patient.toJSON(exportFields).toString();
            this.logger.debug("Sending patient JSON: [{}]", patientJSON);

            data.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_PATIENTJSON,
                    URLEncoder.encode(patientJSON, XWiki.DEFAULT_ENCODING)));

            data.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_PATIENTSTATE,
                    URLEncoder.encode(patientState.toString(), XWiki.DEFAULT_ENCODING)));

            if (groupName != null) {
                data.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_GROUPNAME, groupName));
            }
            if (remoteGUID != null) {
                data.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_GUID, remoteGUID));
            }

            method = generateRequest(remoteServerIdentifier, data);
            if (method == null) {
                return null;
            }
            try (CloseableHttpResponse httpResponse = this.client.execute(method)) {
                int returnCode = httpResponse.getStatusLine().getStatusCode();
                this.logger.trace("Push HTTP return code: {}", returnCode);

                String response = IOUtils.toString(httpResponse.getEntity().getContent(), Consts.UTF_8);
                this.logger.trace("RESPONSE FROM SERVER: {}", response);
                JSONObject responseJSON = new JSONObject(response);

                return new DefaultPushServerSendPatientResponse(responseJSON);
            }
        } catch (Exception ex) {
            this.logger.error("Failed to push patient: {}", ex.getMessage(), ex);
        } finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
        return null;
    }

    @Override
    public PushServerGetPatientIDResponse getPatientURL(String remoteServerIdentifier, String remoteGUID,
            String userName, String password, String userToken) {
        this.logger.debug("===> Contacting server: [{}]", remoteServerIdentifier);

        HttpPost method = null;

        try {
            List<NameValuePair> data = generateRequestData(ShareProtocol.CLIENT_POST_ACTIONKEY_VALUE_GETID,
                    userName, password, userToken,
                    this.getProtocolVersionForPushingToServer(remoteServerIdentifier));
            data.add(new BasicNameValuePair(ShareProtocol.CLIENT_POST_KEY_NAME_GUID, remoteGUID));

            method = generateRequest(remoteServerIdentifier, data);
            if (method == null) {
                return null;
            }

            try (CloseableHttpResponse httpResponse = this.client.execute(method)) {
                int returnCode = httpResponse.getStatusLine().getStatusCode();
                this.logger.trace("Push HTTP return code: {}", returnCode);

                String response = IOUtils.toString(httpResponse.getEntity().getContent(), Consts.UTF_8);
                this.logger.trace("RESPONSE FROM SERVER: {}", response);
                JSONObject responseJSON = new JSONObject(response);

                return new DefaultPushServerGetPatientIDResponse(responseJSON);
            }
        } catch (Exception ex) {
            this.logger.error("Failed to get patient URL: {}", ex.getMessage(), ex);
        } finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
        return null;
    }

    private String getProtocolVersionForPushingToServer(String remoteServerIdentifier) {
        return this.protocolVersionsCache.containsKey(remoteServerIdentifier)
                ? this.protocolVersionsCache.get(remoteServerIdentifier)
                : ShareProtocol.CURRENT_PUSH_PROTOCOL_VERSION;
    }
}