NativeHandlers.java
/**
* Copyright (C) 2017 HttpBuilder-NG Project
*
* 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 groovyx.net.http;
import groovy.json.JsonBuilder;
import groovy.json.JsonSlurper;
import groovy.lang.Closure;
import groovy.lang.GString;
import groovy.lang.Writable;
import groovy.util.XmlSlurper;
import groovy.util.slurpersupport.GPathResult;
import groovy.xml.StreamingMarkupBuilder;
import groovyx.net.http.util.IoUtils;
import org.apache.xml.resolver.Catalog;
import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class NativeHandlers {
/**
* Default success handler, just returns the passed data, which is the data
* returned by the invoked parser.
*
* @param fromServer Backend independent representation of what the server returned
* @param data The parsed data
* @return The data object.
*/
public static Object success(final FromServer fromServer, final Object data) {
return data;
}
/**
* Default failure handler. Throws an HttpException.
*
* @param fromServer Backend independent representation of what the server returned
* @param data If parsing was possible, this will be the parsed data, otherwise null
* @return Nothing will be returned, the return type is Object for interface consistency
* @throws HttpException
*/
public static Object failure(final FromServer fromServer, final Object data) {
throw new HttpException(fromServer, data);
}
/**
* Default exception handler. Throws a RuntimeException.
*
* @param thrown The original thrown exception
* @return Nothing will be returned, the return type is Object for interface consistency
* @throws RuntimeException
*/
public static Object exception(final Throwable thrown) {
final RuntimeException rethrow = ((thrown instanceof RuntimeException) ?
(RuntimeException) thrown :
new RuntimeException(thrown));
throw rethrow;
}
protected static class Expanding {
CharBuffer charBuffer = CharBuffer.allocate(2048);
final char[] charAry = new char[2048];
private void resize(final int toWrite) {
final int byAtLeast = toWrite - charBuffer.remaining();
int next = charBuffer.capacity() << 1;
while ((next - charBuffer.capacity()) + charBuffer.remaining() < byAtLeast) {
next = next << 1;
}
CharBuffer tmp = CharBuffer.allocate(next);
charBuffer.flip();
tmp.put(charBuffer);
charBuffer = tmp;
}
public void append(final int total) {
if (charBuffer.remaining() < total) {
resize(total);
}
charBuffer.put(charAry, 0, total);
}
}
protected static final ThreadLocal<Expanding> tlExpanding = new ThreadLocal<Expanding>() {
@Override
protected Expanding initialValue() {
return new Expanding();
}
};
/**
* The set of available content encoders.
*/
public static class Encoders {
// TODO: better testing around encoders
public static Object checkNull(final Object body) {
if (body == null) {
throw new NullPointerException("Effective body cannot be null");
}
return body;
}
public static void checkTypes(final Object body, final Class<?>[] allowedTypes) {
final Class<?> type = body.getClass();
for (Class<?> allowed : allowedTypes) {
if (allowed.isAssignableFrom(type)) {
return;
}
}
final String msg = String.format("Cannot encode bodies of type %s, only bodies of: %s",
type.getName(),
Arrays.stream(allowedTypes).map(Class::getName).collect(Collectors.joining(", ")));
throw new IllegalArgumentException(msg);
}
public static InputStream readerToStream(final Reader r, final Charset cs) throws IOException {
return new ReaderInputStream(r, cs);
}
public static InputStream stringToStream(final String s, final Charset cs) {
return new CharSequenceInputStream(s, cs);
}
public static boolean handleRawUpload(final ChainedHttpConfig config, final ToServer ts) {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
final Object body = request.actualBody();
final Charset charset = request.actualCharset();
try {
if (body instanceof File) {
ts.toServer(new FileInputStream((File) body));
return true;
} else if (body instanceof Path) {
ts.toServer(Files.newInputStream((Path) body));
return true;
} else if (body instanceof byte[]) {
ts.toServer(new ByteArrayInputStream((byte[]) body));
return true;
} else if (body instanceof InputStream) {
ts.toServer((InputStream) body);
return true;
} else if (body instanceof Reader) {
ts.toServer(new ReaderInputStream((Reader) body, charset));
return true;
} else {
return false;
}
} catch (IOException e) {
throw new TransportingException(e);
}
}
private static final Class[] BINARY_TYPES = new Class[]{ByteArrayInputStream.class, InputStream.class, byte[].class, Closure.class};
/**
* Standard encoder for binary types. Accepts ByteArrayInputStream, InputStream, and byte[] types.
*
* @param config Fully configured chained request
* @param ts Formatted http body is passed to the ToServer argument
*/
public static void binary(final ChainedHttpConfig config, final ToServer ts) {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
final Object body = checkNull(request.actualBody());
if (handleRawUpload(config, ts)) {
return;
}
checkTypes(body, BINARY_TYPES);
if (body instanceof byte[]) {
ts.toServer(new ByteArrayInputStream((byte[]) body));
} else {
throw new UnsupportedOperationException();
}
}
private static final Class[] TEXT_TYPES = new Class[]{Closure.class, Writable.class, Reader.class, String.class};
/**
* Standard encoder for text types. Accepts String and Reader types
*
* @param config Fully configured chained request
* @param ts Formatted http body is passed to the ToServer argument
*/
public static void text(final ChainedHttpConfig config, final ToServer ts) throws IOException {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
if (handleRawUpload(config, ts)) {
return;
}
final Object body = checkNull(request.actualBody());
checkTypes(body, TEXT_TYPES);
ts.toServer(stringToStream(body.toString(), request.actualCharset()));
}
private static final Class[] FORM_TYPES = {Map.class, String.class};
/**
* Standard encoder for requests with content type 'application/x-www-form-urlencoded'.
* Accepts String and Map types. If the body is a String type the method assumes it is properly
* url encoded and is passed to the ToServer parameter as is. If the body is a Map type then
* the output is generated by the {@link Form} class.
*
* @param config Fully configured chained request
* @param ts Formatted http body is passed to the ToServer argument
*/
public static void form(final ChainedHttpConfig config, final ToServer ts) {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
if (handleRawUpload(config, ts)) {
return;
}
final Object body = checkNull(request.actualBody());
checkTypes(body, FORM_TYPES);
if (body instanceof String) {
ts.toServer(stringToStream((String) body, request.actualCharset()));
} else if (body instanceof Map) {
final Map<?, ?> params = (Map) body;
final String encoded = Form.encode(params, request.actualCharset());
ts.toServer(stringToStream(encoded, request.actualCharset()));
} else {
throw new UnsupportedOperationException();
}
}
private static final Class[] XML_TYPES = new Class[]{String.class, StreamingMarkupBuilder.class};
/**
* Standard encoder for requests with an xml body.
* <p>
* Accepts String and {@link Closure} types. If the body is a String type the method passes the body
* to the ToServer parameter as is. If the body is a {@link Closure} then the closure is converted
* to xml using Groovy's {@link StreamingMarkupBuilder}.
*
* @param config Fully configured chained request
* @param ts Formatted http body is passed to the ToServer argument
*/
public static void xml(final ChainedHttpConfig config, final ToServer ts) {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
if (handleRawUpload(config, ts)) {
return;
}
final Object body = checkNull(request.actualBody());
checkTypes(body, XML_TYPES);
if (body instanceof String) {
ts.toServer(stringToStream((String) body, request.actualCharset()));
} else if (body instanceof Closure) {
final StreamingMarkupBuilder smb = new StreamingMarkupBuilder();
ts.toServer(stringToStream(smb.bind(body).toString(), request.actualCharset()));
} else {
throw new UnsupportedOperationException();
}
}
/**
* Standard encoder for requests with a json body.
* <p>
* Accepts String, {@link GString} and {@link Closure} types. If the body is a String type the method passes the body
* to the ToServer parameter as is. If the body is a {@link Closure} then the closure is converted
* to json using Groovy's {@link JsonBuilder}.
*
* @param config Fully configured chained request
* @param ts Formatted http body is passed to the ToServer argument
*/
public static void json(final ChainedHttpConfig config, final ToServer ts) {
final ChainedHttpConfig.ChainedRequest request = config.getChainedRequest();
if (handleRawUpload(config, ts)) {
return;
}
final Object body = checkNull(request.actualBody());
final String json = ((body instanceof String || body instanceof GString)
? body.toString()
: new JsonBuilder(body).toString());
ts.toServer(stringToStream(json, request.actualCharset()));
}
}
/**
* The default collection of response content parsers.
*/
public static class Parsers {
// TODO: better testing around parsers
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final Logger log = LoggerFactory.getLogger(Parsers.class);
/**
* This CatalogResolver is static to avoid the overhead of re-parsing the catalog definition file every time. Unfortunately, there's no
* way to share a single Catalog instance between resolvers. The {@link Catalog} class is technically not thread-safe, but as long as you
* do not parse catalog files while using the resolver, it should be fine.
*/
public static CatalogResolver catalogResolver;
static {
CatalogManager catalogManager = new CatalogManager();
catalogManager.setIgnoreMissingProperties(true);
catalogManager.setUseStaticCatalog(false);
catalogManager.setRelativeCatalogs(true);
try {
catalogResolver = new CatalogResolver(catalogManager);
catalogResolver.getCatalog().parseCatalog(NativeHandlers.class.getResource("/catalog/html.xml"));
} catch (IOException ex) {
if (log.isWarnEnabled()) {
log.warn("Could not resolve default XML catalog", ex);
}
}
}
/**
* Standard parser for raw bytes.
*
* @param fromServer Backend indenpendent representation of data returned from http server
* @return Raw bytes of body returned from http server
*/
public static byte[] streamToBytes(final ChainedHttpConfig config, final FromServer fromServer) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
IoUtils.transfer(fromServer.getInputStream(), baos, true);
return baos.toByteArray();
}
/**
* Standard parser for text response content.
*
* @param config the http client configuration
* @param fromServer Backend independent representation of data returned from http server
* @return Body of response
*/
public static String textToString(final ChainedHttpConfig config, final FromServer fromServer) {
try {
final Reader reader = new InputStreamReader(fromServer.getInputStream(), fromServer.getCharset());
final Expanding e = tlExpanding.get();
e.charBuffer.clear();
int total;
while ((total = reader.read(e.charAry)) != -1) {
e.append(total);
}
e.charBuffer.flip();
return e.charBuffer.toString();
} catch (IOException ioe) {
throw new TransportingException(ioe);
}
}
/**
* Standard parser for responses with content type 'application/x-www-form-urlencoded'.
*
* @param fromServer Backend indenpendent representation of data returned from http server
* @return Form data
*/
public static Map<String, List<String>> form(final ChainedHttpConfig config, final FromServer fromServer) {
return Form.decode(fromServer.getInputStream(), fromServer.getCharset());
}
/**
* Standard parser for xml responses.
*
* @param fromServer Backend indenpendent representation of data returned from http server
* @return Body of response
*/
public static GPathResult xml(final ChainedHttpConfig config, final FromServer fromServer) {
try {
final XmlSlurper xml = new XmlSlurper();
xml.setEntityResolver(catalogResolver);
xml.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false);
xml.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return xml.parse(new InputStreamReader(fromServer.getInputStream(), fromServer.getCharset()));
} catch (IOException | SAXException | ParserConfigurationException ex) {
throw new TransportingException(ex);
}
}
/**
* Standard parser for json responses.
*
* @param fromServer Backend indenpendent representation of data returned from http server
* @return Body of response
*/
public static Object json(final ChainedHttpConfig config, final FromServer fromServer) {
return new JsonSlurper().parse(new InputStreamReader(fromServer.getInputStream(), fromServer.getCharset()));
}
/**
* Transfers the contents of the {@link InputStream} into the {@link OutputStream}, optionally closing the stream.
*
* @param istream the input stream
* @param ostream the output stream
* @param close whether or not to close the output stream
* @deprecated Use the version in {@link IoUtils} instead - this one just delegates to it
*/
@Deprecated
public static void transfer(final InputStream istream, final OutputStream ostream, final boolean close) {
IoUtils.transfer(istream, ostream, close);
}
}
}