Merged BRANCHES/DEV/V4.0-BUG-FIX to HEAD:

35637: RemoteCredentialsService and RemoteAlfrescoTicketService, with tests


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@35639 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Nick Burch
2012-04-24 16:12:47 +00:00
parent e542141b84
commit d0fdeafa2c
36 changed files with 4692 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
/*
* Copyright (C) 2005-2012 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.repo.remoteconnector;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorRequest;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorService;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
/**
* Helper wrapper around a Remote Request, to be performed by the
* {@link RemoteConnectorService}.
*
* @author Nick Burch
* @since 4.0.2
*/
public class RemoteConnectorRequestImpl implements RemoteConnectorRequest
{
public static final String HEADER_CONTENT_TYPE = "Content-Type";
private final String url;
private final String methodName;
private final HttpMethodBase method;
private final List<Header> headers = new ArrayList<Header>();
private RequestEntity requestBody;
public RemoteConnectorRequestImpl(String url, String methodName)
{
this(url, buildHttpClientMethod(url, methodName));
}
public RemoteConnectorRequestImpl(String url, Class<? extends HttpMethodBase> method)
{
this(url, buildHttpClientMethod(url, method));
}
private RemoteConnectorRequestImpl(String url, HttpMethodBase method)
{
this.url = url;
this.method = method;
this.methodName = method.getName();
}
protected static HttpMethodBase buildHttpClientMethod(String url, String method)
{
if ("GET".equals(method))
{
return new GetMethod(url);
}
if ("POST".equals(method))
{
return new PostMethod(url);
}
if ("PUT".equals(method))
{
return new PutMethod(url);
}
if ("DELETE".equals(method))
{
return new DeleteMethod(url);
}
if (TestingMethod.METHOD_NAME.equals(method))
{
return new TestingMethod(url);
}
throw new UnsupportedOperationException("Method '"+method+"' not supported");
}
protected static HttpMethodBase buildHttpClientMethod(String url, Class<? extends HttpMethodBase> method)
{
HttpMethodBase request = null;
try
{
request = method.getConstructor(String.class).newInstance(url);
}
catch(Exception e)
{
throw new AlfrescoRuntimeException("HttpClient broken", e);
}
return request;
}
public String getURL()
{
return url;
}
public String getMethod()
{
return methodName;
}
public HttpMethodBase getMethodInstance()
{
return method;
}
public String getContentType()
{
for (Header hdr : headers)
{
if (HEADER_CONTENT_TYPE.equals( hdr.getName() ))
{
return hdr.getValue();
}
}
return null;
}
public void setContentType(String contentType)
{
for (Header hdr : headers)
{
if (HEADER_CONTENT_TYPE.equals( hdr.getName() ))
{
hdr.setValue(contentType);
return;
}
}
headers.add(new Header(HEADER_CONTENT_TYPE, contentType));
}
public RequestEntity getRequestBody()
{
return requestBody;
}
public void setRequestBody(String body)
{
try
{
requestBody = new StringRequestEntity(body, getContentType(), "UTF-8");
}
catch (UnsupportedEncodingException e) {} // Can't occur
}
public void setRequestBody(byte[] body)
{
requestBody = new ByteArrayRequestEntity(body);
}
public void setRequestBody(InputStream body)
{
requestBody = new InputStreamRequestEntity(body);
}
public void setRequestBody(RequestEntity body)
{
requestBody = body;
}
public Header[] getRequestHeaders()
{
return headers.toArray(new Header[headers.size()]);
}
public void addRequestHeader(Header header)
{
addRequestHeaders(new Header[] {header});
}
public void addRequestHeader(String name, String value)
{
addRequestHeader(new Header(name,value));
}
public void addRequestHeaders(Header[] headers)
{
for (Header newHdr : headers)
{
// See if we already have one of these headers
Header existingHdr = null;
for (Header hdr : this.headers)
{
if (newHdr.getName().equals( hdr.getName() ))
{
existingHdr = hdr;
}
}
// Update or add as needed
if (existingHdr != null)
{
existingHdr.setValue(newHdr.getValue());
}
else
{
this.headers.add(newHdr);
}
}
}
/**
* An HttpClient Method implementation for the method "TESTING",
* which we use in certain unit tests
*/
private static class TestingMethod extends GetMethod
{
private static final String METHOD_NAME = "TESTING";
private TestingMethod(String url)
{
super(url);
}
@Override
public String getName()
{
return METHOD_NAME;
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2005-2012 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.repo.remoteconnector;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorRequest;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorResponse;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorService;
import org.apache.commons.httpclient.Header;
import org.apache.tika.io.IOUtils;
/**
* Helper wrapper around a Remote Request, to be performed by the
* {@link RemoteConnectorService}.
*
* @author Nick Burch
* @since 4.0.2
*/
public class RemoteConnectorResponseImpl implements RemoteConnectorResponse
{
private RemoteConnectorRequest request;
private String contentType;
private String charset;
private Header[] headers;
private InputStream bodyStream;
private byte[] bodyBytes;
/**
* Creates a new Response object with the data coming from a stream.
* Because of the HttpClient lifecycle, a HttpClient response
* InputStream shouldn't be used as cleanup is needed
*/
public RemoteConnectorResponseImpl(RemoteConnectorRequest request, String contentType,
String charset, Header[] headers, InputStream response)
{
this.request = request;
this.contentType = contentType;
this.charset = charset;
this.headers = headers;
this.bodyStream = response;
this.bodyBytes = null;
}
public RemoteConnectorResponseImpl(RemoteConnectorRequest request, String contentType,
String charset, Header[] headers, byte[] response)
{
this(request, contentType, charset, headers, new ByteArrayInputStream(response));
this.bodyBytes = response;
}
@Override
public String getCharset()
{
return charset;
}
@Override
public String getContentType()
{
int split = contentType.indexOf(';');
if (split == -1)
{
return contentType;
}
else
{
return contentType.substring(0, split);
}
}
@Override
public String getRawContentType()
{
return contentType;
}
@Override
public RemoteConnectorRequest getRequest()
{
return request;
}
@Override
public Header[] getResponseHeaders()
{
return headers;
}
@Override
public byte[] getResponseBodyAsBytes() throws IOException
{
if (bodyBytes == null)
{
bodyBytes = IOUtils.toByteArray(bodyStream);
bodyStream.close();
// Build a new stream version in case they also want that
bodyStream = new ByteArrayInputStream(bodyBytes);
}
return bodyBytes;
}
@Override
public InputStream getResponseBodyAsStream()
{
return bodyStream;
}
@Override
public String getResponseBodyAsString() throws IOException
{
String charset = this.charset;
if (charset == null)
{
charset = "UTF-8";
}
return new String(getResponseBodyAsBytes(), charset);
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright (C) 2005-2012 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.repo.remoteconnector;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorRequest;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorResponse;
import org.alfresco.service.cmr.remoteconnector.RemoteConnectorService;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.json.simple.parser.JSONParser;
import org.springframework.extensions.webscripts.Status;
/**
* HttpClient powered implementation of {@link RemoteConnectorService}, which
* performs requests to remote HTTP servers
*
* @author Nick Burch
* @since 4.0.2
*/
public class RemoteConnectorServiceImpl implements RemoteConnectorService
{
/**
* The logger
*/
private static Log logger = LogFactory.getLog(RemoteConnectorServiceImpl.class);
private static final long MAX_BUFFER_RESPONSE_SIZE = 10*1024*1024;
private HttpClient httpClient;
public RemoteConnectorServiceImpl()
{
httpClient = new HttpClient();
httpClient.setHttpConnectionManager(new MultiThreadedHttpConnectionManager());
}
/**
* Builds a new Request object
*/
public RemoteConnectorRequest buildRequest(String url, String method)
{
return new RemoteConnectorRequestImpl(url, method);
}
/**
* Builds a new Request object, using HttpClient method descriptions
*/
public RemoteConnectorRequest buildRequest(String url, Class<? extends HttpMethodBase> method)
{
return new RemoteConnectorRequestImpl(url, method);
}
/**
* Executes the specified request, and return the response
*/
public RemoteConnectorResponse executeRequest(RemoteConnectorRequest request) throws IOException, AuthenticationException
{
RemoteConnectorRequestImpl reqImpl = (RemoteConnectorRequestImpl)request;
HttpMethodBase httpRequest = reqImpl.getMethodInstance();
// Attach the headers to the request
for (Header hdr : request.getRequestHeaders())
{
httpRequest.addRequestHeader(hdr);
}
// Attach the body, if possible
if (httpRequest instanceof EntityEnclosingMethod)
{
if (request.getRequestBody() != null)
{
((EntityEnclosingMethod)httpRequest).setRequestEntity( reqImpl.getRequestBody() );
}
}
// Log what we're doing
if (logger.isDebugEnabled())
logger.debug("Performing " + request.getMethod() + " request to " + request.getURL());
// Perform the request
int status = httpClient.executeMethod(httpRequest);
String statusText = httpRequest.getStatusText();
Header[] responseHdrs = httpRequest.getResponseHeaders();
Header responseContentTypeH = httpRequest.getResponseHeader(RemoteConnectorRequestImpl.HEADER_CONTENT_TYPE);
String responseCharSet = httpRequest.getResponseCharSet();
String responseContentType = (responseContentTypeH != null ? responseContentTypeH.getValue() : null);
// Decide on how best to handle the response, based on the size
// Ideally, we want to close the HttpClient resources immediately, but
// that isn't possible for very large responses
RemoteConnectorResponse response = null;
if (httpRequest.getResponseContentLength() > MAX_BUFFER_RESPONSE_SIZE)
{
// Need to wrap the InputStream in something that'll close
InputStream wrappedStream = new HttpClientReleasingInputStream(httpRequest);
// Now build the response
response = new RemoteConnectorResponseImpl(request, responseContentType, responseCharSet,
responseHdrs, wrappedStream);
}
else
{
// Fairly small response, just keep the bytes and make life simple
response = new RemoteConnectorResponseImpl(request, responseContentType, responseCharSet,
responseHdrs, httpRequest.getResponseBody());
// Now we have the bytes, we can close the HttpClient resources
httpRequest.releaseConnection();
httpRequest = null;
}
// Log the response
if (logger.isDebugEnabled())
logger.debug("Response was " + status + " " + statusText);
// Decide if we should throw an exception
if (status == Status.STATUS_FORBIDDEN)
{
// Tidy if needed
if (httpRequest != null)
httpRequest.releaseConnection();
// Then report the error
throw new AuthenticationException(statusText);
}
if (status == Status.STATUS_INTERNAL_SERVER_ERROR)
{
// Tidy if needed
if (httpRequest != null)
httpRequest.releaseConnection();
// Then report the error
throw new IOException(statusText);
}
// TODO Handle the rest of the different status codes
// Return our created response
return response;
}
/**
* Executes the given request, requesting a JSON response, and
* returns the parsed JSON received back
*
* @throws ParseException If the response is not valid JSON
*/
public JSONObject executeJSONRequest(RemoteConnectorRequest request) throws ParseException, IOException, AuthenticationException
{
return doExecuteJSONRequest(request, this);
}
public static JSONObject doExecuteJSONRequest(RemoteConnectorRequest request, RemoteConnectorService service) throws ParseException, IOException, AuthenticationException
{
// Set as JSON
request.setContentType(MimetypeMap.MIMETYPE_JSON);
// Perform the request
RemoteConnectorResponse response = service.executeRequest(request);
// Parse this as JSON
JSONParser parser = new JSONParser();
String jsonText = response.getResponseBodyAsString();
Object json = parser.parse(jsonText);
// Check it's the right type and return
if (json instanceof JSONObject)
{
return (JSONObject)json;
}
else
{
throw new ParseException(0, json);
}
}
private static class HttpClientReleasingInputStream extends FilterInputStream
{
private HttpMethodBase httpRequest;
private HttpClientReleasingInputStream(HttpMethodBase httpRequest) throws IOException
{
super(httpRequest.getResponseBodyAsStream());
this.httpRequest = httpRequest;
}
@Override
public void close() throws IOException
{
// Tidy the main stream
super.close();
// Now release the underlying resources
if (httpRequest != null)
{
httpRequest.releaseConnection();
httpRequest = null;
}
}
}
}