com.hp.alm.ali.rest.client.AliRestClient.java Source code

Java tutorial

Introduction

Here is the source code for com.hp.alm.ali.rest.client.AliRestClient.java

Source

/*
 * Copyright 2013 Hewlett-Packard Development Company, L.P
 *
 * 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.hp.alm.ali.rest.client;

import com.hp.alm.ali.rest.client.exception.AuthenticationFailureException;
import com.hp.alm.ali.rest.client.exception.HttpStatusBasedException;
import com.hp.alm.ali.rest.client.filter.Filter;
import com.hp.alm.ali.rest.client.filter.IdentityFilter;
import com.hp.alm.ali.rest.client.filter.IssueTicketFilter;
import com.hp.alm.ali.rest.client.filter.ResponseFilter;
import com.hp.alm.ali.utils.XmlUtils;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.auth.AuthPolicy;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.io.IOUtils;
import org.jdom.Document;
import org.jdom.Element;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.*;

import static com.hp.alm.ali.utils.PathUtils.pathJoin;

/**
 * Thin wrapper around commons-http client that provides basic support for communication with the HP ALM REST.
 * <p>
 *
 * No higher level abstractions are currently provided, this library only simplifies following tasks:
 *
 * <ul>
 *     <li>authentication; {@link #login()}, {@link #logout()}, {@link SessionStrategy}</li>
 *     <li>domain/project listing: {@link #listDomains()}, {@link #listCurrentProjects()}</li>
 * </ul>
 *
 * <h3>URL composition</h3>
 *
 * Position based expansion is used in methods that accept template with parameters:
    
 * <pre>
 *     client.getForString("defects/{0}/attachments/{1}", 1001, "readme.txt")
 * </pre>
 *
 * In the DOMAIN/PROJECT of http://localhost:8080/qcbin expands to:
 *
 * <pre>
 *     http://localost:8080/qcbin/domains/DOMAIN/projects/PROJECT/defects/1001/attachments/readme.txt
 * </pre>
 */
public class AliRestClient implements RestClient {

    private static final LocationBasedBuilder<PostMethod> POST_BUILDER = new LocationBasedBuilder<PostMethod>() {
        @Override
        public PostMethod build(String location) {
            return new PostMethod(location);
        }
    };
    private static final LocationBasedBuilder<PutMethod> PUT_BUILDER = new LocationBasedBuilder<PutMethod>() {
        @Override
        public PutMethod build(String location) {
            return new PutMethod(location);
        }
    };
    private static final LocationBasedBuilder<GetMethod> GET_BUILDER = new LocationBasedBuilder<GetMethod>() {
        @Override
        public GetMethod build(String location) {
            return new GetMethod(location);
        }
    };
    private static final LocationBasedBuilder<DeleteMethod> DELETE_BUILDER = new LocationBasedBuilder<DeleteMethod>() {
        @Override
        public DeleteMethod build(String location) {
            return new DeleteMethod(location);
        }
    };
    public static final Set<Integer> AUTH_FAIL_STATUSES = Collections
            .unmodifiableSet(new HashSet<Integer>(Arrays.asList(401, 403)));

    private LinkedList<ResponseFilter> responseFilters;

    /**
     * Creates ALM ALI rest client
     *
     * @param location location of an alm server e.g. http://localost:8080/qcbin
     * @param domain   ALM domain
     * @param project  ALM project
     * @param userName ALM user name
     * @param password ALM user password
     */
    public static AliRestClient create(String location, String domain, String project, String userName,
            String password, SessionStrategy sessionStrategy) {
        return new AliRestClient(location, domain, project, userName, password, sessionStrategy);
    }

    static final String COOKIE_SSO_NAME = "LWSSO_COOKIE_KEY";
    static final String COOKIE_SESSION_NAME = "QCSession";

    static final String CLIENT_TYPE = "ALI_IDEA_plugin";

    public static final int DEFAULT_CLIENT_TIMEOUT = 30000;

    private final String location;
    private final String userName;
    private final String password;
    private volatile String domain;
    private volatile String project;

