SEARCH-2909 Return Http Status Code 501 (Not Implemented) in REST API… (#519)

* SEARCH-2909 Return Http Status Code 501 (Not Implemented) in REST API for invocations to Search Services that are not supported.

* SEARCH-2909 Refactor exception mapping to simplify QueryParserException.

* SEARCH-2909 Add unit tests for postQuery.

This includes a test for the status code replacement with unsupported operations.

* SEARCH-2909 Add new unit test file to test suite.

Also add copyright header to test class.

Co-authored-by: Tom Page <thomas.page@alfresco.com>
This commit is contained in:
Angel Borroy
2021-06-11 17:03:55 +02:00
committed by GitHub
parent 29d5093c93
commit f1a5425f62
10 changed files with 371 additions and 87 deletions

View File

@@ -41,7 +41,11 @@ public class DefaultExceptionResolver implements ExceptionResolver<Exception>
@Override
public ErrorResponse resolveException(Exception ex)
{
return new ErrorResponse(DEFAULT_MESSAGE_ID, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), ex.getStackTrace(), null);
return new ErrorResponse(DEFAULT_MESSAGE_ID,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
ex.getLocalizedMessage(),
ex.getStackTrace(),
null);
}
}

View File

@@ -0,0 +1,48 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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/>.
* #L%
*/
package org.alfresco.rest.framework.core.exceptions;
import org.alfresco.repo.search.QueryParserException;
/**
* QueryParserException is related with search requests to Search Services.
*/
public class QueryParserExceptionResolver implements ExceptionResolver<QueryParserException>
{
@Override
public ErrorResponse resolveException(QueryParserException ex)
{
return new ErrorResponse(
DefaultExceptionResolver.DEFAULT_MESSAGE_ID,
// Mapping the original HTTP Status code returned by Search Services
ex.getHttpStatusCode(),
ex.getLocalizedMessage(),
ex.getStackTrace(),
null);
}
}

View File

