com.ittm_solutions.ipacore.IpaApi.java Source code

Java tutorial

Introduction

Here is the source code for com.ittm_solutions.ipacore.IpaApi.java

Source

/*
 * Copyright (C) 2016 ITTM S.A.
 *
 * Written by Nils Christian <nils.christian@ittm-solutions.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.ittm_solutions.ipacore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
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.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.util.EntityUtils;

/**
 * Wrapper around the REST API of Ingenuity Pathway Analysis (IPA).
 * <p>
 * This class wraps calls to IPA's REST API. It handles the login procedure,
 * parses responses and returns Java objects (instead of plain strings from http
 * responses), most importantly {@link IpaAnalysisResults} for the results of an
 * analysis.
 * <p>
 * For details check out the documentation about `IPA Integration Module` on
 * <a href="http://www.ingenuity.com/products/custom-solutions">Ingenuity's
 * website</a> (last retrieved July 26, 2016).
 */
public class IpaApi {
    /**
     * user name used to log in to IPA server
     */
    private String username;
    /**
     * password used to log in to IPA server
     */
    private String password;
    /**
     * url of IPA server
     */
    private String serverUrl;
    /**
     * context used for keeping the http session
     */
    private HttpClientContext hcContext = null;
    /**
     * manager to reuse http connections
     */
    private PoolingHttpClientConnectionManager connectionManager = null;
    private final Log log = LogFactory.getLog(IpaApi.class);

    /**
     * Constructor.
     *
     * @param username
     *            the username used to connect to the IPA server
     * @param password
     *            the password used to connect to the IPA server
     * @param serverUrl
     *            the URL of the the IPA server
     */
    public IpaApi(String username, String password, String serverUrl) {
        this.username = username;
        this.password = password;
        this.serverUrl = serverUrl;
        log.debug("Created IpaApi for user: " + username + ", password: *, server: " + serverUrl);
    }

    /**
     * Constructor using the default URL for the IPA server.
     *
     * @param username
     *            the username used to connect to the IPA server
     * @param password
     *            the password used to connect to the IPA server
     */
    public IpaApi(String username, String password) {
        this(username, password, "https://analysis.ingenuity.com");
    }

    /**
     * Returns the connection manager used for all connections to the IPA
     * server.
     * <p>
     * This method initializes the connection manager on its first call,
     * subsequent calls return the same instance.
     *
     * @return connection manager
     */
    private PoolingHttpClientConnectionManager connectionManager() {
        if (connectionManager == null) {
            connectionManager = new PoolingHttpClientConnectionManager();
            connectionManager.setMaxTotal(10);
            // There must be only one connection as IPA fails otherwise
            connectionManager.setDefaultMaxPerRoute(1);
        }

        return connectionManager;
    }

    /**
     * Returns the http client context used for all connections to the IPA
     * server.
     * <p>
     * This method initializes the http client context on its first call,
     * subsequent calls return the same instance.
     *
     * @return http client context
     */
    private HttpClientContext clientContext() {
        if (hcContext == null) {
            // Create a local instance of cookie store
            CookieStore cookieStore = new BasicCookieStore();
            // Create local HTTP context
            hcContext = HttpClientContext.create();
            // Bind custom cookie store to the local context
            hcContext.setCookieStore(cookieStore);
        }

        return hcContext;
    }

