WebUtils.java

  1. /*
  2.  *  WebUtils.java
  3.  *
  4.  *  Copyright (c) 2007-2011, The University of Sheffield.
  5.  *
  6.  *  This file is part of GATE Mímir (see http://gate.ac.uk/family/mimir.html),
  7.  *  and is free software, licenced under the GNU Lesser General Public License,
  8.  *  Version 3, June 2007 (also included with this distribution as file
  9.  *  LICENCE-LGPL3.html).
  10.  *
  11.  *  Dominic Rout 5 Apr 2017
  12.  *  Valentin Tablan, 29 Jan 2010
  13.  *
  14.  *  $Id: WebUtils.java 17423 2014-02-26 10:36:54Z valyt $
  15.  */
  16. package gate.mimir.tool;

  17. import org.apache.http.ConnectionReuseStrategy;
  18. import org.apache.http.auth.AuthScope;
  19. import org.apache.http.auth.UsernamePasswordCredentials;
  20. import org.apache.http.client.CookieStore;
  21. import org.apache.http.client.CredentialsProvider;
  22. import org.apache.http.client.config.CookieSpecs;
  23. import org.apache.http.client.config.RequestConfig;
  24. import org.apache.http.client.methods.CloseableHttpResponse;
  25. import org.apache.http.client.methods.HttpGet;
  26. import org.apache.http.client.methods.HttpPost;
  27. import org.apache.http.client.methods.HttpUriRequest;
  28. import org.apache.http.client.protocol.HttpClientContext;
  29. import org.apache.http.client.utils.URIBuilder;
  30. import org.apache.http.config.SocketConfig;
  31. import org.apache.http.entity.ByteArrayEntity;
  32. import org.apache.http.entity.SerializableEntity;
  33. import org.apache.http.impl.client.BasicCredentialsProvider;
  34. import org.apache.http.impl.client.CloseableHttpClient;
  35. import org.apache.http.impl.client.HttpClientBuilder;
  36. import org.apache.http.impl.client.HttpClients;
  37. import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
  38. import org.apache.http.protocol.HttpContext;

  39. import java.io.*;
  40. import java.net.URISyntaxException;
  41. import java.nio.CharBuffer;
  42. import java.net.HttpURLConnection;
  43. import java.util.concurrent.TimeUnit;

  44. /**
  45.  * A collection of methods that provide various utility functions for web
  46.  * applications.
  47.  */
  48. public class WebUtils {

  49.     protected PoolingHttpClientConnectionManager connectionManager;
  50.     protected CredentialsProvider credsProvider;
  51.     protected CloseableHttpClient client;
  52.     protected boolean hasContext;
  53.     protected CookieStore cookieJar;
  54.     protected UsernamePasswordCredentials creds;


  55.     public WebUtils() {
  56.         this(null, null, null, 10);
  57.     }

  58.     public WebUtils(CookieStore cookieJar) {
  59.         this(null, null, null, 10);
  60.     }

  61.     public WebUtils(String userName, String password) {
  62.         this(null, userName, password, 10);
  63.     }

  64.     public WebUtils(CookieStore cookieJar,
  65.                     String userName, String password) {
  66.         this(cookieJar, userName, password, 10);

  67.     }

  68.     public WebUtils(CookieStore cookieJar,
  69.                     String userName, String password, int maxConnections) {
  70.         connectionManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);
  71.         connectionManager.setMaxTotal(maxConnections);
  72.         connectionManager.setDefaultMaxPerRoute(maxConnections);

  73.         SocketConfig.Builder socketConfigBuilder = connectionManager.getDefaultSocketConfig().custom();

  74.         SocketConfig config = socketConfigBuilder.
  75.                 setSoReuseAddress(true).
  76.                 setSoKeepAlive(true).
  77.                 setSoLinger(0).
  78.                 build();
  79.         connectionManager.setDefaultSocketConfig(config);

  80.         this.cookieJar = cookieJar;


  81.         hasContext = cookieJar != null || userName != null || password != null;

  82.         credsProvider = new BasicCredentialsProvider();
  83.         if (userName != null && password != null) {
  84.             creds = new UsernamePasswordCredentials(userName, password);
  85.         } else {
  86.             creds = null;
  87.         }

  88.         HttpClientBuilder builder = HttpClients.custom()
  89.                 .setConnectionManager(connectionManager)
  90.                 .setDefaultSocketConfig(config);

  91.         if (cookieJar != null) {
  92.             RequestConfig globalConfig = RequestConfig.custom()
  93.                     .setCookieSpec(CookieSpecs.DEFAULT)
  94.                     .build();

  95.             builder
  96.                 .setDefaultCookieStore(cookieJar)
  97.                 .setDefaultRequestConfig(globalConfig);
  98.         }

  99.         client = builder.build();
  100.     }

  101.     /**
  102.      * Constructs a URL from a base URL segment and a set of query parameters.
  103.      * @param urlBase the string that will be the prefix of the returned.
  104.      * This should include everything apart from the query part of the URL.
  105.      * @param params an array of String values, which should contain alternating
  106.      * parameter names and parameter values. It is obvious that the size of this
  107.      * array must be an even number.
  108.      * @return a URl built according to the provided parameters. If for example
  109.      * the following parameter values are provided: <b>urlBase:</b>
  110.      * <tt>http://host:8080/appName/service</tt>; <b>params:</b> <tt>foo1, bar1,
  111.      * foo2, bar2, foo3, bar3</tt>, then the following URL would be returned:
  112.      * <tt>http://host:8080/appName/service?foo1=bar1&amp;foo2=bar2&amp;foo3=bar3</tt>
  113.      */
  114.     public static String buildUrl(String urlBase, String... params){
  115.         StringBuilder str = new StringBuilder(urlBase);
  116.         if(params != null && params.length > 0){
  117.             str.append('?');
  118.             for(int i = 0 ; i < (params.length/2) - 1; i++){
  119.                 str.append(params[i * 2]);
  120.                 str.append('=');
  121.                 str.append(params[i * 2 + 1]);
  122.                 str.append('&');
  123.             }
  124.             //and now, the last parameter
  125.             str.append(params[params.length - 2]);
  126.             str.append('=');
  127.             str.append(params[params.length - 1]);
  128.         }
  129.         return str.toString();
  130.     }


  131.     protected HttpContext getContext() {
  132.         HttpClientContext context = HttpClientContext.create();
  133.         context.setCredentialsProvider(credsProvider);
  134.         if (this.cookieJar != null) {
  135.             context.setCookieStore(this.cookieJar);
  136.         }

  137.         return context;
  138.     }

  139.     public CloseableHttpResponse execute(HttpUriRequest request) throws IOException {
  140.         // If we have a context, we have to generate a new one for each request,
  141.         // because sharing them between threads seems to break after a few hundred thousan
  142.         // requests.
  143.         if (hasContext) {
  144.             // Fetch a context to use.
  145.             HttpContext context = getContext();

  146.             if (creds != null) {
  147.                 // Attach any credentials provided to the given host.
  148.                 credsProvider.setCredentials(
  149.                         new AuthScope(request.getURI().getHost(),
  150.                                 AuthScope.ANY_PORT),
  151.                         creds);
  152.             }

  153.             // Run the request.
  154.             return this.client.execute(request, context);
  155.         } else {
  156.             // No cookies or auth needed - just run the request as is.
  157.             return this.client.execute(request);
  158.         }
  159.     }

  160.     /**
  161.      * Calls a web service action (i.e. it connects to a URL). If the connection
  162.      * fails, for whatever reason, or the response code is different from
  163.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  164.      * This method will write all content available from the
  165.      * input stream of the resulting connection to the provided Appendable.
  166.      *
  167.      * @param out     an {@link Appendable} to which the output is written.
  168.      * @param baseUrl the constant part of the URL to be accessed.
  169.      * @param params  an array of String values, that contain an alternation of
  170.      *                parameter name, and parameter values.
  171.      * @throws IOException if the connection fails.
  172.      */
  173.     public void getText(final Appendable out, String baseUrl, String... params)
  174.             throws IOException {
  175.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  176.         new RequestExecutor<Void>(this)
  177.                 .runRequest(request, response -> {
  178.                     InputStream contentInputStream = response.getEntity().getContent();
  179.                     try {
  180.                         Reader r = new InputStreamReader(contentInputStream, "UTF-8");
  181.                         char[] bufArray = new char[4096];
  182.                         CharBuffer buf = CharBuffer.wrap(bufArray);
  183.                         int charsRead = -1;
  184.                         while ((charsRead = r.read(bufArray)) >= 0) {
  185.                             buf.position(0);
  186.                             buf.limit(charsRead);
  187.                             out.append(buf);

  188.                         }
  189.                     } finally {
  190.                         contentInputStream.close();
  191.                     }
  192.                     return null;
  193.                 });
  194.     }

  195.     /**
  196.      * Calls a web service action (i.e. it connects to a URL). If the connection
  197.      * fails, for whatever reason, or the response code is different from
  198.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  199.      * This method will write all content available from the
  200.      * input stream of the resulting connection to a String and return it.
  201.      *
  202.      * @param baseUrl the constant part of the URL to be accessed.
  203.      * @param params  an array of String values, that contain an alternation of
  204.      *                parameter name, and parameter values.
  205.      * @throws IOException if the connection fails.
  206.      */
  207.     public String getString(String baseUrl, String... params) throws IOException {
  208.         StringBuffer resultBuffer = new StringBuffer();
  209.         getText(resultBuffer, baseUrl, params);
  210.         return resultBuffer.toString();
  211.     }


  212.     /**
  213.      * Calls a web service action (i.e. it connects to a URL), and reads a
  214.      * serialised int value from the resulting connection. If the connection
  215.      * fails, for whatever reason, or the response code is different from
  216.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  217.      * This method will drain (and discard) all additional content available from
  218.      * either the input and error streams of the resulting connection (which
  219.      * should permit connection keepalives).
  220.      *
  221.      * @param baseUrl the constant part of the URL to be accessed.
  222.      * @param params  an array of String values, that contain an alternation of
  223.      *                parameter name, and parameter values.
  224.      * @throws IOException if the connection fails.
  225.      */
  226.     public int getInt(String baseUrl, String... params)
  227.             throws IOException {
  228.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  229.         return new RequestExecutor<Integer>(this)
  230.                 .runObjectRequest(request, (ObjectInputStream o) -> o.readInt());
  231.     }

  232.     /**
  233.      * Calls a web service action (i.e. it connects to a URL). If the connection
  234.      * fails, for whatever reason, or the response code is different from
  235.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  236.      * This method will drain (and discard) all content available from either the
  237.      * input and error streams of the resulting connection (which should permit
  238.      * connection keepalives).
  239.      *
  240.      * @param baseUrl the constant part of the URL to be accessed.
  241.      * @param params  an array of String values, that contain an alternation of
  242.      *                parameter name, and parameter values.
  243.      * @throws IOException if the connection fails.
  244.      */
  245.      public void getVoid(String baseUrl, String... params) throws IOException {
  246.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  247.         new RequestExecutor<Void>(this)
  248.                 .runRequest(request, response -> null);
  249.     }

  250.     /**
  251.      * Calls a web service action (i.e. it connects to a URL), and reads a
  252.      * serialised long value from the resulting connection. If the connection
  253.      * fails, for whatever reason, or the response code is different from
  254.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  255.      * This method will drain (and discard) all additional content available from
  256.      * either the input and error streams of the resulting connection (which
  257.      * should permit connection keepalives).
  258.      *
  259.      * @param baseUrl the constant part of the URL to be accessed.
  260.      * @param params  an array of String values, that contain an alternation of
  261.      *                parameter name, and parameter values.
  262.      * @throws IOException if the connection fails.
  263.      */
  264.      public long getLong(String baseUrl, String... params)
  265.             throws IOException {
  266.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  267.         return new RequestExecutor<Long>(this)
  268.                 .runObjectRequest(request, (ObjectInputStream o) -> o.readLong());
  269.     }

  270.     /**
  271.      * Calls a web service action (i.e. it connects to a URL), and reads a
  272.      * serialised double value from the resulting connection. If the connection
  273.      * fails, for whatever reason, or the response code is different from
  274.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  275.      * This method will drain (and discard) all additional content available from
  276.      * either the input and error streams of the resulting connection (which
  277.      * should permit connection keepalives).
  278.      *
  279.      * @param baseUrl the constant part of the URL to be accessed.
  280.      * @param params  an array of String values, that contain an alternation of
  281.      *                parameter name, and parameter values.
  282.      * @throws IOException if the connection fails.
  283.      */
  284.      public double getDouble(String baseUrl, String... params)
  285.             throws IOException {
  286.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  287.         return new RequestExecutor<Double>(this)
  288.                 .runObjectRequest(request, (ObjectInputStream o) -> o.readDouble());
  289.     }

  290.     /**
  291.      * Calls a web service action (i.e. it connects to a URL), and reads a
  292.      * serialised boolean value from the resulting connection. If the connection
  293.      * fails, for whatever reason, or the response code is different from
  294.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  295.      * This method will drain (and discard) all additional content available from
  296.      * either the input and error streams of the resulting connection (which
  297.      * should permit connection keepalives).
  298.      *
  299.      * @param baseUrl the constant part of the URL to be accessed.
  300.      * @param params  an array of String values, that contain an alternation of
  301.      *                parameter name, and parameter values.
  302.      * @throws IOException if the connection fails.
  303.      */
  304.     public boolean getBoolean(String baseUrl, String... params)
  305.             throws IOException {
  306.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  307.         return new RequestExecutor<Boolean>(this)
  308.                 .runObjectRequest(request, ObjectInputStream::readBoolean);
  309.     }

  310.     /**
  311.      * Calls a web service action (i.e. it connects to a URL), and reads a
  312.      * serialised Object value from the resulting connection. If the connection
  313.      * fails, for whatever reason, or the response code is different from
  314.      * {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  315.      * This method will drain (and discard) all additional content available from
  316.      * either the input and error streams of the resulting connection (which
  317.      * should permit connection keepalives).
  318.      *
  319.      * @param baseUrl the constant part of the URL to be accessed.
  320.      * @param params  an array of String values, that contain an alternation of
  321.      *                parameter name, and parameter values.
  322.      * @throws IOException            if the connection fails.
  323.      * @throws ClassNotFoundException if the value read from the remote connection
  324.      *                                is of a type unknown to the local JVM.
  325.      */
  326.     public Object getObject(String baseUrl, String... params)
  327.             throws IOException, ClassNotFoundException {
  328.         HttpGet request = new HttpGet(buildUrl(baseUrl, params));

  329.         try {
  330.             return new RequestExecutor<>(this)
  331.                     .runObjectRequest(request, ObjectInputStream::readObject);

  332.         } catch (RuntimeException e) {
  333.             if (e.getCause() instanceof ClassNotFoundException) {
  334.                 throw (ClassNotFoundException) e.getCause();
  335.             } else {
  336.                 throw e;
  337.             }
  338.         }
  339.     }

  340.     /**
  341.      * Calls a web service action (i.e. it connects to a URL) using the POST HTTP
  342.      * method, sending the given object in Java serialized format as the request
  343.      * body.  The request is sent using chunked transfer encoding, and the
  344.      * request's Content-Type is set to application/octet-stream.  If the
  345.      * connection fails, for whatever reason, or the response code is different
  346.      * from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  347.      * This method will drain (and discard) all content available from either the
  348.      * input and error streams of the resulting connection (which should permit
  349.      * connection keepalives).
  350.      *
  351.      * @param baseUrl the constant part of the URL to be accessed.
  352.      * @param object  the object to serialize and send in the POST body
  353.      * @param params  an array of String values, that contain an alternation of
  354.      *                parameter name, and parameter values.
  355.      * @throws IOException if the connection fails.
  356.      */
  357.     public void postObject(String baseUrl, Serializable object,
  358.                            String... params) throws IOException {
  359.         HttpPost request = new HttpPost(buildUrl(baseUrl, params));

  360.         request.setHeader("Content-Type", "application/octet-stream");
  361.         // Set up the entity to send to the server.
  362.         SerializableEntity entity = new SerializableEntity(object);
  363.         entity.setChunked(true);
  364.         request.setEntity(entity);

  365.         // Now run the request
  366.         new RequestExecutor<Void>(this)
  367.                 .runRequest(request, a -> null);
  368.     }

  369.     /**
  370.      * Calls a web service action (i.e. it connects to a URL) using the POST HTTP
  371.      * method, sending the given bytes as the request
  372.      * body.  The request is sent using chunked transfer encoding, and the
  373.      * request's Content-Type is set to application/octet-stream.  If the
  374.      * connection fails, for whatever reason, or the response code is different
  375.      * from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  376.      * This method will drain (and discard) all content available from either the
  377.      * input and error streams of the resulting connection (which should permit
  378.      * connection keepalives).
  379.      *
  380.      * @param baseUrl the constant part of the URL to be accessed.
  381.      * @param data    a {@link ByteArrayOutputStream} containing the data to be
  382.      *                written. Its {@link ByteArrayOutputStream#writeTo(OutputStream)} method
  383.      *                will be called causing it to write its data to the output connection.
  384.      * @param params  an array of String values, that contain an alternation of
  385.      *                parameter name, and parameter values.
  386.      * @throws IOException if the connection fails.
  387.      */
  388.     public void postData(String baseUrl, ByteArrayOutputStream data,
  389.                          String... params) throws IOException {
  390.         HttpPost request = new HttpPost(buildUrl(baseUrl, params));

  391.         request.setHeader("Content-Type", "application/octet-stream");

  392.         ByteArrayEntity entity = new ByteArrayEntity(data.toByteArray());
  393.         entity.setChunked(true);
  394.         request.setEntity(entity);

  395.         new RequestExecutor<Void>(this)
  396.                 .runRequest(request, a -> null);
  397.     }

  398.     /**
  399.      * Calls a web service action (i.e. it connects to a URL) using the POST HTTP
  400.      * method, sending the given object in Java serialized format as the request
  401.      * body.  The request is sent using chunked transfer encoding, and the
  402.      * request's Content-Type is set to application/octet-stream.  If the
  403.      * connection fails, for whatever reason, or the response code is different
  404.      * from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
  405.      * The response from the server is read and Java-deserialized, the resulting
  406.      * Object being returned.
  407.      * <p>
  408.      * This method will then drain (and discard) all the remaining content
  409.      * available from either the input and error streams of the resulting
  410.      * connection (which should permit connection keepalives).
  411.      *
  412.      * @param baseUrl the constant part of the URL to be accessed.
  413.      * @param object  the object to serialize and send in the POST body
  414.      * @param params  an array of String values, that contain an alternation of
  415.      *                parameter name, and parameter values.
  416.      * @return the de-serialized value sent by the remote endpoint.
  417.      * @throws IOException            if the connection fails.
  418.      * @throws ClassNotFoundException if the data sent from the remote endpoint
  419.      *                                cannot be deserialized to a class locally known.
  420.      */
  421.     public Object rpcCall(String baseUrl, Serializable object,
  422.                           String... params) throws IOException, ClassNotFoundException {
  423.         HttpPost request = new HttpPost(buildUrl(baseUrl, params));
  424.         request.setHeader("Content-Type", "application/octet-stream");
  425.         // Set up the entity to send to the server.
  426.         SerializableEntity entity = new SerializableEntity(object);
  427.         entity.setChunked(true);
  428.         request.setEntity(entity);

  429.         // Now run the request
  430.         return new RequestExecutor<>(this)
  431.                 .runObjectRequest(request, ObjectInputStream::readObject);
  432.     }

  433.     protected static class RequestExecutor<T> {
  434.         private WebUtils webUtils;

  435.         RequestExecutor(WebUtils webUtils) {
  436.             this.webUtils = webUtils;
  437.         }

  438.         public T runRequest(HttpUriRequest request, CheckedRequestConsumer<T> consumer) throws IOException {
  439.             CloseableHttpResponse response = webUtils.execute(request);
  440.             try {
  441.                 long code = response.getStatusLine().getStatusCode();

  442.                 if (code == HttpURLConnection.HTTP_OK) {
  443.                     // try to get more details
  444.                     return consumer.run(response);
  445.                 } else {
  446.                     // some problem -> try to get more details
  447.                     String message = response.getStatusLine().getReasonPhrase();
  448.                     throw new IOException(code
  449.                             + (message != null ? " (" + message + ")" : "")
  450.                             + " Remote connection failed.");
  451.                 }
  452.             } catch (ClassNotFoundException e) {
  453.                 throw new RuntimeException(e);
  454.             } finally {
  455.                 // make sure the connection is drained, to allow connection keepalive
  456.                 response.close();
  457.             }
  458.         }

  459.         public T runObjectRequest(HttpUriRequest request, final CheckedObjectInputStreamConsumer<T> consumer) throws IOException {
  460.             return runRequest(request, (CloseableHttpResponse response) -> {
  461.                 InputStream contentInputStream = null;
  462.                 try {
  463.                     contentInputStream = response.getEntity().getContent();
  464.                     return consumer.run(new ObjectInputStream(contentInputStream));
  465.                 } finally {
  466.                     contentInputStream.close();
  467.                 }
  468.             });
  469.         }


  470.         public interface CheckedRequestConsumer<T> {
  471.             T run(CloseableHttpResponse response) throws IOException, ClassNotFoundException;
  472.         }

  473.         public interface CheckedObjectInputStreamConsumer<T> {
  474.             T run(ObjectInputStream response) throws IOException, ClassNotFoundException;
  475.         }
  476.     }


  477. }