com.ibm.ws.lars.rest.RepositoryContext.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.ws.lars.rest.RepositoryContext.java

Source

/*******************************************************************************
 * Copyright (c) 2015 IBM Corp.
 *
 * 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.ibm.ws.lars.rest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.junit.rules.ExternalResource;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ibm.ws.lars.rest.exceptions.InvalidJsonAssetException;
import com.ibm.ws.lars.rest.model.Asset;
import com.ibm.ws.lars.rest.model.AssetList;
import com.ibm.ws.lars.rest.model.Attachment;
import com.ibm.ws.lars.rest.model.AttachmentList;
import com.ibm.ws.lars.testutils.FatUtils;

/**
 * Context used by testcases to perform HTTP operations against one LARS server. Tests should use
 * this class rather than directly calling httpclient (or the java.net APIs!) directly.
 *
 * A RepositoryContext aims to encapsulate every parameter needed to communicate with the server
 * (host, port, protocol, user, password etc). Some tests may use more than one RepositoryContext
 * (for example, security tests that need to log on as more than one user).
 */
public class RepositoryContext extends ExternalResource {
    private final boolean cleanRepository;

    private final String hostname;
    private final int portNumber;
    private final String protocol;

    private final String fullURL;

    private CloseableHttpClient httpClient;

    private final String user;
    private final String password;
    private UsernamePasswordCredentials credentials;

    private HttpHost targetHost;
    private HttpClientContext context;

    private final static ObjectMapper jsonReader = new ObjectMapper();

    /* package */enum Protocol {
        HTTP, HTTPS
    }

    @SuppressWarnings("serial")
    /* package */static final Map<Protocol, String> DEFAULT_URLS = new HashMap<Protocol, String>() {
        {
            {
                put(Protocol.HTTP,
                        "http://localhost:" + FatUtils.LIBERTY_PORT_HTTP + FatUtils.LARS_APPLICATION_ROOT);
                put(Protocol.HTTPS,
                        "https://localhost:" + FatUtils.LIBERTY_PORT_HTTPS + FatUtils.LARS_APPLICATION_ROOT);
            }
        }
    };

    /**
     * Special constant that can be passed as an expected response code to indicate that either a
     * 401 Not Authorized or a 403 Forbidden response is expected.
     */
    public static final int RC_REJECT = -2;