    private final SessionStrategy sessionStrategy;
    private final HttpClient httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
    private volatile SessionContext sessionContext = null;
    private volatile String encoding = "UTF-8";

    private AliRestClient(String location, String domain, String project, String userName, String password,
            SessionStrategy sessionStrategy) {
        if (location == null) {
            throw new IllegalArgumentException("location==null");
        }
        validateProjectAndDomain(domain, project);
        this.location = location;
        this.userName = userName;
        this.password = password;
        this.domain = domain;
        this.project = project;
        this.sessionStrategy = sessionStrategy;
        setTimeout(DEFAULT_CLIENT_TIMEOUT);
        httpClient.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(0, false));

        responseFilters = new LinkedList<ResponseFilter>();
        responseFilters.add(new IssueTicketFilter());
    }

    private void validateProjectAndDomain(String domain, String project) {
        if (domain == null && project != null) {
            throw new IllegalArgumentException("When project is specified domain must be specified too.");
        }
    }

    @Override
    public void setTimeout(int timeout) {
        httpClient.getParams().setSoTimeout(timeout);
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(timeout);
    }

    @Override
    public void setHttpProxy(String proxyHost, int proxyPort) {
        httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
    }

    @Override
    public void setHttpProxyCredentials(String username, String password) {
        Credentials cred = new UsernamePasswordCredentials(username, password);
        AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT);
        httpClient.getState().setProxyCredentials(scope, cred);
    }

    @Override
    public String getEncoding() {
        return encoding;
    }

    @Override
    public SessionStrategy getSessionStrategy() {
        return sessionStrategy;
    }

    @Override
    public void setEncoding(String encoding) {
        if (encoding != null) {
            Charset.forName(encoding);
        }
        this.encoding = encoding;
    }

    @Override
    public void setDomain(String domain) {
        validateProjectAndDomain(domain, project);
        this.domain = domain;
    }

    @Override
    public void setProject(String project) {
        validateProjectAndDomain(domain, project);
        this.project = project;
    }

    @Override
    public String getDomain() {
        return domain;
    }

    @Override
    public String getProject() {
        return project;
    }

    @Override
    public List<Cookie> getCookies(String cookieName) {
        LinkedList<Cookie> ret = new LinkedList<Cookie>();
        for (Cookie cookie : httpClient.getState().getCookies()) {
            if (cookieName.equals(cookie.getName())) {
                ret.add(cookie);
            }
        }
        return ret;
    }

    @Override
    public synchronized void login() {
        // exclude the NTLM authentication scheme (requires NTCredentials we don't supply)
        List<String> authPrefs = new ArrayList<String>(2);
        authPrefs.add(AuthPolicy.DIGEST);
        authPrefs.add(AuthPolicy.BASIC);
        httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);

        // first try Apollo style login
        String authPoint = pathJoin("/", location, "/authentication-point/alm-authenticate");
        String authXml = createAuthXml();
        PostMethod post = initPostMethod(authPoint, authXml);
        ResultInfo resultInfo = ResultInfo.create(null);
        executeAndWriteResponse(post, resultInfo, Collections.<Integer>emptySet());

        if (resultInfo.getHttpStatus() == HttpStatus.SC_NOT_FOUND) {
            // try Maya style login
            Credentials cred = new UsernamePasswordCredentials(userName, password);
            AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT);
            httpClient.getParams().setParameter(HttpMethodParams.CREDENTIAL_CHARSET, "UTF-8");
            httpClient.getState().setCredentials(scope, cred);

            authPoint = pathJoin("/", location, "/authentication-point/authenticate");
            GetMethod get = new GetMethod(authPoint);
            resultInfo = ResultInfo.create(null);
            executeAndWriteResponse(get, resultInfo, Collections.<Integer>emptySet());
        }
        HttpStatusBasedException.throwForError(resultInfo);
        if (resultInfo.getHttpStatus() != 200) {
            // during login we only accept 200 status (to avoid redirects and such as seemingly correct login)
            throw new AuthenticationFailureException(resultInfo);
        }

        Cookie[] cookies = httpClient.getState().getCookies();
        Cookie ssoCookie = getSessionCookieByName(cookies, COOKIE_SSO_NAME);
        addTenantCookie(ssoCookie);

        //Since ALM 12.00 it is required explicitly ask for QCSession calling "/rest/site-session"
        //For all the rest of HP ALM / AGM versions it is optional
        String siteSessionPoint = pathJoin("/", location, "/rest/site-session");
        String sessionParamXml = createRestSessionXml();
        post = initPostMethod(siteSessionPoint, sessionParamXml);
        resultInfo = ResultInfo.create(null);
        executeAndWriteResponse(post, resultInfo, Collections.<Integer>emptySet());
        //AGM throws 403
        if (resultInfo.getHttpStatus() != HttpStatus.SC_FORBIDDEN) {
            HttpStatusBasedException.throwForError(resultInfo);
        }

        cookies = httpClient.getState().getCookies();
        Cookie qcCookie = getSessionCookieByName(cookies, COOKIE_SESSION_NAME);
        sessionContext = new SessionContext(location, ssoCookie, qcCookie);
    }

    private PostMethod initPostMethod(String restEndPoint, String xml) {
        PostMethod post = new PostMethod(restEndPoint);
        post.setRequestEntity(createRequestEntity(InputData.create(xml)));
        post.addRequestHeader("Content-type", "application/xml");

        return post;
    }

    private String createAuthXml() {
        Element authElem = new Element("alm-authentication");
        Element userElem = new Element("user");
        authElem.addContent(userElem);
        userElem.setText(userName);
        Element passElem = new Element("password");
        passElem.setText(password);
        authElem.addContent(passElem);
        return XMLOutputterFactory.getXMLOutputter().outputString(new Document(authElem));
    }

    private String createRestSessionXml() {
        Element sessionParamElem = new Element("session-parameters");
        Element clientTypeElem = new Element("client-type");
        sessionParamElem.addContent(clientTypeElem);
        clientTypeElem.setText(CLIENT_TYPE);
        return XMLOutputterFactory.getXMLOutputter().outputString(new Document(sessionParamElem));
    }

    private void addTenantCookie(Cookie ssoCookie) {
        if (ssoCookie != null) {
            Cookie tenant_id_cookie = new Cookie(ssoCookie.getDomain(), "TENANT_ID_COOKIE", "0");
            tenant_id_cookie.setDomainAttributeSpecified(true);
            tenant_id_cookie.setPath(ssoCookie.getPath());
            tenant_id_cookie.setPathAttributeSpecified(true);
            httpClient.getState().addCookie(tenant_id_cookie);
        }
    }

    private Cookie getSessionCookieByName(Cookie[] cookies, String name) {
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                return cookie;
            }
        }
        return null;
    }

    @Override
    public synchronized void logout() {
        if (sessionContext != null) {
            GetMethod get = new GetMethod(pathJoin("/", location, "/authentication-point/logout"));
            ResultInfo resultInfo = ResultInfo.create(null);
            executeAndWriteResponse(get, resultInfo, Collections.<Integer>emptySet());
            HttpStatusBasedException.throwForError(resultInfo);
            sessionContext = null;
        }
    }

    @Override
    public String getForString(String template, Object... params) {
        ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
        ResultInfo result = ResultInfo.create(responseBody);
        get(result, template, params);
        HttpStatusBasedException.throwForError(result);
        return responseBody.toString();
    }

    @Override
    public InputStream getForStream(String template, Object... params) {
        ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
        ResultInfo result = ResultInfo.create(responseBody);
        get(result, template, params);
        HttpStatusBasedException.throwForError(result);
        return new ByteArrayInputStream(responseBody.toByteArray());
    }

    @Override
    public int get(ResultInfo result, String template, Object... params) {
        GetMethod method = createMethod(domain, project, GET_BUILDER, null, template, params);
        executeHttpMethod(method, result);
        return result.getHttpStatus();
    }

    @Override
    public int put(InputData inputData, ResultInfo result, String template, Object... params) {
        PutMethod putMethod = createMethod(domain, project, PUT_BUILDER, createRequestEntity(inputData), template,
                params);
        setHeaders(putMethod, inputData.getHeaders());
        executeHttpMethod(putMethod, result);
        return result.getHttpStatus();
    }

    @Override
    public int delete(ResultInfo result, String template, Object... params) {
        DeleteMethod deleteMethod = createMethod(domain, project, DELETE_BUILDER, null, template, params);
        executeHttpMethod(deleteMethod, result);
        return result.getHttpStatus();
    }

    @Override
    public int post(InputData data, ResultInfo result, String template, Object... params) {
        PostMethod postMethod = createMethod(domain, project, POST_BUILDER, createRequestEntity(data), template,
                params);
        setHeaders(postMethod, data.getHeaders());
        executeHttpMethod(postMethod, result);
        return result.getHttpStatus();
    }

    private void setHeaders(HttpMethod method, Map<String, String> headers) {
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            method.setRequestHeader(entry.getKey(), entry.getValue());
        }
    }

    private <T extends HttpMethod> T createMethod(String domain, String project, LocationBasedBuilder<T> builder,
            RequestEntity requestEntity, String template, Object... params) {
        String location = composeLocation(domain, project, template, params);
        T method = builder.build(location);
        if (requestEntity != null) {
            ((EntityEnclosingMethod) method).setRequestEntity(requestEntity);
        }
        return method;
    }

    private RequestEntity createRequestEntity(InputData inputData) {
        return inputData.getRequestEntity(encoding);
    }

    private void writeResponse(ResultInfo result, HttpMethod method, boolean writeBodyAndHeaders) {
        OutputStream bodyStream = result.getBodyStream();

        StatusLine statusLine = method.getStatusLine();
        if (statusLine != null) {
            result.setReasonPhrase(statusLine.getReasonPhrase());
        }
        try {
            result.setLocation(method.getURI().toString());
        } catch (URIException e) {
            throw new RuntimeException(e);
        }
        if (writeBodyAndHeaders) {
            Map<String, String> headersMap = result.getHeaders();
            Header[] headers = method.getResponseHeaders();
            for (Header header : headers) {
                headersMap.put(header.getName(), header.getValue());
            }
        }
        result.setHttpStatus(method.getStatusCode());
        Filter filter = new IdentityFilter(result);
        for (ResponseFilter responseFilter : responseFilters) {
            filter = responseFilter.applyFilter(filter, method, result);
        }
        if (writeBodyAndHeaders && bodyStream != null && method.getStatusCode() != 204) {
            try {
                InputStream responseBody = method.getResponseBodyAsStream();
                if (responseBody != null) {
                    IOUtils.copy(responseBody, filter.getOutputStream());
                    bodyStream.flush();
                    bodyStream.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private String composeLocation(String domain, String project, String template, Object... params) {
        String[] strParams = encodeParams(params);
        String substituted = MessageFormat.format(template, strParams);
        Object encDomain = encodeParams(new Object[] { domain })[0];
        Object encProject = encodeParams(new Object[] { project })[0];
        if (encDomain == null) {
            return pathJoin("/", location, "/rest", substituted);
        } else if (encProject == null) {
            return pathJoin("/", location, "/rest/domains", encDomain.toString(), substituted);
        }
        return pathJoin("/", location, "/rest/domains", encDomain.toString(), "projects", encProject.toString(),
                substituted);
    }

    private String[] encodeParams(Object params[]) {
        String enc = encoding;
        String result[] = new String[params.length];
        for (int i = 0; i < params.length; i++) {
            if (params[i] == null) {
                result[i] = null;
                continue;
            }
            String strPar = params[i].toString();
            try {
                if (enc == null) {
                    result[i] = strPar;
                } else {
                    result[i] = URLEncoder.encode(strPar, enc).replace("+", "%20");
                }
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e); //should never happen
            }
        }
        return result;
    }

    private boolean tryLogin(ResultInfo resultInfo, HttpMethod method) {
        try {
            login();
            return true;
        } catch (HttpStatusBasedException e) {
            resultInfo.setHttpStatus(e.getHttpStatus());
            resultInfo.setReasonPhrase(e.getReasonPhrase());
            try {
                resultInfo.setLocation(e.getLocation() + " [on-behalf-of: " + method.getURI().toString() + "]");
            } catch (URIException e2) {
                resultInfo.setLocation(e.getLocation() + " [on-behalf-of: " + method.getPath() + "]");
            }
            return false;
        }
    }

    private void executeHttpMethod(HttpMethod method, ResultInfo resultInfo) {
        switch (sessionStrategy) {
        case NONE:
            executeAndWriteResponse(method, resultInfo, Collections.<Integer>emptySet());
            return;
        case AUTO_LOGIN:
            SessionContext myContext = null;
            synchronized (this) {
                if (sessionContext == null) {
                    if (!tryLogin(resultInfo, method)) {
                        return;
                    }
                } else {
                    myContext = sessionContext;
                }
            }
            int statusCode = executeAndWriteResponse(method, resultInfo, AUTH_FAIL_STATUSES);
            if (AUTH_FAIL_STATUSES.contains(statusCode)) {
                synchronized (this) {
                    if (myContext == sessionContext) {
                        // login (unless someone else just did it)
                        if (!tryLogin(resultInfo, method)) {
                            return;
                        }
                    }
                }
                // and try again
                executeAndWriteResponse(method, resultInfo, Collections.<Integer>emptySet());
            }
        }
    }

    private int executeAndWriteResponse(HttpMethod method, ResultInfo resultInfo,
            Set<Integer> doNotWriteForStatuses) {
        try {
            int status = -1;
            // prevent creation of multiple implicit sessions
            // hopefully the first request to come (and fill in the session) will be short
            boolean hasQcSession;
            synchronized (this) {
                hasQcSession = hasQcSessionCookie();
                if (!hasQcSession) {
                    status = httpClient.executeMethod(method);
                }
            }
            if (hasQcSession) {
                status = httpClient.executeMethod(method);
            }
            writeResponse(resultInfo, method, !doNotWriteForStatuses.contains(status));
            return status;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            method.releaseConnection();
        }
    }

    private boolean hasQcSessionCookie() {
        for (Cookie cookie : httpClient.getState().getCookies()) {
            if (COOKIE_SESSION_NAME.equals(cookie.getName())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public List<String> listDomains() {
        return listValues(createMethod(null, null, GET_BUILDER, null, "domains"), "Domain");
    }

    @Override
    public List<String> listCurrentProjects() {
        if (domain == null) {
            throw new IllegalStateException("domain==null");
        }
        return listValues(createMethod(domain, null, GET_BUILDER, null, "projects"), "Project");
    }

    private List<String> listValues(GetMethod method, String entity) {
        ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
        ResultInfo resultInfo = ResultInfo.create(responseBody);
        executeHttpMethod(method, resultInfo);
        return getAttributeValues(new ByteArrayInputStream(responseBody.toByteArray()), entity, "Name");
    }

    private List<String> getAttributeValues(InputStream xml, String elemName, String attrName) {
        try {
            XMLInputFactory factory = XmlUtils.createBasicInputFactory();
            XMLStreamReader parser;
            parser = factory.createXMLStreamReader(xml);
            List<String> result = new LinkedList<String>();
            while (true) {
                int event = parser.next();
                if (event == XMLStreamConstants.END_DOCUMENT) {
                    parser.close();
                    break;
                }
                if (event == XMLStreamConstants.START_ELEMENT && elemName.equals(parser.getLocalName())) {

                    for (int i = 0; i < parser.getAttributeCount(); i++) {
                        String localName = parser.getAttributeLocalName(i);
                        if (attrName.equals(localName)) {
                            result.add(parser.getAttributeValue(i));
                            break;
                        }
                    }
                }
            }
            return result;
        } catch (XMLStreamException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                xml.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    private static interface LocationBasedBuilder<T extends HttpMethod> {

        T build(String location);

    }
}