blob: d1ca7e5f667303e91c573f77468a0643a4778b70 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sling.servlethelpers.internalrequests;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.OptionalInt;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.servlethelpers.MockRequestPathInfo;
import org.apache.sling.servlethelpers.MockSlingHttpServletRequest;
import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/** Fluent helper for Sling internal requests.
*
* The {@link ServletInternalRequest} and {@link SlingInternalRequest}
* subclasses provide two modes for executing the
* internal requests, one that's very similar to the way Sling
* executes an HTTP request and another one that's faster by
* calling Servlets or Scripts directly.
*/
public abstract class InternalRequest {
protected final ResourceResolver resourceResolver;
protected final String path;
protected String selectorString;
protected String extension;
protected String requestMethod = DEFAULT_METHOD;
protected String contentType;
private Reader bodyReader;
private boolean explicitStatusCheck;
private Map<String, Object> parameters = new HashMap<>();
private MockSlingHttpServletRequest request;
private MockSlingHttpServletResponse response;
protected final Logger log = LoggerFactory.getLogger(getClass());
public static final String DEFAULT_METHOD = "GET";
/** An slf4j MDC value is set at this key with request information.
* That's useful for troubleshooting when using multiple internal
* requests in the context of a single HTTP request.
*/
public static final String MDC_KEY = "sling." + InternalRequest.class.getSimpleName();
/** Clients use subclasses of this one */
protected InternalRequest(@NotNull ResourceResolver resourceResolver, @NotNull String path) {
checkNotNull(ResourceResolver.class, resourceResolver);
checkNotNull("path", path);
this.resourceResolver = resourceResolver;
this.path = path;
}
protected void checkNotNull(String info, Object candidate) {
if(candidate == null) {
throw new IllegalArgumentException(info + " is null");
}
}
protected void checkNotNull(Class<?> clazz, Object candidate) {
checkNotNull(clazz.getSimpleName(), candidate);
}
/** Set the HTTP request method to use - defaults to GET */
public InternalRequest withRequestMethod(String method) {
this.requestMethod = method.toUpperCase();
return this;
}
/** Set the HTTP request's Content-Type */
public InternalRequest withContentType(String contentType) {
this.contentType = contentType;
return this;
}
/** Use the supplied Reader as the request's body content */
public InternalRequest withBody(Reader bodyContent) {
bodyReader = bodyContent;
return this;
}
/** Sets the optional selectors of the internal request, which influence
* the Servlet/Script resolution.
*/
public InternalRequest withSelectors(String ... selectors) {
if(selectors == null) {
return this;
}
StringBuilder sb = new StringBuilder();
Arrays.stream(selectors).forEach(sel -> sb.append(sb.length() == 0 ? "" : ".").append(sel));
selectorString = sb.toString();
return this;
}
/** Sets the optional extension of the internal request, which influence
* the Servlet/Script resolution.
*/
public InternalRequest withExtension(String extension) {
this.extension = extension;
return this;
}
/** Set a request parameter */
public InternalRequest withParameter(String key, Object value) {
if(key != null && value != null) {
parameters.put(key, value);
} else {
throw new IllegalArgumentException("Null key or value");
}
return this;
}
/** Add the supplied request parameters to the current ones */
public InternalRequest withParameters(Map<String, Object> additionalParameters) {
if(additionalParameters != null) {
parameters.putAll(additionalParameters);
};
return this;
}
/** Execute the internal request. Can be called right after
* creating it, if no options need to be set.
*
* @throws IOException if the request was already executed,
* or if an error occurs during execution.
*/
public final InternalRequest execute() throws IOException {
if(request != null) {
throw new IOException("Request was already executed");
}
final Resource resource = getExecutionResource();
request = new MockSlingHttpServletRequest(resourceResolver) {
@Override
protected MockRequestPathInfo newMockRequestPathInfo() {
MockRequestPathInfo rpi = super.newMockRequestPathInfo();
rpi.setResourcePath(path);
rpi.setExtension(extension);
rpi.setSelectorString(selectorString);
return rpi;
}
@Override
public BufferedReader getReader() {
if(bodyReader != null) {
return new BufferedReader(bodyReader);
} else {
return super.getReader();
}
}
};
request.setMethod(requestMethod);
request.setContentType(contentType);
request.setResource(resource);
request.setParameterMap(parameters);
response = new MockSlingHttpServletResponse();
MDC.put(MDC_KEY, toString());
try {
delegateExecute(request, response, resourceResolver);
} catch(ServletException sx) {
throw new IOException("ServletException in execute()", sx);
}
return this;
}
/** Provide the Resource to use to execute the request */
protected abstract Resource getExecutionResource();
/** Execute the supplied Request */
protected abstract void delegateExecute(SlingHttpServletRequest request, SlingHttpServletResponse response, ResourceResolver resourceResolver)
throws ServletException, IOException;
protected void assertRequestExecuted() throws IOException {
if(request == null) {
throw new IOException("Request hasn't been executed");
}
}
/** After executing the request, checks that the request status is one
* of the supplied values.
*
* If this is not called before methods that access the response, a check
* for a 200 OK status is done automatically unless this was called
* with no arguments before.
*
* This makes sure a status check is done or explicitly disabled.
*
* @param acceptableValues providing no values means "don't care"
* @throws IOException if status doesn't match any of these values
*/
public InternalRequest checkStatus(int ... acceptableValues) throws IOException {
assertRequestExecuted();
explicitStatusCheck = true;
if(acceptableValues == null || acceptableValues.length == 0) {
return this;
}
final int actualStatus = getStatus();
final OptionalInt found = Arrays.stream(acceptableValues).filter(expected -> expected == actualStatus).findFirst();
if(!found.isPresent()) {
final StringBuilder sb = new StringBuilder();
Arrays.stream(acceptableValues).forEach(val -> sb.append(sb.length() == 0 ? "" : ",").append(val));
throw new IOException("Unexpected response status " + actualStatus + ", expected one of '" + sb + "'");
}
return this;
}
/** If response status hasn't been explicitly checked, ensure it's 200 */
private void maybeCheckOkStatus() throws IOException {
if(!explicitStatusCheck) {
try {
checkStatus(HttpServletResponse.SC_OK);
} finally {
explicitStatusCheck = false;
}
}
}
/** After executing the request, checks that the response content-type
* is as expected.
*
* @throws IOException if the actual content-type doesn't match the expected one
*/
public InternalRequest checkResponseContentType(String contentType) throws IOException {
assertRequestExecuted();
if(!contentType.equals(response.getContentType())) {
throw new IOException("Expected content type " + contentType + " but got " + response.getContentType());
}
return this;
}
/** Return the response status. The execute method must be called before this one.
* @throws IOException if the request hasn't been executed yet
*/
public int getStatus() throws IOException {
assertRequestExecuted();
return response.getStatus();
}
/** Return the response object. The execute method must be called before this one.
* A check for "200 OK" status is done automatically unless {@link #checkStatus} has
* been called before.
*
* @throws IOException if the request hasn't been executed yet or if the status
* check fails.
*/
public SlingHttpServletResponse getResponse() throws IOException {
assertRequestExecuted();
maybeCheckOkStatus();
return response;
}
/** Return the response as a String. The execute method must be called before this one.
*
* A check for "200 OK" status is done automatically unless {@link #checkStatus} has
* been called before.
*
* @throws IOException if the request hasn't been executed yet or if the status
* check fails.
*/
public String getResponseAsString() throws IOException {
assertRequestExecuted();
maybeCheckOkStatus();
return response.getOutputAsString();
}
}