package org.basex.query.util.http;
import static java.lang.Integer.*;
import static java.net.HttpURLConnection.*;
import static org.basex.data.DataText.*;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Locale;
import org.basex.core.Prop;
import org.basex.io.serial.Serializer;
import org.basex.io.serial.SerializerProp;
import org.basex.query.QueryException;
import org.basex.query.item.ANode;
import org.basex.query.item.B64;
import org.basex.query.item.Bln;
import org.basex.query.item.Hex;
import org.basex.query.item.Item;
import org.basex.query.iter.ItemCache;
import org.basex.query.iter.Iter;
import org.basex.query.util.http.Request.Part;
import org.basex.util.Base64;
import org.basex.util.InputInfo;
import org.basex.util.TokenBuilder;
import org.basex.util.hash.TokenMap;
/**
* HTTP Client.
*
* @author BaseX Team 2005-12, BSD License
* @author Rositsa Shadura
*/
public final class HTTPClient {
/** Request attribute: HTTP method. */
private static final byte[] METHOD = token("method");
/** Request attribute: username. */
private static final byte[] USRNAME = token("username");
/** Request attribute: password. */
private static final byte[] PASSWD = token("password");
/** Request attribute: send-authorization. */
private static final byte[] SENDAUTH = token("send-authorization");
/** Body attribute: media-type. */
private static final byte[] MEDIATYPE = token("media-type");
/** Request attribute: href. */
private static final byte[] HREF = token("href");
/** Request attribute: status-only. */
private static final byte[] STATUSONLY = token("status-only");
/** Request attribute: override-media-type. */
private static final byte[] OVERMEDIATYPE = token("override-media-type");
/** Request attribute: follow-redirect. */
private static final byte[] REDIR = token("follow-redirect");
/** Request attribute: timeout. */
private static final byte[] TIMEOUT = token("timeout");
/** boundary marker. */
private static final byte[] BOUNDARY = token("boundary");
/** Carriage return/line feed. */
private static final byte[] CRLF = token("\r\n");
/** Default multipart boundary. */
private static final String DEFAULT_BOUND = "1BEF0A57BE110FD467A";
/** Body attribute: src. */
private static final byte[] SRC = token("src");
/** Media Types. */
/** XML media type. */
private static final byte[] APPL_XHTML = token("application/html+xml");
/** XML media type. */
private static final byte[] APPL_XML = token("application/xml");
/** XML media type. */
private static final byte[] APPL_EXT_XML =
token("application/xml-external-parsed-entity");
/** XML media type. */
private static final byte[] TXT_XML = token("text/xml");
/** XML media type. */
private static final byte[] TXT_EXT_XML =
token("text/xml-external-parsed-entity");
/** XML media types' suffix. */
private static final byte[] MIME_XML_SUFFIX = token("+xml");
/** HTML media type. */
private static final byte[] TXT_HTML = token("text/html");
/** Text media types' prefix. */
private static final byte[] MIME_TEXT_PREFIX = token("text/");
/** Serialization methods defined by the EXPath specification. */
/** Method http:base64Binary. */
private static final byte[] BASE64 = token("http:base64Binary");
/** Method http:hexBinary. */
private static final byte[] HEXBIN = token("http:hexBinary");
/** HTTP header: Content-Type. */
private static final String CONT_TYPE = "Content-Type";
/** HTTP header: Authorization. */
private static final String AUTH = "Authorization";
/** HTTP basic authentication. */
private static final String AUTH_BASIC = "Basic ";
/** Input information. */
private final InputInfo input;
/** Database properties. */
private final Prop prop;
/**
* Constructor.
* @param ii input info
* @param pr database properties
*/
public HTTPClient(final InputInfo ii, final Prop pr) {
input = ii;
prop = pr;
}
/**
* Sends an HTTP request and returns the response.
* @param href URL to send the request to
* @param request request data
* @param bodies content items
* @return HTTP response
* @throws QueryException query exception
*/
public Iter sendRequest(final byte[] href, final ANode request,
final ItemCache bodies) throws QueryException {
try {
if(request == null) {
if(href == null || href.length == 0) NOPARAMS.thrw(input);
final HttpURLConnection conn = openConnection(string(href));
try {
return new ResponseHandler(input, prop).getResponse(
conn, Bln.FALSE.string(), Bln.FALSE.string());
} finally {
conn.disconnect();
}
}
final Request r = new RequestParser(input).parse(request, bodies);
final byte[] dest = href == null ? r.attrs.get(HREF) : href;
if(dest == null) NOURL.thrw(input);
final HttpURLConnection conn = openConnection(string(dest));
try {
setConnectionProps(conn, r);
setRequestHeaders(conn, r);
if(r.bodyContent.size() != 0 || r.parts.size() != 0) {
setContentType(conn, r);
setRequestContent(conn.getOutputStream(), r);
}
return new ResponseHandler(input, prop).getResponse(
conn, r.attrs.get(STATUSONLY), r.attrs.get(OVERMEDIATYPE));
} finally {
conn.disconnect();
}
} catch(final MalformedURLException ex) {
throw HTTPERR.thrw(input, "Invalid URL");
} catch(final ProtocolException ex) {
throw HTTPERR.thrw(input, "Invalid HTTP method");
} catch(final IOException ex) {
throw HTTPERR.thrw(input, ex);
}
}
/**
* Opens an HTTP connection.
* @param dest HTTP URI to open connection to
* @return HHTP connection
* @throws QueryException query exception
* @throws IOException I/O Exception
* @throws MalformedURLException incorrect url
*/
private HttpURLConnection openConnection(final String dest)
throws QueryException, IOException {
final URL url = new URL(dest);
if(!eqic(url.getProtocol(), "HTTP", "HTTPS"))
HTTPERR.thrw(input, "Invalid URL");
return (HttpURLConnection) url.openConnection();
}
/**
* Sets the connection properties.
* @param conn HTTP connection
* @param r request data
* @throws ProtocolException protocol exception
* @throws QueryException query exception
*/
private void setConnectionProps(final HttpURLConnection conn, final Request r)
throws ProtocolException, QueryException {
if(r.bodyContent != null || r.parts.size() != 0) conn.setDoOutput(true);
conn.setRequestMethod(
string(r.attrs.get(METHOD)).toUpperCase(Locale.ENGLISH));
final byte[] timeout = r.attrs.get(TIMEOUT);
if(timeout != null) conn.setConnectTimeout(parseInt(string(timeout)));
final byte[] redirect = r.attrs.get(REDIR);
if(redirect != null) setFollowRedirects(Bln.parse(redirect, input));
}
/**
* Sets content type of HTTP request.
* @param conn HTTP connection
* @param r request data
*/
private static void setContentType(final HttpURLConnection conn,
final Request r) {
final byte[] contTypeHdr = r.headers.get(lc(token(CONT_TYPE)));
// if header "Content-Type" is set explicitly by the user, its value is used
if(contTypeHdr != null) {
conn.setRequestProperty(CONT_TYPE, string(contTypeHdr));
// otherwise @media-type of <http:body/> is considered
} else {
final String mediaType = string(r.payloadAttrs.get(MEDIATYPE));
if(r.isMultipart) {
final byte[] b = r.payloadAttrs.get(BOUNDARY);
final String boundary = b != null ? string(b) : DEFAULT_BOUND;
final StringBuilder sb = new StringBuilder();
sb.append(mediaType).append("; ").append("boundary=").append(boundary);
conn.setRequestProperty(CONT_TYPE, sb.toString());
} else {
conn.setRequestProperty(CONT_TYPE, mediaType);
}
}
}
/**
* Sets HTTP request headers.
* @param conn HTTP connection
* @param r request data
* @throws QueryException query exception
*/
private void setRequestHeaders(final HttpURLConnection conn,
final Request r) throws QueryException {
final byte[][] headerNames = r.headers.keys();
for(final byte[] headerName : headerNames)
conn.addRequestProperty(string(headerName),
string(r.headers.get(headerName)));
// HTTP Basic Authentication
final byte[] sendAuth = r.attrs.get(SENDAUTH);
if(sendAuth != null && Bln.parse(sendAuth, input))
conn.setRequestProperty(AUTH,
encodeCredentials(string(r.attrs.get(USRNAME)),
string(r.attrs.get(PASSWD))));
}
/**
* Set HTTP request content.
* @param out output stream
* @param r request data
* @throws IOException I/O exception
* @throws QueryException query exception
*/
public void setRequestContent(final OutputStream out, final Request r)
throws IOException, QueryException {
if(r.isMultipart) {
writeMultipart(r, out);
} else {
writePayload(r.bodyContent, r.payloadAttrs, out);
}
out.close();
}
/**
* Encodes credentials with Base64 encoding.
* @param u user name
* @param p password
* @return encoded credentials
*/
private static String encodeCredentials(final String u, final String p) {
return AUTH_BASIC + Base64.encode(u + ':' + p);
}
/**
* Writes the payload of a body or part in the output stream of the
* connection.
* @param payload body/part payload
* @param payloadAtts payload attributes
* @param out output stream
* @throws IOException I/O exception
* @throws QueryException query exception
*/
private void writePayload(final ItemCache payload, final TokenMap payloadAtts,
final OutputStream out) throws IOException, QueryException {
final byte[] mediaType = payloadAtts.get(MEDIATYPE);
byte[] method = payloadAtts.get(METHOD);
final byte[] src = payloadAtts.get(SRC);
// no resource to set the content from
if(src == null) {
// default value @method is determined by @media-type
if(method == null) {
if(eq(mediaType, APPL_XHTML)) method = token(M_XHTML);
else if(eq(mediaType, APPL_XML) || eq(mediaType, APPL_EXT_XML)
|| eq(mediaType, TXT_XML) || eq(mediaType, TXT_EXT_XML)
|| endsWith(mediaType, MIME_XML_SUFFIX)) method = token(M_XML);
else if(eq(mediaType, TXT_HTML)) method = token(M_HTML);
else if(startsWith(mediaType, MIME_TEXT_PREFIX)) method = token(M_TEXT);
// default serialization method is XML
else method = token(M_XML);
}
// write content depending on the method
if(eq(method, BASE64)) {
writeBase64(payload, out);
} else if(eq(method, HEXBIN)) {
writeHex(payload, out);
} else {
write(payload, payloadAtts, method, out);
}
} else {
// if the src attribute is present, the content is set as the content of
// the linked resource
writeResource(src, out);
}
}
/**
* Writes the payload of a body in case method is base64Binary.
* @param payload payload
* @param out connection output stream
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
private void writeBase64(final ItemCache payload,
final OutputStream out) throws IOException, QueryException {
for(int i = 0; i < payload.size(); i++) {
final Item item = payload.get(i);
if(item instanceof B64) {
out.write(((B64) item).toJava());
} else {
out.write(new B64(item.string(input)).toJava());
}
}
}
/**
* Writes the payload of a body in case method is hexBinary.
* @param payload payload
* @param out connection output stream
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
private void writeHex(final ItemCache payload, final OutputStream out)
throws IOException, QueryException {
for(int i = 0; i < payload.size(); i++) {
final Item item = payload.get(i);
if(item instanceof Hex) {
out.write(((Hex) item).toJava());
} else {
out.write(new Hex(item.string(input)).toJava());
}
}
}
/**
* Writes the payload of a body using the serialization parameters.
* @param payload payload
* @param attrs payload attributes
* @param method serialization method
* @param out connection output stream
* @throws IOException I/O Exception
*/
private static void write(final ItemCache payload, final TokenMap attrs,
final byte[] method, final OutputStream out) throws IOException {
// extract serialization parameters
final TokenBuilder tb = new TokenBuilder();
tb.add(METHOD).add('=').add(method);
for(final byte[] key : attrs.keys()) {
if(!eq(key, SRC))
tb.add(',').add(key).add('=').add(attrs.get(key));
}
// serialize items according to the parameters
final SerializerProp sp = new SerializerProp(tb.toString());
final Serializer ser = Serializer.get(out, sp);
try {
payload.serialize(ser);
} finally {
ser.close();
}
}
/**
* Reads the content of the linked resource.
* @param src resource link
* @param out output stream
* @throws IOException I/O Exception
*/
private static void writeResource(final byte[] src, final OutputStream out)
throws IOException {
final InputStream bis = new URL(string(src)).openStream();
try {
final byte[] buf = new byte[256];
while(true) {
final int len = bis.read(buf, 0, buf.length);
if(len <= 0) break;
out.write(buf, 0, len);
}
} finally {
bis.close();
}
}
/**
* Writes parts of multipart message in the output stream of the HTTP
* connection.
* @param r request data
* @param out output stream
* @throws IOException I/O exception
* @throws QueryException query exception
*/
private void writeMultipart(final Request r, final OutputStream out)
throws IOException, QueryException {
final byte[] boundary = r.payloadAttrs.get(BOUNDARY);
for(final Part part : r.parts) writePart(part, out, boundary);
out.write(new TokenBuilder().add("--").
add(boundary).add("--").add(CRLF).finish());
}
/**
* Writes a single part of a multipart message.
* @param part part
* @param out connection output stream
* @param boundary boundary
* @throws IOException I/O exception
* @throws QueryException query exception
*/
private void writePart(final Part part, final OutputStream out,
final byte[] boundary) throws IOException, QueryException {
// write boundary preceded by "--"
final TokenBuilder boundTb = new TokenBuilder();
boundTb.add("--").add(boundary).add(CRLF);
out.write(boundTb.finish());
// write headers
for(final byte[] headerName : part.headers.keys()) {
final TokenBuilder hdrTb = new TokenBuilder();
hdrTb.add(headerName).add(": ").add(
part.headers.get(headerName)).add(CRLF);
out.write(hdrTb.finish());
}
out.write(CRLF);
// write content
writePayload(part.bodyContent, part.bodyAttrs, out);
out.write(CRLF);
}
}