    @Override
    protected void before() throws InvalidJsonAssetException, IOException, KeyManagementException,
            NoSuchAlgorithmException, KeyStoreException {

        targetHost = new HttpHost(hostname, portNumber, protocol);

        /* Create the HTTPClient that we use to make all HTTP calls */
        HttpClientBuilder b = HttpClientBuilder.create();

        // Trust all certificates
        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
            @Override
            public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                return true;
            }
        }).build();
        b.setSslcontext(sslContext);

        // By default, it will verify the hostname in the certificate, which should be localhost
        // and therefore should match. If we start running these tests against a LARS server on
        // a different host then we may need disable hostname verification.

        context = HttpClientContext.create();

        httpClient = b.build();

        /*
         * Create the HTTPClientContext with the appropriate credentials. We'll use this whenever we
         * make an HTTP call.
         */
        if (user != null && password != null) {
            credentials = new UsernamePasswordCredentials(user, password);

            CredentialsProvider credsProvider = new BasicCredentialsProvider();
            credsProvider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                    credentials);

            AuthCache authCache = new BasicAuthCache();
            BasicScheme basicAuth = new BasicScheme();
            authCache.put(targetHost, basicAuth);

            context.setCredentialsProvider(credsProvider);
            context.setAuthCache(authCache);
        }

        /* Clean the repository but only if the client asked us to. */
        if (cleanRepository) {
            cleanRepo();
        }
    }

    @Override
    protected void after() {
        // Clean the repository if the client asked us to when creating this HTTPContext
        if (cleanRepository) {
            try {
                cleanRepo();
            } catch (InvalidJsonAssetException | IOException e) {
                fail("TODO");
            }
        }

        try {
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
            fail("Unexpected exception closing httpClient: " + e);
        }
    }

    public RepositoryContext(String url, String user, String password, boolean cleanRepository) {

        this.cleanRepository = cleanRepository;

        URI uri;
        try {
            uri = new URI(url);
        } catch (URISyntaxException ex) {
            // test code, hide this exception
            throw new RuntimeException(ex);
        }

        // Ultimately, these parameters may vary and therefore may need to be
        // passed in. For now, they are hardcoded.
        this.protocol = uri.getScheme();
        this.hostname = uri.getHost();
        this.portNumber = uri.getPort();
        this.user = user;
        this.password = password;

        fullURL = uri.toString();
    }

    protected static RepositoryContext createAsAdmin(String url, boolean cleanRepository) {
        return new RepositoryContext(url, "admin", "passw0rd", cleanRepository);
    }

    protected static RepositoryContext createAsAdmin(boolean cleanRepository, Protocol protocol) {
        String url = DEFAULT_URLS.get(protocol);
        if (url == null) {
            throw new AssertionError("This should never happen. Didn't find url for " + protocol);
        }
        return createAsAdmin(url, cleanRepository);
    }

    protected static RepositoryContext createAsUser(Protocol protocol) {
        String url = DEFAULT_URLS.get(protocol);
        if (url == null) {
            throw new AssertionError("This should never happen. Didn't find url for " + protocol);
        }
        return new RepositoryContext(url, "user", "passw0rd", false);
    }

    /**
     * @return the user
     */
    public String getUser() {
        return user;
    }

    public String getFullURL() {
        return fullURL;
    }

    public String doPost(String url, String content, int expectedStatusCode)
            throws ClientProtocolException, IOException {
        HttpPost post = new HttpPost(fullURL + url);
        return doRequest(post, content, expectedStatusCode);
    }

    public String doPut(String url, String content, int expectedStatusCode)
            throws ClientProtocolException, IOException {
        HttpPut put = new HttpPut(fullURL + url);
        return doRequest(put, content, expectedStatusCode);
    }

    public String doGet(String url, int expectedStatusCode) throws ClientProtocolException, IOException {
        HttpGet get = new HttpGet(fullURL + url);
        return doRequest(get, expectedStatusCode);
    }

    public String doDelete(String url, int expectedStatusCode) throws ClientProtocolException, IOException {
        HttpDelete delete = new HttpDelete(fullURL + url);
        return doRequest(delete, expectedStatusCode);
    }

    public HttpResponse doHead(String url, int expectedStatusCode) throws ClientProtocolException, IOException {
        HttpHead head = new HttpHead(fullURL + url);
        HttpResponse response = httpClient.execute(targetHost, head, context);

        assertStatusCode(expectedStatusCode, response);
        return response;
    }

    public String doRequest(HttpEntityEnclosingRequestBase request, String content, int expectedStatusCode)
            throws ClientProtocolException, IOException {
        HttpEntity requestEntity = new ByteArrayEntity(content.getBytes(StandardCharsets.UTF_8),
                ContentType.APPLICATION_JSON);
        request.setEntity(requestEntity);

        return doRequest(request, expectedStatusCode);
    }

    public String doPostMultipart(String url, String name, String json, byte[] content, ContentType contentType,
            int expectedStatusCode) throws ClientProtocolException, IOException {
        HttpPost post = new HttpPost(fullURL + url);
        HttpEntity requestEntity = MultipartEntityBuilder.create()
                .addPart("attachmentInfo", new StringBody(json, ContentType.APPLICATION_JSON))
                .addPart(name, new ByteArrayBody(content, contentType, name)).build();
        post.setEntity(requestEntity);

        return doRequest(post, expectedStatusCode);
    }

    public String doRequest(HttpRequestBase request, int expectedStatusCode)
            throws ClientProtocolException, IOException {

        HttpResponse response = httpClient.execute(targetHost, request, context);

        assertStatusCode(expectedStatusCode, response);

        HttpEntity responseEntity = response.getEntity();
        String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
        return responseString;
    }

    public byte[] doGetAsByteArray(String url, int expectedStatusCode) throws ClientProtocolException, IOException {
        HttpGet get = new HttpGet(fullURL + url);
        HttpResponse response = httpClient.execute(targetHost, get, context);

        assertStatusCode(expectedStatusCode, response);
        return EntityUtils.toByteArray(response.getEntity());
    }

    private void assertStatusCode(int expectedStatusCode, HttpResponse response)
            throws ParseException, IOException {
        StatusLine statusLine = response.getStatusLine();

        int actualStatusCode = statusLine.getStatusCode();

        boolean statusCodeMatches = expectedStatusCode == actualStatusCode
                || (expectedStatusCode == RC_REJECT && (actualStatusCode == 403 || actualStatusCode == 401));

        if (!statusCodeMatches) {
            String failMessage = "Unexpected status code: " + actualStatusCode + "; expected: " + expectedStatusCode
                    + "\n";
            failMessage += "Full HTTP response:";
            try {
                failMessage += EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            } catch (UnsupportedEncodingException e) {
                failMessage += "Unable to get full HTTP response!";

            }

            fail(failMessage);
        }
    }

    private void cleanRepo() throws InvalidJsonAssetException, IOException {
        deleteAllAssets();
        AssetList assets = doGetAllAssets();
        assertEquals("The repository is not empty", 0, assets.size());
    }

    void deleteAllAssets() throws IOException, InvalidJsonAssetException {
        AssetList assets = doGetAllAssets();
        for (Asset asset : assets) {
            deleteAsset(asset.get_id(), -1);
        }
    }

    protected void doGetAllAssetsBad(int expectedRC)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {
        doGet("/assets", expectedRC);
    }

    protected AssetList doGetAllAssets(int expectedRC)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {
        String response = doGet("/assets", expectedRC);

        // It is never acceptable for the server to return us a non-valid
        // JSON string in response to this call so we can parse the JSON
        // and return it to the client.
        return AssetList.jsonArrayToAssetList(response);
    }

    protected AssetList doGetAllAssets() throws IOException, InvalidJsonAssetException, ParseException {
        return doGetAllAssets(200);
    }

    /**
     * expectedStatusCode should be -1 to indicate a standard response is expected
     *
     * @param id the id of the attachment to delete or, oddly, if it's null then we want to DELETE
     *            the /assets URL (which should fail).
     * @param expectedStatusCode
     * @throws IOException
     */
    protected String deleteAsset(String id, int expectedStatusCode) throws IOException {
        // 204 for 'deleted, no content returned'
        if (expectedStatusCode == -1) {
            expectedStatusCode = 204;
        }

        String url = "/assets";
        if (id != null) {
            url += "/" + id;
        }

        return doDelete(url, expectedStatusCode);
    }

    boolean repositoryIsEmpty() throws IOException, InvalidJsonAssetException {
        AssetList assets = doGetAllAssets();
        if (assets.size() == 0) {
            return true;
        }
        return false;
    }

    Asset updateAssetNoAttachments(Asset toUpdate, int expectedResponse)
            throws IOException, InvalidJsonAssetException {
        String assetJson = doPut("/assets/" + toUpdate.get_id(), toUpdate.toJson(), expectedResponse);

        if (assetJson != null && !!!assetJson.equals("")) {
            return Asset.deserializeAssetFromJson(assetJson);
        } else {
            return null;
        }
    }

    Asset getAsset(String id) throws IOException, InvalidJsonAssetException {
        String assetJson = doGet("/assets/" + id, 200);
        return Asset.deserializeAssetFromJson(assetJson);
    }

    /**
     * Get asset with the expectation that it will fail
     */
    String getBadAsset(String id, int expectedStatusCode) throws IOException {
        return doGet("/assets/" + id, expectedStatusCode);
    }

    AssetList getAllAssets(String filter) throws IOException, InvalidJsonAssetException {
        // escape '|' character used in or-filtering
        filter = filter.replace("|", "%7C");
        String assetsJson = doGet("/assets?" + filter, 200);
        return AssetList.jsonArrayToAssetList(assetsJson);
    }

    List<Map<String, Object>> getAssetSummary(String parameters)
            throws JsonParseException, JsonMappingException, IOException {
        String resultString = doGet("/assets/summary?" + parameters, 200);
        return jsonReader.readValue(resultString, new TypeReference<List<Map<String, Object>>>() {
        });
    }

    void getBadAssetSummary(String parameters, int expectedRC) throws IOException {
        doGet("/assets/summary?" + parameters, expectedRC);
    }

    int getAssetCount(String parameters) throws IOException {
        HttpResponse response = doHead("/assets?" + parameters, HttpStatus.SC_NO_CONTENT);
        return Integer.parseInt(response.getFirstHeader("count").getValue());
    }

    protected Attachment doPostAttachmentNoContent(String assetId, String name, Attachment attachment)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {
        List<NameValuePair> qparams = new ArrayList<>();
        qparams.add(new BasicNameValuePair("name", name));
        String url = "/assets/" + assetId + "/attachments?" + URLEncodedUtils.format(qparams, "UTF-8");

        String response = doPost(url, attachment.toJson(), 200);

        Attachment createdAttachment = Attachment.jsonToAttachment(response);

        return createdAttachment;
    }

    protected void doPostBadAttachmentNoContent(String assetId, String name, Attachment attachment, int expectedRC,
            String expectedMessage) throws ClientProtocolException, IOException, InvalidJsonAssetException {
        List<NameValuePair> qparams = new ArrayList<>();
        qparams.add(new BasicNameValuePair("name", name));
        String url = "/assets/" + assetId + "/attachments?" + URLEncodedUtils.format(qparams, "UTF-8");
        String message = doPost(url, attachment.toJson(), expectedRC);

        if (expectedMessage != null) {
            String errorMessage = parseErrorObject(message.toString());
            assertEquals("Unexpected message from server", expectedMessage, errorMessage);
        }
    }

    protected Attachment doPostAttachmentWithContent(String assetId, String name, Attachment attachment,
            byte[] content, ContentType contentType)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {

        List<NameValuePair> qparams = new ArrayList<>();
        qparams.add(new BasicNameValuePair("name", name));
        String url = "/assets/" + assetId + "/attachments?" + URLEncodedUtils.format(qparams, "UTF-8");
        String response = doPostMultipart(url, name, attachment.toJson(), content, contentType, 200);

        return Attachment.jsonToAttachment(response);
    }

    protected void doPostBadAttachmentWithContent(String assetId, String name, Attachment attachment,
            byte[] content, ContentType contentType, int expectedRC, String expectedMessage)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {

        List<NameValuePair> qparams = new ArrayList<>();
        qparams.add(new BasicNameValuePair("name", name));
        String url = "/assets/" + assetId + "/attachments?" + URLEncodedUtils.format(qparams, "UTF-8");

        String response = doPostMultipart(url, name, attachment.toJson(), content, contentType, expectedRC);

        if (expectedMessage != null) {
            String errorMessage = parseErrorObject(response);
            assertEquals("Unexpected message from server", expectedMessage, errorMessage);
        }
    }

    protected AttachmentList doGetAllAttachmentsForAsset(String assetId)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {
        String response = doGet("/assets/" + assetId + "/attachments/", 200);
        return AttachmentList.jsonToAttachmentList(response.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * If an asset has only one attachment, this method retrieves it, asserts that its id is correct
     * and then returns it.
     *
     * @throws InvalidJsonAssetException
     * @throws IOException
     * @throws ClientProtocolException
     */
    protected Attachment doGetOnlyAttachment(String assetId, String attachmentId)
            throws ClientProtocolException, IOException, InvalidJsonAssetException {
        AttachmentList retrievedAttachments = doGetAllAttachmentsForAsset(assetId);
        assertEquals("Asset is expected to have just one attachment", 1, retrievedAttachments.size());
        Attachment retrievedAttachment = retrievedAttachments.get(0);

        assertEquals("Incorrect attachment id", attachmentId, retrievedAttachment.get_id());
        assertEquals("Asset id on attachment is expected to match owning asset", assetId,
                retrievedAttachment.getAssetId());

        return retrievedAttachment;
    }

    /**
     * Designed to be called to try to retrieve an attachment and to expect an error return code to
     * come back from the server (ie only use this method in error cases).
     *
     * @throws IOException
     */
    protected void doGetAttachmentContentInError(String assetId, String attachmentId, String attachmentName,
            int expectedStatusCode, String expectedErrorMessage) throws IOException {
        String url = "/assets/" + assetId + "/attachments/" + attachmentId + "/" + attachmentName;
        String response = doGet(url, expectedStatusCode);
        if (expectedErrorMessage != null) {
            assertEquals(expectedErrorMessage, parseErrorObject(response));
        }
    }

    protected byte[] doGetAttachmentContent(String assetId, String attachmentId, String attachmentName,
            int expectedRC) throws IOException {
        String url = "/assets/" + assetId + "/attachments/" + attachmentId + "/" + attachmentName;
        return doGetAsByteArray(url, expectedRC);
    }

    protected byte[] doGetAttachmentContent(String assetId, String attachmentId, String attachmentName)
            throws IOException {
        return doGetAttachmentContent(assetId, attachmentId, attachmentName, 200);
    }

    protected void doDeleteAttachment(String assetId, String attachmentId)
            throws ClientProtocolException, IOException {
        doDeleteAttachment(assetId, attachmentId, 204);
    }

    protected void doDeleteAttachment(String assetId, String attachmentId, int expectedResponseCode)
            throws ClientProtocolException, IOException {
        doDelete("/assets/" + assetId + "/attachments/" + attachmentId, expectedResponseCode);
    }

    protected void addBadAsset(Asset toAdd, int expectedResponseCode)
            throws IOException, InvalidJsonAssetException {
        doPost("/assets", toAdd.toJson(), expectedResponseCode);
    }

    protected Asset addAssetNoAttachments(Asset toAdd) throws IOException, InvalidJsonAssetException {
        String assetJson = doPost("/assets", toAdd.toJson(), 200);
        return Asset.deserializeAssetFromJson(assetJson);
    }

    String updateAssetState(String id, String stateAction, int expectedStatusCode)
            throws IOException, InvalidJsonAssetException {
        String json = "{\"action\":\"" + stateAction + "\"}";

        String response = doPut("/assets/" + id + "/state", json, expectedStatusCode);

        // A state update will get back nothing for a good request, or an error
        // for a bad one
        return response;
    }

    /**
     * If the supplied errorObject is valid JSON, look for a field called 'error' and return it. If
     * the object is not JSON, just return it (expecting that it is an HTML error message). If valid
     * JSON, but no 'error' field found, returns null.
     *
     * @param errorObject
     * @return
     * @throws ServerException if the errorObject is badly formed JSON (as supposed to not JSON at
     *             all).
     */
    public String parseErrorObject(String errorObject) throws ServerException {
        if (errorObject == null) {
            return null;
        }

        ObjectMapper mapper = new ObjectMapper();
        Map<?, ?> jsonMap = null;
        try {
            jsonMap = mapper.readValue(errorObject, Map.class);
        } catch (JsonParseException e) {
            // Assume the string is an html error message.
            return errorObject;
        } catch (JsonMappingException e) {
            // Assume the string is an html error message.
            return errorObject;
        } catch (IOException e) {
            // No idea what would cause this.
            throw new ServerException(e);
        }
        String errorMessage = readJsonStringValue(jsonMap, "message");
        if (errorMessage != null && !errorMessage.isEmpty()) {
            return errorMessage;
        } else {
            return errorObject;
        }
    }

    private static String readJsonStringValue(Map<?, ?> jsonMap, String key) {
        Object value = jsonMap.get(key);
        if (value == null) {
            return null;
        }

        if (value instanceof String) {
            return (String) value;
        }

        return null;
    }

    /**
     * Thrown when a test fails in such a way that it seems as though the server has fallen over (as
     * supposed to having produced an expected error message). This is likely to translate to a 500
     * error or similar.
     */
    static class ServerException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        ServerException(Throwable cause) {
            super(cause);
        }

        ServerException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}