Java tutorial
/* * 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); } }