@@ -26,10 +26,12 @@
package org.alfresco.rest.framework.tools;
import org.alfresco.metrics.rest.RestMetricsReporter;
import org.alfresco.repo.search.QueryParserException;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.exceptions.DefaultExceptionResolver;
import org.alfresco.rest.framework.core.exceptions.ErrorResponse;
import org.alfresco.rest.framework.core.exceptions.ExceptionResolver;
import org.alfresco.rest.framework.core.exceptions.QueryParserExceptionResolver;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -48,6 +50,7 @@ public class ApiAssistant {
private ExceptionResolver<Exception> defaultResolver = new DefaultExceptionResolver();
private ExceptionResolver<WebScriptException> webScriptExceptionResolver;
private ExceptionResolver<QueryParserException> queryParserExceptionResolver;
private ExceptionResolver<Exception> resolver;
private JacksonHelper jsonHelper;
private RestMetricsReporter restMetricsReporter;
@@ -77,6 +80,10 @@ public class ApiAssistant {
{
error = webScriptExceptionResolver.resolveException((WebScriptException) ex);
}
else if (ex instanceof QueryParserException)
{
error = queryParserExceptionResolver.resolveException((QueryParserException) ex);
}
else
{
error = resolver.resolveException(ex);
@@ -100,6 +107,11 @@ public class ApiAssistant {
this.webScriptExceptionResolver = webScriptExceptionResolver;
}
public void setQueryParserExceptionResolver(ExceptionResolver<QueryParserException> queryParserExceptionResolver)
{
this.queryParserExceptionResolver = queryParserExceptionResolver;
}
public void setResolver(ExceptionResolver<Exception> resolver) {
this.resolver = resolver;
}

View File

@@ -134,6 +134,8 @@
</bean>
<bean id="webScriptExceptionResolver" class="org.alfresco.rest.framework.core.exceptions.WebScriptExceptionResolver">
</bean>
<bean id="queryParserExceptionResolver" class="org.alfresco.rest.framework.core.exceptions.QueryParserExceptionResolver">
</bean>
<bean id="simpleMappingExceptionResolverParent" abstract="true" class="org.alfresco.rest.framework.core.exceptions.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<map>
@@ -182,6 +184,7 @@
<property name="resolver" ref="simpleMappingExceptionResolver" />
<property name="webScriptExceptionResolver" ref="webScriptExceptionResolver" />
<property name="restMetricsReporter" ref="restMetricsReporter"/>
<property name="queryParserExceptionResolver" ref="queryParserExceptionResolver" />
</bean>
<!-- Using annotation-config=false means AutowiredAnnotationBeanPostProcessor

View File

@@ -31,6 +31,7 @@ import static org.junit.Assert.assertNull;
import org.alfresco.repo.forms.FormNotFoundException;
import org.alfresco.repo.node.integrity.IntegrityException;
import org.alfresco.repo.search.QueryParserException;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
import org.alfresco.rest.framework.core.exceptions.DeletedResourceException;
@@ -122,4 +123,13 @@ public class ExceptionResolverTests
assertEquals(422, response.getStatusCode());
}
/** Check that the status code from SS is passed back to the caller. */
@Test
public void testQueryParserException()
{
ErrorResponse response = assistant.resolveException(new QueryParserException("Endpoint not found", 404));
assertNotNull(response);
assertEquals("Expected status code to be passed through from query parser.", 404, response.getStatusCode());
}
}

View File

@@ -33,10 +33,12 @@
</bean>
<bean id="webScriptExceptionResolver" class="org.alfresco.rest.framework.core.exceptions.WebScriptExceptionResolver">
</bean>
<bean id="queryParserExceptionResolver" class="org.alfresco.rest.framework.core.exceptions.QueryParserExceptionResolver" />
<bean id="apiAssistant" class="org.alfresco.rest.framework.tools.ApiAssistant">
<property name="jsonHelper" ref="jsonHelper" />
<property name="resolver" ref="simpleMappingExceptionResolver" />
<property name="webScriptExceptionResolver" ref="webScriptExceptionResolver" />
<property name="queryParserExceptionResolver" ref="queryParserExceptionResolver" />
</bean>
<bean id="simpleMappingExceptionResolver" class="org.alfresco.rest.framework.core.exceptions.SimpleMappingExceptionResolver">
<property name="exceptionMappings">

View File

@@ -26,6 +26,9 @@
package org.alfresco.repo.search;
import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.http.HttpStatus;
import java.util.List;
/**
* @author Andy
@@ -33,11 +36,10 @@ import org.alfresco.error.AlfrescoRuntimeException;
*/
public class QueryParserException extends AlfrescoRuntimeException
{
/**
*
*/
/** Serial version UUID. */
private static final long serialVersionUID = 4886993838297301968L;
/** Http Status Code that should be returned by Remote API. */
private int httpStatusCode;
/**
* @param msgId
@@ -45,7 +47,6 @@ public class QueryParserException extends AlfrescoRuntimeException
public QueryParserException(String msgId)
{
super(msgId);
// TODO Auto-generated constructor stub
}
/**
@@ -55,7 +56,6 @@ public class QueryParserException extends AlfrescoRuntimeException
public QueryParserException(String msgId, Object[] msgParams)
{
super(msgId, msgParams);
// TODO Auto-generated constructor stub
}
/**
@@ -65,7 +65,6 @@ public class QueryParserException extends AlfrescoRuntimeException
public QueryParserException(String msgId, Throwable cause)
{
super(msgId, cause);
// TODO Auto-generated constructor stub
}
/**
@@ -76,7 +75,22 @@ public class QueryParserException extends AlfrescoRuntimeException
public QueryParserException(String msgId, Object[] msgParams, Throwable cause)
{
super(msgId, msgParams, cause);
// TODO Auto-generated constructor stub
}
/**
* Constructor for exception that allows setting an HTTP status code.
*
* @param msgId Message for the exception
* @param httpStatusCode Status code to return for exception
*/
public QueryParserException(String msgId, int httpStatusCode)
{
super(msgId);
this.httpStatusCode = httpStatusCode;
}
public int getHttpStatusCode()
{
return httpStatusCode;
}
}

View File

@@ -29,26 +29,29 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.alfresco.repo.search.QueryParserException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
public abstract class AbstractSolrQueryHTTPClient
{
/** Logger for the class. */
private static final Log LOGGER = LogFactory.getLog(AbstractSolrQueryHTTPClient.class);
public static final int DEFAULT_SAVEPOST_BUFFER = 4096;
// Constants copied from org.apache.solr.common.params.HighlightParams (solr-solrj:1.4.1)
@@ -80,10 +83,12 @@ public abstract class AbstractSolrQueryHTTPClient
public static final String HIGHLIGHT_PARAMS_PATTERN = HIGHLIGHT_PARAMS_HIGHLIGHT + "." + HIGHLIGHT_PARAMS_REGEX + ".pattern";
public static final String HIGHLIGHT_PARAMS_MAX_RE_CHARS = HIGHLIGHT_PARAMS_HIGHLIGHT + "." + HIGHLIGHT_PARAMS_REGEX + ".maxAnalyzedChars";
protected JSONObject postQuery(HttpClient httpClient, String url, JSONObject body) throws UnsupportedEncodingException,
IOException, HttpException, URIException, JSONException
/** List of SOLR Exceptions that should be returning HTTP 501 status code in Remote API. */
private static final List<String> STATUS_CODE_501_EXCEPTIONS = List.of("java.lang.UnsupportedOperationException");
protected JSONObject postQuery(HttpClient httpClient, String url, JSONObject body) throws IOException, JSONException
{
PostMethod post = new PostMethod(url);
PostMethod post = createNewPostMethod(url);
if (body.toString().length() > DEFAULT_SAVEPOST_BUFFER)
{
post.getParams().setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, true);
@@ -103,9 +108,33 @@ public abstract class AbstractSolrQueryHTTPClient
httpClient.executeMethod(post);
}
}
String responseBodyStr = post.getResponseBodyAsString();
if (post.getStatusCode() != HttpServletResponse.SC_OK)
{
throw new QueryParserException("Request failed " + post.getStatusCode() + " " + url.toString());
String trace = null;
try
{
trace = new JSONObject(responseBodyStr).getJSONObject("error").getString("trace");
}
catch (JSONException jsonException)
{
LOGGER.warn("Node 'error.trace' is not present in Search Services error response: " + responseBodyStr);
LOGGER.warn("A generic error message will be provided. Check SOLR log file in order to find the root cause for this issue");
}
int httpStatusCode = post.getStatusCode();
String message = "Solr request failed with " + httpStatusCode + " " + url;
// Override the status code for certain exceptions with 501.
if (trace != null)
{
String traceException = trace.substring(0, trace.indexOf(":")).trim();
if (STATUS_CODE_501_EXCEPTIONS.contains(traceException))
{
httpStatusCode = org.apache.http.HttpStatus.SC_NOT_IMPLEMENTED;
}
}
throw new QueryParserException(message, httpStatusCode);
}
Reader reader = new BufferedReader(new InputStreamReader(post.getResponseBodyAsStream(), post.getResponseCharSet()));
@@ -118,4 +147,10 @@ public abstract class AbstractSolrQueryHTTPClient
post.releaseConnection();
}
}
/** Helper method that can be overridden by unit tests. */
protected PostMethod createNewPostMethod(String url)
{
return new PostMethod(url);
}
}

