/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2013. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.bigdata.rdf.sail.webapp;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServlet;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import com.bigdata.journal.IIndexManager;
import com.bigdata.rdf.sail.webapp.client.ConnectOptions;
/**
* This class supports making requests to the server with fairly low level control.
* Each operation is set up by calls to the protected methods such as
* {@link #setMethodisPost(String, String)}, {@link #setAllow400s()},
* and then to call {@link #serviceRequest(String...)} to actually
* process the request.
* This process may be repeated multiple times.
* After each call to {@link #serviceRequest(String...)}
* the options are reset to the defaults.
* @author jeremycarroll
*
*/
public abstract class AbstractProtocolTest extends AbstractTestNanoSparqlClient<IIndexManager> {
protected interface RequestFactory {
HttpUriRequest createRequest(String ... params);
};
protected static final String SELECT = "SELECT (1 as ?one){}";
protected static final String ASK = "ASK WHERE {}";
protected static final String CONSTRUCT = "CONSTRUCT { <a:b> <c:d> <e:f> } WHERE {}";
protected static final long PAUSE_BEFORE_CLOSE_TIME = 100;
private static int updateCounter = 0;
private static String update() {
return "INSERT { <http://example.org/a> <http://example.org/a> <http://example.org/" + updateCounter++ + "> } WHERE {}";
}
private static String askIfUpdated() {
return "ASK { <http://example.org/a> <http://example.org/a> <http://example.org/" + updateCounter + "> }";
}
/**
* A SPARQL ASK Query that returns true iff {@Link #update} has successfully run
*/
private final String askIfUpdated = askIfUpdated();
/**
* A SPARQL Update that adds a triple
*/
final String update = update();
HttpServlet servlet;
HttpClient client;
private String responseContentType = null;
private String accept = null;
private boolean permit400s = false;
private Header[] headers = null;
private final String getSparqlURL(final String serviceURL) {
return serviceURL + "/sparql";
}
private final RequestFactory GET = new RequestFactory(){
@Override
public HttpUriRequest createRequest(String... params) {
final StringBuffer url = new StringBuffer();
url.append(getSparqlURL(m_serviceURL));
char sep = '?';
for (int i=0;i<params.length;i+=2) {
url.append(sep);
url.append(params[i]);
url.append('=');
try {
url.append(URLEncoder.encode(params[i+1], "UTF-8"));
} catch (final UnsupportedEncodingException e) {
// JVM must support UTF-8
throw new Error(e);
}
sep='&';
}
return new HttpGet(url.toString());
}
};
private volatile RequestFactory requestFactory = GET;
protected RequestFactory getRequestFactory() {
return requestFactory;
}
@Override
public void setUp() throws Exception {
super.setUp();
client = new DefaultHttpClient(newInstance());
resetDefaultOptions();
}
@Override
public void tearDown() throws Exception {
client.getConnectionManager().shutdown();
client = null;
servlet = null;
super.tearDown();
}
/**
* This method is called automatically after each call to {@link #serviceRequest(String...)}
* so probably is unnecessary.
*/
protected void resetDefaultOptions() {
accept = null;
requestFactory = GET;
accept = null;
permit400s = false;
headers = null;
}
/**
*
* @return The content type of the last response, null if none (e.g. a 204?)
*/
protected String getResponseContentType() {
return responseContentType;
}
/**
* Sets the accept header, default is "*"
* @param mimetype
*/
protected void setAccept(String mimetype) {
accept = mimetype;
}
/**
*
* @param mimetype
*/
protected void setHeaders(Header[] headers) {
this.headers = headers;
}
static private Pattern charset = Pattern.compile("[; ]charset *= *\"?([^ ;\"]*)([ \";]|$)");
/**
* Sanity check the {@link #charset} pattern
* @param argv
*/
public static void main(String argv[]) {
for (final String t:new String[]{
"text/html ; charset=iso-8856-1",
"text/html ; charset=iso-8856-1; foo = bar",
"text/html ;charset=iso-8856-1; foo = bar",
"text/html ; charset= \"iso-8856-1\"",
"text/html ; charset=iso-8856-1; foo = bar",
"text/html ; charset = iso-8856-1; foo = bar",
"text/html ; foo = bar",
"text/html",
}) {
final Matcher m = charset.matcher(t);
System.err.println(t+ " ====> "+(m.find()?m.group(1):""));
}
}
protected ClientConnectionManager newInstance() {
final ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(
newSchemeRegistry());
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost to 50
final HttpHost localhost = new HttpHost("locahost");
cm.setMaxForRoute(new HttpRoute(localhost), 50);
return cm;
}
protected SchemeRegistry newSchemeRegistry() {
final SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory
.getSocketFactory()));
schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory
.getSocketFactory()));
return schemeRegistry;
}
/**
* This is the main entry point for subclasses.
* This method sends a request to the server, as set up
* by setABC methods, and returns the string send back to the client.
* @param paramValues This is an even number of param [=] value pairs. Multiple values for the same param are supported.
* These are passed to the server either as URL query params, or as URL encoded values in the body if the method
* {@link #setMethodisPostUrlEncodedData()} has been called.
* @return the data returned by the server.
* @throws IOException
*/
protected String serviceRequest(final String ... paramValues) throws IOException {
HttpUriRequest req;
responseContentType = null;
try {
try {
req = requestFactory.createRequest(paramValues);
} catch (final Exception e) {
throw new RuntimeException(e);
}
req.setHeader("Accept", accept==null?"*":accept);
if(headers != null) {
req.setHeaders(headers);
}
final HttpResponse resp = client.execute(req);
String page="";
final HttpEntity entity = resp.getEntity();
if (entity != null ) {
String encoding = "utf-8";
assertNotNull("Entity in " + resp.getStatusLine().getStatusCode()+" response must specify content type",entity.getContentType());
final Matcher m = charset.matcher(entity.getContentType().getValue());
if (m.find()) {
encoding = m.group(1);
}
page = QueryServlet.readFully(new InputStreamReader(entity.getContent(),encoding));
responseContentType = entity.getContentType().getValue();
}
if ( resp.getStatusLine().getStatusCode()>=(permit400s?500:400) ) {
fail(resp.getStatusLine().toString()+"\n"+ page);
}
return page;
}
finally {
resetDefaultOptions();
}
}
private Map<String,String[]> pairs2map(String... paramValues) {
final Map<String,String[]> params = new HashMap<String,String[]>();
for (int i=0;i<paramValues.length;i+=2) {
final String key = paramValues[i];
final String value = paramValues[i+1];
final String[] val = params.get(key);
if (val==null) {
params.put(key, new String[]{value});
} else {
// horridly inefficient, never called?
final String nval[] = new String[val.length+1];
System.arraycopy(val, 0, nval, 0, val.length);
nval[val.length] = value;
params.put(key, nval);
}
}
return params;
}
/**
* The method is a POST usng url-encoded form data, with the parameters being those past
to {@link #serviceRequest(String...)} call.
*/
protected void setMethodisPostUrlEncodedData() {
requestFactory = new RequestFactory(){
@Override
public HttpUriRequest createRequest(String... params) {
final HttpPost rslt = new HttpPost(getSparqlURL(m_serviceURL));
try {
rslt.setEntity(ConnectOptions.getFormEntity(pairs2map(params)));
} catch (final Exception e) {
throw new RuntimeException(e);
}
return rslt;
}
};
}
/**
* The method is a POST of the given document
* @param mimeType The mimetype of the document
* @param body The string of the document body
*/
protected void setMethodisPost(String mimeType, String body) {
StringEntity toPostx = null;
try {
toPostx = new StringEntity(body, mimeType,"utf-8");
} catch (final UnsupportedEncodingException e) {
throw new Error(e);
}
final HttpEntity toPost = toPostx;
requestFactory = new RequestFactory(){
@Override
public HttpUriRequest createRequest(String... params) {
final StringBuffer url = new StringBuffer();
url.append(getSparqlURL(m_serviceURL));
char sep = '?';
for (int i=0;i<params.length;i+=2) {
url.append(sep);
url.append(params[i]);
url.append('=');
try {
url.append(URLEncoder.encode(params[i+1], "UTF-8"));
} catch (final UnsupportedEncodingException e) {
// JVM must support UTF-8
throw new Error(e);
}
sep='&';
}
final HttpPost rslt = new HttpPost(url.toString());
rslt.setEntity(toPost);
return rslt;
}
};
}
/**
* Normally a 400 or 404 response fails the test, calling this method allows such responses.
*/
protected void setAllow400s() {
this.permit400s = true;
}
/**
* Assert that the update from {@link #update} has or has not taken place.
* This calls {@link #resetDefaultOptions()}, and the next call to {@link #serviceRequest(String...)}
* will need to be setup after this call.
* @param expected The expected result
* @throws IOException
*/
protected void checkUpdate(boolean expected) throws IOException {
resetDefaultOptions();
assertTrue(serviceRequest("query",askIfUpdated).contains(Boolean.toString(expected)));
}
/**
* The next request is a GET, (this is the default)
*/
protected void setMethodAsGet() {
requestFactory = GET;
}
public AbstractProtocolTest(HttpServlet servlet, String name) {
super(name);
this.servlet = servlet;
}
public AbstractProtocolTest(String name) {
this(new QueryServlet(), name);
}
}