/*
* Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
*
* 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 freemarker.test.servlet;
import static org.junit.Assert.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class WebAppTestCase {
public static final String IGNORED_MASK = "[IGNORED]";
private static final Logger LOG = LoggerFactory.getLogger(WebAppTestCase.class);
private static final String ATTR_JETTY_CONTAINER_INCLUDE_JAR_PATTERN
= "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
private static final String EXPECTED_DIR = "/WEB-INF/expected/";
private static Server server;
private static ContextHandlerCollection contextHandlers;
private static Map<String, WebAppContext> deployedWebApps = new HashMap<String, WebAppContext>();
@BeforeClass
public static void beforeClass() throws Exception {
// Work around Java 5 bug(?) that causes Jasper to fail with "zip file closed" when it reads the JSTL jar:
org.eclipse.jetty.util.resource.Resource.setDefaultUseCaches(false);
LOG.info("Starting embedded Jetty...");
server = new Server(0);
contextHandlers = new ContextHandlerCollection();
server.setHandler(contextHandlers);
server.start();
}
@AfterClass
public static void afterClass() throws Exception {
LOG.info("Stopping embedded Jetty...");
server.stop();
server.join(); // TODO redundant?
}
protected final String getResponseContent(String webAppName, String webAppRelURL) throws Exception {
HTTPResponse resp = getHTTPResponse(webAppName, webAppRelURL);
if (resp.getStatusCode() != HttpURLConnection.HTTP_OK) {
fail("Expected HTTP status " + HttpURLConnection.HTTP_OK + ", but got "
+ resp.getStatusCode() + " (message: " + resp.getStatusMessage() + ") for URI "
+ resp.getURI());
}
return resp.getContent();
}
protected final int getResponseStatusCode(String webAppName, String webAppRelURL) throws Exception {
HTTPResponse resp = getHTTPResponse(webAppName, webAppRelURL);
return resp.getStatusCode();
}
protected final HTTPResponse getHTTPResponse(String webAppName, String webAppRelURL) throws Exception {
if (webAppName.startsWith("/") || webAppName.endsWith("/")) {
throw new IllegalArgumentException("\"webAppName\" can't start or end with \"/\": " + webAppName);
}
if (webAppRelURL.startsWith("/") || webAppRelURL.endsWith("/")) {
throw new IllegalArgumentException("\"webappRelURL\" can't start or end with \"/\": " + webAppRelURL);
}
ensureWebAppIsDeployed(webAppName);
final URI uri = new URI("http://localhost:" + server.getConnectors()[0].getLocalPort()
+ "/" + webAppName + "/" + webAppRelURL);
final HttpURLConnection httpCon = (HttpURLConnection) uri.toURL().openConnection();
httpCon.connect();
try {
LOG.debug("HTTP GET: {}", uri);
final int responseCode = httpCon.getResponseCode();
final String content;
if (responseCode == 200) {
InputStream in = httpCon.getInputStream();
try {
content = IOUtils.toString(in, "UTF-8");
} finally {
in.close();
}
} else {
content = null;
}
return new HTTPResponse(
responseCode, httpCon.getResponseMessage(),
content,
uri);
} finally {
httpCon.disconnect();
}
}
/**
* Compares the output of the JSP and the FTL version of the same page, ignoring some of the whitespace differences.
* @param webAppRelURLWithoutExt something like {@code "tester?view=foo"}, which will be extended to
* {@code "tester?view=foo.jsp"} and {@code "tester?view=foo.ftl"}, and then the output of these extended
* URL-s will be compared.
*/
protected void assertJSPAndFTLOutputEquals(String webAppName, String webAppRelURLWithoutExt) throws Exception {
assertOutputsEqual(webAppName, webAppRelURLWithoutExt + ".jsp", webAppRelURLWithoutExt + ".ftl");
}
protected void assertOutputsEqual(String webAppName, String webAppRelURL1, final String webAppRelURL2)
throws Exception {
String jspOutput = normalizeWS(getResponseContent(webAppName, webAppRelURL1), true);
String ftlOutput = normalizeWS(getResponseContent(webAppName, webAppRelURL2), true);
assertEquals(jspOutput, ftlOutput);
}
protected void assertExpectedEqualsOutput(String webAppName, String expectedFileName, String webAppRelURL)
throws Exception {
assertExpectedEqualsOutput(webAppName, expectedFileName, webAppRelURL, true);
}
protected void assertExpectedEqualsOutput(String webAppName, String expectedFileName, String webAppRelURL,
boolean compressWS) throws Exception {
assertExpectedEqualsOutput(webAppName, expectedFileName, webAppRelURL, compressWS, null);
}
/**
* @param expectedFileName
* The name of the file that stores the expected content, relatively to
* {@code servketContext:/WEB-INF/expected}.
* @param ignoredParts
* Parts that will be search-and-replaced with {@value #IGNORED_MASK} with both in the expected and
* actual outputs.
*/
protected void assertExpectedEqualsOutput(String webAppName, String expectedFileName, String webAppRelURL,
boolean compressWS, List<Pattern> ignoredParts) throws Exception {
final String actual = normalizeWS(getResponseContent(webAppName, webAppRelURL), compressWS);
final String expected;
{
final InputStream in = new URL(getWebAppDirURL(webAppName) + EXPECTED_DIR + expectedFileName).openStream();
try {
expected = normalizeWS(IOUtils.toString(in, "utf-8"), compressWS);
} finally {
in.close();
}
}
assertEquals(maskIgnored(expected, ignoredParts), maskIgnored(actual, ignoredParts));
}
private String maskIgnored(String s, List<Pattern> ignoredParts) {
if (ignoredParts == null) return s;
for (Pattern ignoredPart : ignoredParts) {
s = ignoredPart.matcher(s).replaceAll(IGNORED_MASK);
}
return s;
}
protected synchronized void restartWebAppIfStarted(String webAppName) throws Exception {
WebAppContext context = deployedWebApps.get(webAppName);
if (context != null) {
context.stop();
context.start();
}
}
private Pattern BR = Pattern.compile("\r\n|\r");
private Pattern MULTI_LINE_WS = Pattern.compile("[\t ]*[\r\n][\t \r\n]*", Pattern.DOTALL);
private Pattern SAME_LINE_WS = Pattern.compile("[\t ]+", Pattern.DOTALL);
private String normalizeWS(String s, boolean compressWS) {
if (compressWS) {
return SAME_LINE_WS.matcher(
MULTI_LINE_WS.matcher(s).replaceAll("\n"))
.replaceAll(" ")
.trim();
} else {
return BR.matcher(s).replaceAll("\n");
}
}
private synchronized void ensureWebAppIsDeployed(String webAppName) throws Exception {
if (deployedWebApps.containsKey(webAppName)) {
return;
}
final String webAppDirURL = getWebAppDirURL(webAppName);
WebAppContext context = new WebAppContext(webAppDirURL, "/" + webAppName);
// Pattern of jar file names scanned for META-INF/*.tld:
context.setAttribute(
ATTR_JETTY_CONTAINER_INCLUDE_JAR_PATTERN,
".*taglib.*\\.jar$");
contextHandlers.addHandler(context);
// As we add this after the Server was started, it has to be started manually:
context.start();
deployedWebApps.put(webAppName, context);
LOG.info("Deployed web app.: {}", webAppName);
}
@SuppressFBWarnings(value="UI_INHERITANCE_UNSAFE_GETRESOURCE", justification="By design relative to subclass")
private String getWebAppDirURL(String webAppName) throws IOException {
final URL webXmlURL;
{
final String relResPath = "webapps/" + webAppName + "/WEB-INF/web.xml";
Class<?> baseClass = this.getClass();
findWebXmlURL: do {
URL r = baseClass.getResource(relResPath);
if (r != null) {
webXmlURL = r;
break findWebXmlURL;
}
baseClass = baseClass.getSuperclass();
if (!WebAppTestCase.class.isAssignableFrom(baseClass)) {
throw new IOException("Can't find test class relative resource: " + relResPath);
}
} while (true);
}
try {
return webXmlURL.toURI().resolve("..").toString();
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to get grandparent URL for " + webXmlURL, e);
}
}
private static class HTTPResponse {
private final int statusCode;
private final String content;
private final String statusMessage;
private final URI uri;
public HTTPResponse(int statusCode, String statusMessage, String content, URI uri) {
this.statusCode = statusCode;
this.content = content;
this.statusMessage = statusMessage;
this.uri = uri;
}
public String getStatusMessage() {
return statusMessage;
}
public int getStatusCode() {
return statusCode;
}
public String getContent() {
return content;
}
public URI getURI() {
return uri;
}
}
}