View File

@@ -120,6 +120,9 @@ import org.junit.runners.Suite;
org.alfresco.util.schemacomp.validator.TypeNameOnlyValidatorTest.class,
org.alfresco.util.test.OmittedTestClassFinderUnitTest.class,
org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class,
org.alfresco.repo.search.impl.solr.AbstractSolrQueryHTTPClientTest.class,
org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngineTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorLimitsTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorPermissionsTest.class,
@@ -131,8 +134,6 @@ import org.junit.runners.Suite;
org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class,
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,

View File

@@ -0,0 +1,155 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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/>.
* #L%
*/
package org.alfresco.repo.search.impl.solr;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import org.alfresco.repo.search.QueryParserException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.methods.PostMethod;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
/** Tests for the {@link AbstractSolrQueryHTTPClient}. */
public class AbstractSolrQueryHTTPClientTest
{
/** A URL for use in the tests. */
private static final String URL = "http://this/is/a/url";
/** The abstract class under test. */
private AbstractSolrQueryHTTPClient abstractSolrQueryHTTPClient = spy(AbstractSolrQueryHTTPClient.class);
@Mock
private HttpClient httpClient;
@Mock
private JSONObject body;
@Mock
private PostMethod postMethod;
@Mock
private Header header;
@Before
public void setUp() throws Exception
{
openMocks(this);
doReturn(postMethod).when(abstractSolrQueryHTTPClient).createNewPostMethod(URL);
when(postMethod.getResponseCharSet()).thenReturn("UTF-8");
}
/** Check postQuery works as expected for the success case. */
@Test
public void testPostQuery_success() throws Exception
{
when(body.toString()).thenReturn("Example body");
when(postMethod.getStatusCode()).thenReturn(HttpServletResponse.SC_OK);
when(postMethod.getResponseBodyAsStream()).thenReturn(convertStringToInputStream("{}"));
JSONObject response = abstractSolrQueryHTTPClient.postQuery(httpClient, URL, body);
assertEquals("Unexpected JSON response received.", "{}", response.toString());
}
/** Check that the status code is usually passed through from Solr. */
@Test
public void testPostQuery_failure() throws Exception
{
String failureMessage = "{\"error\": {\"trace\": \"ExceptionClass: Stacktrace\"}}";
when(body.toString()).thenReturn("Example body");
when(postMethod.getStatusCode()).thenReturn(HttpServletResponse.SC_NOT_FOUND);
when(postMethod.getResponseBodyAsStream()).thenReturn(convertStringToInputStream(failureMessage));
when(postMethod.getResponseBodyAsString()).thenReturn(failureMessage);
try
{
abstractSolrQueryHTTPClient.postQuery(httpClient, URL, body);
fail("Expected a QueryParserException to be thrown.");
}
catch (QueryParserException e)
{
assertEquals("Unexpected status code in exception.", e.getHttpStatusCode(), HttpServletResponse.SC_NOT_FOUND);
verify(postMethod).releaseConnection();
}
}
/** Check that the status code is replaced with "Not Implemented" for an unsupported query option. */
@Test
public void testPostQuery_unsupportedOperation() throws Exception
{
String failureMessage = "{\"error\": {\"trace\": \"java.lang.UnsupportedOperationException: Stacktrace\"}}";
when(body.toString()).thenReturn("Example body");
when(postMethod.getStatusCode()).thenReturn(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
when(postMethod.getResponseBodyAsStream()).thenReturn(convertStringToInputStream(failureMessage));
when(postMethod.getResponseBodyAsString()).thenReturn(failureMessage);
try
{
abstractSolrQueryHTTPClient.postQuery(httpClient, URL, body);
fail("Expected a QueryParserException to be thrown.");
}
catch (QueryParserException e)
{
assertEquals("Unexpected status code in exception.", e.getHttpStatusCode(), HttpServletResponse.SC_NOT_IMPLEMENTED);
}
}
/** Check that a redirect can be followed if the endpoint reports that it's moved. */
@Test
public void testPostQuery_moved() throws Exception
{
when(body.toString()).thenReturn("Example body");
// Report "moved" for the first invocation and then OK for subsequent requests.
when(postMethod.getStatusCode()).thenReturn(HttpServletResponse.SC_MOVED_PERMANENTLY).thenReturn(HttpServletResponse.SC_OK);
when(postMethod.getResponseBodyAsStream()).thenReturn(convertStringToInputStream("{}"));
when(postMethod.getResponseHeader("location")).thenReturn(header);
when(header.getValue()).thenReturn("http://new/URL");
JSONObject response = abstractSolrQueryHTTPClient.postQuery(httpClient, URL, body);
verify(postMethod).setURI(new URI("http://new/URL", true));
assertEquals("Unexpected JSON response received.", "{}", response.toString());
}
/** Create an input stream containing the given string. */
private ByteArrayInputStream convertStringToInputStream(String message)
{
return new ByteArrayInputStream(message.getBytes());
}
}