    /**
     * Returns a http client with reusing connections.
     * <p>
     * This method uses the http client context and the connection manager to
     * create a http client.
     *
     * @return http client
     */
    private CloseableHttpClient httpClient() {
        PoolingHttpClientConnectionManager cm = this.connectionManager();
        LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm)
                .setRedirectStrategy(redirectStrategy).build();
        return httpClient;
    }

    /**
     * Returns the final URI in a redirect chain of a request.
     *
     * @param context
     *            client context as returned by {@code clientContext}
     * @return URI
     */
    private static URI currentRedirectUri(HttpClientContext context) {
        HttpUriRequest currentReq = (HttpUriRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
        HttpHost currentHost = (HttpHost) context.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
        URI uri = null;
        try {
            uri = ((currentReq.getURI().isAbsolute()) ? currentReq.getURI()
                    : (new URI(currentHost.toURI() + currentReq.getURI())));
        } catch (URISyntaxException e) {
            // this should never happen
            throw new RuntimeException(e);
        }
        return uri;
    }

    /**
     * Parses html content and returns the login ticket.
     * <p>
     * This method reads through the html code and returns the login ticket
     * contained in tag {@code name="lt" value="<ticket>"}.
     *
     * @param is
     *            the input stream containing the response content
     * @return login ticket
     */

    private String loginTicket(InputStream is) throws IpaApiException {
        final Pattern loginTicketRegex = Pattern.compile(".*name=\"lt\" value=\"([^\"]+)\".*");
        try (InputStreamReader isr = new InputStreamReader(is); BufferedReader reader = new BufferedReader(isr)) {
            String str = null;
            while ((str = reader.readLine()) != null) {
                Matcher m = loginTicketRegex.matcher(str);
                if (m.find()) {
                    return m.group(1);
                }
            }
        } catch (IOException e) {
            throw new IpaApiException(e);
        }
        // login ticket not defined
        throw new IpaApiException("lt not found");
    }

    /**
     * Performs authentication with IPA server with the given http client and
     * context.
     *
     * @param hc
     *            http client as returned by {@code httpClient}
     * @param context
     *            client context as returned by {@code clientContext}
     *
     * @return login ticket
     */
    private void login(CloseableHttpClient hc, HttpClientContext context) throws IpaApiException {
        String lt = null;
        URI loginUri = null;
        HttpGet httpget = new HttpGet(serverUrl);
        try (CloseableHttpResponse response = hc.execute(httpget, context)) {
            log.info("get " + serverUrl + " status " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                throw new IpaApiHttpResponseException(serverUrl, response);
            }

            // get the final URI from the redirect chain
            loginUri = currentRedirectUri(context);

            // get the login ticket from the html content
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                throw new IpaApiEntityNullException();
            }
            lt = this.loginTicket(entity.getContent());
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        ArrayList<NameValuePair> postParameters = new ArrayList<NameValuePair>();
        postParameters.add(new BasicNameValuePair("username", username));
        postParameters.add(new BasicNameValuePair("password", password));
        postParameters.add(new BasicNameValuePair("_eventId", "submit"));
        postParameters.add(new BasicNameValuePair("lt", lt));
        HttpPost httppost = new HttpPost(loginUri);
        try {
            httppost.setEntity(new UrlEncodedFormEntity(postParameters));
        } catch (UnsupportedEncodingException e) {
            throw new IpaApiException(e);
        }
        try (CloseableHttpResponse response = hc.execute(httppost, context)) {
            log.info("post " + loginUri + " status " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                throw new IpaApiHttpResponseException(serverUrl, response);
            }

            if (!currentRedirectUri(context).toString().startsWith(serverUrl)) {
                throw new IpaApiAuthenticationFailedException();
            }
        } catch (IOException e) {
            throw new IpaApiException(e);
        }
        log.debug("authentication successful");
    }

    /**
     * Checks the string (a part of the content) for known errors.
     * <p>
     * This method reads through the given substring of the response content and
     * checks whether it contains a fragment that could indicate an error.
     *
     * @return null or the error indicator
     */
    private static IpaErrorIndication containsStringIndicatingError(String contentFragement) {
        // FIXME an error div may span multiple lines, and could include other
        // divs; these regexes are not sufficient, a real xml parser should be
        // used
        final Pattern errDivRgx1 = Pattern.compile(".*?<\\s*div\\s+id='errors'.*?>(.*?)</\\s*div\\s*>.*");
        final Pattern errDivRgx2 = Pattern.compile(".*?<\\s*div\\s+id='errors'.*?>(.*)");

        if (Pattern.matches(".*Your current IPA license does not have privileges to access this feature.*",
                contentFragement)) {
            return IpaErrorIndication.UNAUTHORIZED;
        }
        if (Pattern.matches(
                ".*The dataset name \\(.*?\\) already exists in the project .*?\\.\\s+Please choose a different dataset name.*",
                contentFragement)) {
            return IpaErrorIndication.DATASET_NAME_EXISTS;
        }
        if (Pattern.matches(
                ".*The analysis name \\(.*?\\) already exists in the project .*?\\.\\s+Please choose a different analysis name.*",
                contentFragement)) {
            return IpaErrorIndication.ANALYSIS_NAME_EXISTS;
        }
        if (Pattern.matches("^No analysis found for ID=(\\d+) for userId=(\\d+)$", contentFragement)) {
            return IpaErrorIndication.ANALYSIS_ID_NOT_FOUND;
        }
        if (Pattern.matches(".*session has been expired.*", contentFragement)) {
            return IpaErrorIndication.SESSION_EXPIRED;
        }
        Matcher m1 = errDivRgx1.matcher(contentFragement);
        if (m1.find()) {
            IpaErrorIndication err = IpaErrorIndication.OTHER;
            err.setMessage(m1.group(1));
            return err;
        }
        Matcher m2 = errDivRgx2.matcher(contentFragement);
        if (m2.find()) {
            IpaErrorIndication err = IpaErrorIndication.OTHER;
            err.setMessage(m2.group(1));
            return err;
        }
        if (Pattern.matches("<\\s*html.*", contentFragement)) {
            return IpaErrorIndication.CONTAINS_HTML;
        }

        return null;
    }

    /**
     * Checks the remaining content of a {@code HttpResponse} for the reason of an
     * error.
     * <p>
     * Errors are communicated through plain text or html in a response. When
     * parsing a response {@link containsStringIndicatingError} may encounter a
     * hint that the content is not what is expected but rather an error (eg.
     * {@code <html>}). This method can take over to parse the rest of the content
     * to find the real reason of the error (eg. a message contained inside a
     * {@code <div id='errors'>}).
     *
     * @param reader
     *          the reader object created from a {@code HttpResponse}; parts of it
     *          may already be consumed
     * @return the main error reason
     */
    private static IpaErrorIndication parseContentForError(BufferedReader reader, IpaErrorIndication currentReason)
            throws IOException {
        if (currentReason == null) {
            throw new IllegalArgumentException();
        }
        IpaErrorIndication errorWithString = null;
        IpaErrorIndication errorWithoutString = null;
        boolean sessionExpired = false;
        boolean unAuthorized = false;
        IpaErrorIndication thisErr = currentReason;
        String currentline = null;
        do {
            if (currentline != null) {
                thisErr = containsStringIndicatingError(currentline);
            }
            if (thisErr != null) {
                if (thisErr == IpaErrorIndication.UNAUTHORIZED) {
                    unAuthorized = true;
                } else if (thisErr == IpaErrorIndication.SESSION_EXPIRED) {
                    sessionExpired = true;
                } else if (thisErr == IpaErrorIndication.CONTAINS_HTML) {
                    // nothing here, the least relevant error
                } else if (thisErr == IpaErrorIndication.OTHER) {
                    // NOTE we have never seen this, but in case there are two
                    // error strings just keep the first
                    if (errorWithString == null) {
                        errorWithString = thisErr;
                    }
                } else {
                    errorWithoutString = thisErr;
                }
            }

        } while ((currentline = reader.readLine()) != null);

        // return the most relevant error
        if (errorWithString != null) {
            return errorWithString;
        }
        if (unAuthorized) {
            return IpaErrorIndication.UNAUTHORIZED;
        }
        if (sessionExpired) {
            return IpaErrorIndication.SESSION_EXPIRED;
        }
        if (errorWithoutString != null) {
            return errorWithoutString;
        }
        return IpaErrorIndication.CONTAINS_HTML;
    }

    /**
     * Checks whether the current session is still usable.
     * <p>
     * This method makes a dummy request and checks whether it receives a valid
     * answer. If this is the case, the connection is still usable.
     *
     * @param hc
     *            http client as returned by {@code httpClient}
     * @param context
     *            client context as returned by {@code clientContext}
     * @return null or the error indicator
     */
    private boolean sessionAlive(CloseableHttpClient hc, HttpClientContext context) throws IpaApiException {
        String path = "/pa/api/v1/projectsearch";
        try {
            URI uri = new URIBuilder(serverUrl).setPath(path).addParameter("query", "XXXXXX").build();
            HttpGet httpget = new HttpGet(uri);
            try (CloseableHttpResponse response = hc.execute(httpget, context)) {
                if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                    return false;
                }

                // check content for strings indicating that we first have to log in
                HttpEntity entity = response.getEntity();
                if (entity == null) {
                    throw new IpaApiEntityNullException();
                }
                try (InputStreamReader isr = new InputStreamReader(entity.getContent());
                        BufferedReader reader = new BufferedReader(isr)) {
                    String contentLine = null;
                    while ((contentLine = reader.readLine()) != null) {
                        IpaErrorIndication err = containsStringIndicatingError(contentLine);
                        if (err != null) {
                            log.debug(parseContentForError(reader, err));
                            return false;
                        }
                    }
                }
            }
        } catch (URISyntaxException | IOException e) {
            throw new IpaApiException(e);
        }
        return true;
    }

    /**
     * Returns a http client that is authenticated (as thus usable).
     *
     * @return http client
     */
    private CloseableHttpClient authenticatedClient() throws IpaApiException {
        CloseableHttpClient hc = this.httpClient();
        HttpClientContext context = this.clientContext();
        if (this.sessionAlive(hc, context)) {
            log.debug("authenticatedClient: reusing existing session");
            return hc;
        }

        this.login(hc, context);
        log.debug("authenticatedClient: created session");
        return hc;
    }

    /**
     * Exports the given analyses or all analyses of an IPA project.
     * <p>
     * See {@link #exportAnalyses} for details on {@code resultTypes}.
     *
     * @param projectId
     *          the id of an IPA project (usually a number)
     * @param analysisIds
     *          the analyses ids
     * @param resultTypes
     *          the result types (see {@code exportAnalyses})
     * @return the exported results wrapped in a class
     */
    private CloseableHttpResponse exportRawAnalyses(String projectId, List<String> analysisIds,
            List<String> resultTypes) throws IpaApiException {
        final String path = "/pa/api/v1/export";
        CloseableHttpResponse response;
        try {
            URIBuilder uribuilder = new URIBuilder(serverUrl).setPath(path);
            if (projectId != null) {
                uribuilder.addParameter("pid", projectId);
            }
            // FIXME if we get a large number of analysis ids we may have to split
            // this list, see section 2.4.1 of IPA Integration Module
            if (analysisIds.size() > 0) {
                uribuilder.addParameter("aid", StringUtils.join(analysisIds, ","));
            }
            if (resultTypes.size() > 0) {
                uribuilder.addParameter("art", StringUtils.join(resultTypes, ","));
            }
            URI uri = uribuilder.build();

            CloseableHttpClient hc = this.authenticatedClient();
            HttpClientContext context = this.clientContext();
            HttpGet httpget = new HttpGet(uri);
            response = hc.execute(httpget, context);
            log.info("get " + uri + " status " + response.getStatusLine().getStatusCode());
        } catch (URISyntaxException | IOException e) {
            throw new IpaApiException(e);
        }
        return response;
    }

    /**
     * Exports the given analyses.
     * <p>
     * This method calls the export endpoint of the IPA API, allowing to export
     * analyses by their ids and restricting the results to certain types.
     * <p>
     * Available {@code result types} are: {@code settings}, {@code networks},
     * {@code canonicalpathways}, {@code biofunctions}, {@code toxfunctions},
     * {@code upstreamregulators}, {@code causalnetworks}, {@code mypathways},
     * {@code mylists}, {@code toxlists} and {@code analysisreadymolecules}. If
     * {@code resultTypes} is null all results will be exported.
     *
     * @param analysisIds
     *          the analyses ids
     * @param resultTypes
     *          the result types
     * @return list of exported results wrapped in a class
     */
    public synchronized List<IpaAnalysisResults> exportAnalyses(List<String> analysisIds, List<String> resultTypes)
            throws IpaApiException {
        List<IpaAnalysisResults> ipaResults;
        try {
            try (CloseableHttpResponse rawAnalyses = exportRawAnalyses(null, analysisIds, resultTypes)) {
                HttpEntity entity = rawAnalyses.getEntity();
                if (entity == null) {
                    throw new IpaApiEntityNullException();
                }
                ipaResults = parseExport(entity.getContent());
            }
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        return ipaResults;
    }

    /**
     * Exports the given analyses.
     * <p>
     * This method calls the export endpoint of the IPA API, allowing to export
     * analyses by their ids.
     *
     * @param analysisIds
     *          the analyses ids
     * @return list of exported results wrapped in a class
     */
    public synchronized List<IpaAnalysisResults> exportAnalyses(List<String> analysisIds) throws IpaApiException {
        return exportAnalyses(analysisIds, new ArrayList<String>());
    }

    /**
     * Exports the given analysis.
     * <p>
     * This method calls the export endpoint of the IPA API, allowing to export
     * one analysis by its id.
     *
     * @param analysisId
     *          the analysis id
     * @return exported results wrapped in a class
     */
    public synchronized IpaAnalysisResults exportAnalysis(String analysisId) throws IpaApiException {
        List<String> analysisIds = new ArrayList<String>(Arrays.asList(analysisId));
        List<IpaAnalysisResults> res = exportAnalyses(analysisIds, new ArrayList<String>());
        if (res.size() != 1) {
            return null;
        }
        return res.get(0);
    }

    /**
     * Exports the given analyses or all analyses of an IPA project.
     * <p>
     * This method calls the export endpoint of the IPA API, allowing to export
     * all analyses of a project with the given id.
     *
     * @param query
     *          the search string (allows wildcards like {@code *})
     * @return list of analysis ids
     */
    public synchronized List<String> searchAnalysis(String query) throws IpaApiException {
        final String path = "/pa/api/v1/projectsearch";

        List<String> analysisIds;
        URI uri;
        try {
            uri = new URIBuilder(serverUrl).setPath(path).addParameter("query", query).build();
        } catch (URISyntaxException e) {
            throw new IpaApiException(e);
        }

        CloseableHttpClient hc = this.authenticatedClient();
        HttpClientContext context = this.clientContext();
        HttpGet httpget = new HttpGet(uri);
        try (CloseableHttpResponse response = hc.execute(httpget, context)) {
            log.info("get " + uri + " status " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                throw new IpaApiHttpResponseException(uri, response);
            }

            // get the login ticket from the html content
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                throw new IpaApiEntityNullException();
            }
            // FIXME don't use EntityUtils.toString
            // FIXME parseContentForError should be used here
            String searchResult = EntityUtils.toString(entity);
            if (searchResult.length() == 0 || searchResult.matches("^\\s+$")) {
                return new ArrayList<String>();
            }
            analysisIds = new ArrayList<String>(Arrays.asList(searchResult.split(",")));
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        return analysisIds;
    }

    // FIXME this parameter list does not reflect what the IPA-API is capable of
    /**
     * Uploads a dataset and initiates an analysis on the server.
     * <p>
     * This method calls the {@code dataanalysis} endpoint of the IPA API,
     * allowing to upload a dataset and starting an analysis. This method does not
     * (yet) provide all parameters of the IPA API supports.
     * <p>
     * For details of the meaning of the parameters refer to the IPA Integration
     * Module documentation.
     *
     * @param projectName
     *          name of the project folder in IPA
     * @param datasetName
     *          name of the dataset saved in IPA (make sure it does not exist in
     *          {@code projectName})
     * @param analysisName
     *          name of the analysis saved in IPA (make sure it does not exist in
     *          {@code projectName})
     * @param geneIdType
     *          gene or protein identifier type (eg. {@code entrezgene},
     *          {@code affymetrix}, ...)
     * @param geneId
     *          gene or protein ids
     * @param expValueType
     *          expression value type (eg. {@code foldchange}, {@code pvalue}, ...
     * @param expValue
     *          expression values corresponding to the {@code geneId} entries
     * @param expValueType2
     *          expression value type of {@code expValue}
     * @param expValue2
     *          second expression values corresponding to the {@code geneId}
     *          entries
     * @param applicationName
     *          the name of the application calling the API (this will be saved in
     *          an analysis's metadata)
     */
    public synchronized void dataAnalysis(String projectName, String datasetName, String analysisName,
            String geneIdType, List<String> geneId, String expValueType, List<? extends Number> expValue,
            String expValueType2, List<? extends Number> expValue2, String applicationName) throws IpaApiException {
        if (geneId == null) {
            throw new IllegalArgumentException("geneId cannot be null");
        }
        final String path = "/pa/api/v2/dataanalysis";
        ArrayList<NameValuePair> postParameters = new ArrayList<NameValuePair>();
        // this is needed to make sure no interaction is required
        postParameters.add(new BasicNameValuePair("ipaview", "projectmanager"));
        if (projectName != null) {
            postParameters.add(new BasicNameValuePair("projectname", projectName));
        }
        if (datasetName != null) {
            postParameters.add(new BasicNameValuePair("datasetname", datasetName));
        }
        if (analysisName != null) {
            postParameters.add(new BasicNameValuePair("analysisname", analysisName));
        }
        if (geneIdType != null) {
            postParameters.add(new BasicNameValuePair("geneidtype", geneIdType));
        }
        for (String val : geneId) {
            postParameters.add(new BasicNameValuePair("geneid", val));
        }
        if (expValueType != null) {
            postParameters.add(new BasicNameValuePair("expvaltype", expValueType));
        }
        for (Number val : expValue) {
            postParameters.add(new BasicNameValuePair("expvalue", val.toString()));
        }
        if (expValueType2 != null) {
            postParameters.add(new BasicNameValuePair("expvaltype2", expValueType2));
        }
        for (Number val : expValue2) {
            postParameters.add(new BasicNameValuePair("expval2", val.toString().toString()));
        }
        if (applicationName != null) {
            postParameters.add(new BasicNameValuePair("applicationname", applicationName));
        }

        try {
            URI uri = new URIBuilder(serverUrl).setPath(path).build();

            CloseableHttpClient hc = this.authenticatedClient();
            HttpClientContext context = this.clientContext();
            HttpPost httppost = new HttpPost(uri);
            httppost.setEntity(new UrlEncodedFormEntity(postParameters));
            try (CloseableHttpResponse response = hc.execute(httppost, context)) {
                log.info("post " + uri + " status " + response.getStatusLine().getStatusCode());
                if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                    HttpEntity entity = response.getEntity();
                    if (entity != null) {
                        log.warn("dataAnalysis http status not ok, content: " + EntityUtils.toString(entity));
                    }
                    throw new IpaApiHttpResponseException(uri, response);
                }
                HttpEntity entity = response.getEntity();
                // boolean gotJnlp = false;
                try (InputStreamReader isr = new InputStreamReader(entity.getContent());
                        BufferedReader reader = new BufferedReader(isr)) {
                    String contentLine = null;
                    // FIXME check for jnlp? final Pattern jnlpRegex = Pattern.compile(".*<jnlp.*");
                    while ((contentLine = reader.readLine()) != null) {
                        IpaErrorIndication err = containsStringIndicatingError(contentLine);
                        if (err != null) {
                            throw new IpaApiException(parseContentForError(reader, err).toString());
                        }

                        // Matcher m = jnlpRegex.matcher(str);
                        // if (m.find())
                        // gotJnlp = true;
                    }
                }
            }
        } catch (URISyntaxException | IOException e) {
            throw new IpaApiException(e);
        }
    }

    /**
     * Returns the analysis name to id mapping for all analyses of a project with
     * the given id.
     * <p>
     * All analyses of the project with the id {@code projectId} are exported and
     * their name to id mapping is returned.
     *
     * @param projectId
     *          the project id, can be found by right-clicking on a project in the
     *          IPA client
     * @return the mapping from analysis name to id
     */
    public synchronized Map<String, String> analysisNameToIdForProjectId(String projectId) throws IpaApiException {
        List<IpaAnalysisResults> ipaSettings = null;
        ArrayList<String> resultsTypes = new ArrayList<String>();
        resultsTypes.add("settings");
        try (CloseableHttpResponse rawAnalyses = exportRawAnalyses(projectId, new ArrayList<String>(),
                resultsTypes)) {
            HttpEntity entity = rawAnalyses.getEntity();
            if (entity == null) {
                throw new IpaApiEntityNullException();
            }
            ipaSettings = parseExport(entity.getContent());
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        Map<String, String> anameToId = new HashMap<String, String>();
        for (IpaAnalysisResults analysis : ipaSettings) {
            anameToId.put(analysis.getAnalysisName(), analysis.getAnalysisId());
        }
        return anameToId;
    }

    /**
     * Returns the analysis name to id mapping for all analyses of a project with
     * the given name.
     * <p>
     * All analyses of the project with the name {@code projectName} are exported
     * and their name to id mapping is returned.
     * <p>
     * You should use {@link analysisNameToIdForProjectId} if possible because it
     * is faster, but currently there is now way to programmatically retrieve the
     * project id from its name.
     *
     * @param projectName
     *          the project name
     * @return the mapping from analysis name to id
     */
    public synchronized Map<String, String> analysisNameToIdForProjectName(String projectName)
            throws IpaApiException {
        return this.analysisNameToIdForProjectName(projectName, "*");
    }

    /**
     * Returns the analysis name to id mapping for all analyses matching
     * {@code query} of a project with the given name.
     * <p>
     * All analyses with a name matching {code query} of the project with the name
     * {@code projectName} are exported and their name to id mapping is returned.
     *
     * @param projectName
     *          the project name
     * @param query
     *          query which analysis names should match
     * @return the mapping from analysis name to id
     */
    public synchronized Map<String, String> analysisNameToIdForProjectName(String projectName, String query)
            throws IpaApiException {
        Map<String, String> anameToId = new HashMap<String, String>();
        // first perform a search that retrieves all analysis ids of all projects
        List<String> analysisIds = this.searchAnalysis(query);
        if (analysisIds.size() == 0) {
            return anameToId;
        }
        // get detailed information about all analyses
        List<IpaAnalysisResults> ipaSettings = null;
        ArrayList<String> resultsTypes = new ArrayList<String>();
        resultsTypes.add("settings");
        try (CloseableHttpResponse rawAnalyses = exportRawAnalyses(null, analysisIds, resultsTypes)) {
            HttpEntity entity = rawAnalyses.getEntity();
            if (entity == null) {
                throw new IpaApiEntityNullException();
            }
            ipaSettings = parseExport(entity.getContent());
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        // filter by project name and populate the map
        for (IpaAnalysisResults analysis : ipaSettings) {
            if (analysis.getProjectName().equals(projectName)) {
                // FIXME add assertion analysisName does not exist in Map (should
                // never happen)
                anameToId.put(analysis.getAnalysisName(), analysis.getAnalysisId());
            }
        }
        return anameToId;
    }

    /**
     * Parses analysis results and returns it as List of
     * {@code IpaAnalysisResults}.
     *
     * @param is
     *          the input stream, usually the content of a http response
     * @return the results
     */
    public static List<IpaAnalysisResults> parseExport(InputStream is) throws IpaApiException {
        // the different sections we are expecting
        final List<String> sections = Collections.unmodifiableList(
                Arrays.asList("Analysis Details", "Canonical Pathways", "Upstream Regulators", "Causal Networks",
                        "Causal Networks", "Diseases and Bio Functions", "Tox Functions", "Regulator Effects",
                        "Networks", "My Lists", "Tox Lists", "My Pathways", "Analysis Ready Molecules"));
        // these sections contain a table
        final List<String> tableSections = Collections
                .unmodifiableList(Arrays.asList("Canonical Pathways", "Upstream Regulators",
                        "Diseases and Bio Functions", "Tox Functions", "Networks", "Analysis Ready Molecules"));
        // the columns which should be marked as numeric
        final List<String> numericColumns = Collections
                .unmodifiableList(Arrays.asList("-log(p-value)", "zScore", "Ratio", "Exp Log Ratio",
                        "Exp Fold Change", "Activation z-score", "Bias Term", "Bias-corrected z-score",
                        "p-value of overlap", "p-Value", "# Molecules", "Score", "Focus Molecules", "Exp p-value"));
        // the regular expression denoting the beginning of a new section
        final Pattern secRgx = Pattern.compile("^(" + StringUtils.join(sections, "|") + ") for (.*)->(.*)->(.*)");

        List<IpaAnalysisResults> ipaResults = new ArrayList<IpaAnalysisResults>();
        try {
            try (InputStreamReader isr = new InputStreamReader(is);
                    BufferedReader reader = new BufferedReader(isr)) {
                IpaAnalysisResults currentIpaResults = new IpaAnalysisResults();
                currentIpaResults.setAnalysisName("");
                String currentSection = "";
                String currentAnalysisId = null;
                SimpleTable currentTable = new SimpleTable();
                String currentLine;
                int sectionLine = 0;

                while ((currentLine = reader.readLine()) != null) {
                    // FIXME if there is stored something like `<html` in the table
                    // this will throw
                    IpaErrorIndication err = containsStringIndicatingError(currentLine);
                    if (err != null) {
                        throw new IpaApiException(parseContentForError(reader, err).toString());
                    }

                    // skip empty lines
                    if (currentLine.isEmpty()) {
                        continue;
                    }

                    String[] parts = currentLine.split("\t");
                    Matcher m = secRgx.matcher(parts[0]);
                    if (m.find()) {
                        String newSectionName = m.group(1);
                        String newSectionWorkspace = m.group(2);
                        String newSectionProjectName = m.group(3);
                        String newSectionAnalysisName = m.group(4);
                        // this is the beginning of a new section
                        if (newSectionAnalysisName.length() == 0) {
                            throw new IpaApiParserException("no analysis name");
                        }

                        // store the old section if there is one
                        if (currentTable.isInitialised()) {
                            currentTable.setName(currentSection);
                            // remove duplicated information from table (Analysis
                            // Name is constant)
                            if (currentTable.header().get(0).equals("Analysis")
                                    || currentTable.header().get(1).equals("Analysis")) {
                                currentTable.deleteColumn("Analysis");
                            }
                            currentIpaResults.getTables().put(currentTable.getName(), currentTable);
                        }

                        if (!newSectionAnalysisName.equals(currentIpaResults.getAnalysisName())) {
                            // this is a new analysis, store the old one
                            if (currentIpaResults.getAnalysisName().length() > 0) {
                                if (currentAnalysisId == null || currentAnalysisId.length() == 0) {
                                    throw new IpaApiParserException("no analysis ID");
                                }
                                currentIpaResults.setAnalysisId(currentAnalysisId);
                                ipaResults.add(currentIpaResults);
                            }
                            currentAnalysisId = null;
                            currentIpaResults = new IpaAnalysisResults();
                            currentIpaResults.setWorkspace(newSectionWorkspace);
                            currentIpaResults.setProjectName(newSectionProjectName);
                            currentIpaResults.setAnalysisName(newSectionAnalysisName);
                        }
                        sectionLine = 0;
                        currentSection = newSectionName;
                        currentTable = new SimpleTable();
                        if (!currentIpaResults.getWorkspace().equals(newSectionWorkspace)
                                || !currentIpaResults.getProjectName().equals(newSectionProjectName)
                                || !currentIpaResults.getAnalysisName().equals(newSectionAnalysisName)) {
                            throw new IpaApiParserException("table metadata mismatch: " + "current("
                                    + currentIpaResults.getWorkspace() + "," + currentIpaResults.getProjectName()
                                    + "," + currentIpaResults.getAnalysisName() + "), " + "new("
                                    + newSectionWorkspace + "," + newSectionProjectName + ","
                                    + newSectionAnalysisName + ")");
                        }
                    } else {
                        sectionLine += 1;
                        if (sectionLine == 1) {
                            if (tableSections.contains(currentSection)) {
                                // this is the header of a new table; remove
                                // trailing empty column name
                                List<String> header = new ArrayList<String>();
                                for (String p : parts) {
                                    header.add(p.trim());
                                }
                                if (header.get(header.size() - 1).length() == 0) {
                                    header.remove(header.size() - 1);
                                }
                                currentTable.setHeader(header);
                                List<SimpleColumnType> columnTypes = new ArrayList<SimpleColumnType>();
                                for (String c : header) {
                                    if (numericColumns.contains(c)) {
                                        columnTypes.add(SimpleColumnType.NUMERIC);
                                    } else {
                                        columnTypes.add(SimpleColumnType.STRING);
                                    }
                                }
                                currentTable.setColumnTypes(columnTypes);
                            }
                        } else {
                            if (tableSections.contains(currentSection)) {
                                if (currentTable.header().size() > parts.length) {
                                    throw new IpaApiParserException("could not parse table (expected "
                                            + currentTable.header().size() + " elements, got " + parts.length);
                                }
                                List<String> row = new ArrayList<String>(Arrays.asList(parts));
                                // beautify certain columns: delete trailing comma, add whitespace after comma
                                for (int i = 0; i < currentTable.header().size(); ++i) {
                                    if (currentTable.header().get(i).equals("Molecules")
                                            || currentTable.header().get(i).equals("Molecules in Network")
                                            || currentTable.header().get(i).equals("Target molecules in dataset")) {
                                        row.set(i, row.get(i).replaceAll(",$", ""));
                                        row.set(i, row.get(i).replaceAll(",([^\\s])", ", $1"));
                                    }
                                }
                                currentTable.addRow(row);
                            } else if (currentSection.equals("Analysis Details")) {
                                if (parts.length == 2) {
                                    if (parts[0].equals("Analysis ID")) {
                                        currentAnalysisId = parts[1];
                                    }
                                }
                            }
                        }
                    }
                }
                // store the last section
                if (currentTable.isInitialised()) {
                    currentTable.setName(currentSection);
                    // remove duplicated information from table (Analysis Name is
                    // constant)
                    if (currentTable.header().get(0).equals("Analysis")
                            || currentTable.header().get(1).equals("Analysis")) {
                        currentTable.deleteColumn("Analysis");
                    }
                    currentIpaResults.getTables().put(currentTable.getName(), currentTable);
                }

                // store the last analysis
                if (currentIpaResults.getAnalysisName().length() > 0) {
                    if (currentAnalysisId == null || currentAnalysisId.length() == 0) {
                        throw new IpaApiParserException("no analysis ID");
                    }
                    currentIpaResults.setAnalysisId(currentAnalysisId);
                    ipaResults.add(currentIpaResults);
                }
            }
        } catch (IOException e) {
            throw new IpaApiException(e);
        }

        return ipaResults;
    }
}