/*
* Copyright (C) 2008 Laurent Caillette
*
* This program 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.daemon;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectHandler;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import static com.google.common.base.Charsets.UTF_8;
import static org.junit.Assert.assertEquals;
import org.novelang.ResourceTools;
import org.novelang.ResourcesForTests;
import org.novelang.common.filefixture.Directory;
import org.novelang.common.filefixture.Resource;
import org.novelang.common.filefixture.ResourceInstaller;
import org.novelang.configuration.ConfigurationTools;
import org.novelang.configuration.parse.DaemonParameters;
import org.novelang.configuration.parse.GenericParametersConstants;
import org.novelang.logger.Logger;
import org.novelang.logger.LoggerFactory;
import org.novelang.outfit.DefaultCharset;
import org.novelang.outfit.TcpPortBooker;
import org.novelang.outfit.loader.CompositeResourceLoader;
import org.novelang.testing.junit.MethodSupport;
/**
* A JUnit {@link org.junit.Rule} supporting concurrent test execution.
*
* @author Laurent Caillette
*/
public class HttpDaemonSupport extends MethodSupport {
private static final Logger LOGGER = LoggerFactory.getLogger( HttpDaemonSupport.class ) ;
static {
ResourcesForTests.initialize() ;
}
private final int daemonPort = TcpPortBooker.THIS.find() ;
protected final ResourceInstaller resourceInstaller;
private HttpDaemon httpDaemon = null ;
public HttpDaemonSupport() {
resourceInstaller = new ResourceInstaller( this ) ;
}
public HttpDaemonSupport( final Object executionLock ) {
super( executionLock ) ;
resourceInstaller = new ResourceInstaller( this ) ;
}
@Override
protected void afterStatementEvaluation() throws Exception {
if( httpDaemon != null ) { // Avoids adding noise to some failing test.
httpDaemon.stop() ;
}
// Don't nullify. This prevents from calling a setup method a second time.
}
// ============
// Daemon setup
// ============
protected final void setup() throws Exception {
daemonSetup( UTF_8 ) ;
}
protected final String setup( final Resource resource ) throws Exception {
resourceInstaller.copy( resource ) ;
daemonSetup( DefaultCharset.RENDERING ) ;
final String novellaSource = resource.getAsString( DefaultCharset.SOURCE ) ;
return novellaSource ;
}
protected final void setup(
final File styleDirectory,
final Charset renderingCharset
) throws Exception {
daemonSetup( styleDirectory, renderingCharset ) ;
}
public final String alternateSetup( // TODO rename into setupAndLoadSourceDocument
final Resource resource,
final Charset sourceCharset,
final Charset renderingCharset
) throws Exception {
resourceInstaller.copy( resource ) ;
daemonSetup( renderingCharset ) ;
final String novellaSource = resource.getAsString( sourceCharset ) ;
return novellaSource ;
}
private void daemonSetup( final File styleDirectory, final Charset renderingCharset )
throws Exception
{
httpDaemon = new HttpDaemon( ResourceTools.createDaemonConfiguration(
daemonPort,
resourceInstaller.getTargetDirectory(),
CompositeResourceLoader.create( ConfigurationTools.BUNDLED_STYLE_DIR, styleDirectory )
) ) ;
httpDaemon.start() ;
}
private void daemonSetup( final Charset renderingCharset )
throws Exception
{
httpDaemon = new HttpDaemon( ResourceTools.createDaemonConfiguration(
daemonPort,
resourceInstaller.getTargetDirectory(),
renderingCharset
) ) ;
httpDaemon.start() ;
}
protected final void setupWithFonts( final Directory fontDirectory )
throws Exception
{
final File directoryAsFile = resourceInstaller.copy( fontDirectory ) ;
final DaemonParameters daemonParameters = new DaemonParameters(
resourceInstaller.getTargetDirectory(),
GenericParametersConstants.OPTIONPREFIX + DaemonParameters.OPTIONNAME_HTTPDAEMON_PORT,
"" + daemonPort,
GenericParametersConstants.OPTIONPREFIX + GenericParametersConstants.OPTIONNAME_FONT_DIRECTORIES,
directoryAsFile.getAbsolutePath()
) ;
httpDaemon = new HttpDaemon(
ConfigurationTools.createDaemonConfiguration( daemonParameters ) ) ;
httpDaemon.start() ;
}
// ========
// Requests
// ========
private final AtomicInteger documentWriteCounter = new AtomicInteger( 0 ) ;
public HttpGet createHttpGet( final String documentRequestAsString ) {
return new HttpGet( "http://localhost:" + daemonPort + documentRequestAsString ) ;
}
public byte[] readAsBytes( final String documentRequestAsString )
throws IOException
{
return readAsBytes( new URL( "http://localhost:" + daemonPort + documentRequestAsString ) ) ;
}
public String readAsString( final Resource resource ) throws IOException {
return readAsString( "/" + resource.getName(), UTF_8 ) ;
}
public String readAsString( final Resource resource, final Charset charset ) throws IOException {
return readAsString( "/" + resource.getName(), charset ) ;
}
public String readAsString( final String documentRequestAsString ) throws IOException {
return new String( readAsBytes( documentRequestAsString ), UTF_8 ) ;
}
public String readAsString( final String documentRequestAsString, final Charset charset )
throws IOException
{
return new String( readAsBytes( documentRequestAsString ), charset ) ;
}
private byte[] readAsBytes( final URL url ) throws IOException {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream() ;
IOUtils.copy( url.openStream(), outputStream ) ;
final byte[] bytes = outputStream.toByteArray() ;
saveResponseContent( bytes ) ;
return bytes ;
}
protected final void renderAndCheckStatusCode( final String documentRequestAsString )
throws IOException
{
final HttpGet httpGet = new HttpGet(
"http://localhost:" + daemonPort + documentRequestAsString ) ;
final HttpResponse httpResponse = new DefaultHttpClient().execute( httpGet ) ;
final ByteArrayOutputStream responseContent = new ByteArrayOutputStream() ;
IOUtils.copy( httpResponse.getEntity().getContent(), responseContent ) ;
saveResponseContent( responseContent.toByteArray() ) ;
final int statusCode = httpResponse.getStatusLine().getStatusCode();
assertEquals( ( long ) HttpStatus.SC_OK, ( long ) statusCode ) ;
}
protected final HttpDaemonFixture.ResponseSnapshot followRedirection( final String originalUrlAsString )
throws IOException
{
return followRedirection( originalUrlAsString, HttpDaemonFixture.DEFAULT_USER_AGENT ) ;
}
/**
* Follows redirection using {@link org.apache.http.client.HttpClient}'s default, and returns response body.
*/
protected final HttpDaemonFixture.ResponseSnapshot followRedirection(
final String originalUrlAsString,
final String userAgent
) throws IOException {
final List< Header > locationsRedirectedTo = Lists.newArrayList() ;
final AbstractHttpClient httpClient = new DefaultHttpClient() ;
httpClient.setRedirectHandler( new RecordingRedirectHandler( locationsRedirectedTo ) ) ;
final HttpParams parameters = new BasicHttpParams() ;
parameters.setIntParameter( CoreConnectionPNames.SO_TIMEOUT, HttpDaemonFixture.TIMEOUT ) ;
final HttpGet httpGet = new HttpGet( originalUrlAsString ) ;
httpGet.setHeader( "User-Agent", userAgent ) ;
httpGet.setParams( parameters ) ;
final HttpResponse httpResponse = httpClient.execute( httpGet ) ;
final HttpDaemonFixture.ResponseSnapshot responseSnapshot =
new HttpDaemonFixture.ResponseSnapshot( httpResponse, locationsRedirectedTo ) ;
saveResponseContent( responseSnapshot.getContent().getBytes() ) ;
return responseSnapshot ;
}
// =====
// Other
// =====
private File fileForNextResponseContent = null ;
/**
* Hints {@link #saveResponseContent(byte[])} of the next file to save to.
* @param fileForNextResponseContent
*/
public void setFileForNextResponseContent( final File fileForNextResponseContent ) {
Preconditions.checkState( this.fileForNextResponseContent == null,
"Already set: %s", this.fileForNextResponseContent );
this.fileForNextResponseContent = Preconditions.checkNotNull( fileForNextResponseContent ) ;
}
/**
* TODO: keep documents in memory, dump them only if the test fails.
* This will avoid a few disk-based operations.
*
* @see #setFileForNextResponseContent(java.io.File)
*/
private void saveResponseContent( final byte[] bytes ) throws IOException {
final int counter = documentWriteCounter.getAndIncrement() ;
final File responseContentDirectory = new File( getDirectory(), "saved" ) ;
responseContentDirectory.mkdirs() ;
final File savedContent ;
if( fileForNextResponseContent == null ) {
savedContent = new File( responseContentDirectory, "saved-" + counter ) ;
} else {
savedContent = fileForNextResponseContent ;
}
try {
FileUtils.writeByteArrayToFile( savedContent, bytes ) ;
} finally {
fileForNextResponseContent = null ;
}
LOGGER.info( "Wrote file '", savedContent.getAbsolutePath(), "'" ) ;
}
public URL createUrl( final String requestAsString ) {
try {
return new URL( "http://localhost:" + daemonPort + requestAsString ) ;
} catch( MalformedURLException e ) {
throw new IllegalArgumentException(
"Couldn't make a URL out from '" + requestAsString + "'", e ) ;
}
}
private static class RecordingRedirectHandler extends DefaultRedirectHandler {
private final List< Header > locations ;
public RecordingRedirectHandler( final List< Header > locations ) {
this.locations = locations ;
}
@Override
public URI getLocationURI( final HttpResponse response, final HttpContext context )
throws ProtocolException
{
locations.addAll( Arrays.asList( response.getHeaders( "Location" ) ) ) ;
return super.getLocationURI( response, context );
}
}
}