/** 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); } }