// Copyright (c) 2014, SAS Institute Inc., Cary, NC, USA, All Rights Reserved package com.sas.unravl.extractors; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.sas.unravl.ApiCall; import com.sas.unravl.UnRAVL; import com.sas.unravl.UnRAVLException; import com.sas.unravl.annotations.UnRAVLExtractorPlugin; import com.sas.unravl.assertions.UnRAVLAssertionException; import com.sas.unravl.util.Json; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.log4j.Logger; /** * Extract headers from the HTTP response, place them in variables, and * optionally parse them into constituent fields. * <p> * The format is: * * <pre> * { "headers" : json-object } * </pre> * * For example, * * <pre> * { "headers" : { "contentType" : "Content-Type", * "location" : "Location" * } * } * </pre> * * This will bind the value of the Content-Type header to the variable * "contentType" and the value of the Location header to the variable * "location". If the right hand side of a pair is an array instead of a string, * then all elements must be a string. The first is the header name, the second * is a regular expression pattern, and the remainder are names which will be * bound to the groups that are matched from the pattern. The value of the * header is parsed with a regular expression and the groups bound to variables. * For example, * * <pre> * { "headers" : { * "contentType" : [ "Content-Type", "^(.*)\\s*;\\s*charset=(.*)$", "mediaType", "charset" ] * } * } * </pre> * * will bind contentTye to the complete value of the Content-Type header, and * extract the media type and charset into variables named "mediaType" and * "charset" according to the pattern. * <p> * (Note that a per the JSON grammar, \\ characters in a JSON string must be * escaped, so the regular expression notation <code>\\s</code> is coded in the * JSON string as <code>\\\\s</code>.) * * </p> * For example, if the Content-Type header was * * <pre> * application/json;charset=UTF-8 * </pre> * * this headers specification will bind the variables:<br> * responseType to "application/json; charset=UTF-8"<br> * mediaType to "application/json, and <br> * charset to "UTF-8". * <p> * If the regular expression does not match, this extractor will throw an * {@link UnRAVLAssertionException} * <p> * This extractor will unbind all the variables before testing the regular * expression, so that bindings left from other tests won't persist and leave a * false positive. * <p> * The more advanced form works like the {@link PatternExtractor} and binds * regular expression grouping values into additional environment variables. For * example, * * <pre> * [ "Content-Type", "responseType", "^(.*)\\s*;\\s*charset=(.*)$", "mediaType", "charset" ] * </pre> * * will bind the value of the Content-Type header into the environment variable * named "responseType", then perform pattern matching to extract the media type * and the encoding character set into the variables mediaType and charset. * * @author David.Biesack@sas.com */ @UnRAVLExtractorPlugin("headers") public class HeadersExtractor extends BaseUnRAVLExtractor { static Logger logger = Logger.getLogger(UnRAVL.class); @Override public void extract(UnRAVL current, ObjectNode extractor, ApiCall call) throws UnRAVLException { super.extract(current, extractor, call); JsonNode val = Json.firstFieldValue(extractor); if (val.isObject()) { extractHeadesr(current, (ObjectNode) val, call); } else { throw new UnRAVLException( String.format( "Unrecognized headers extractor: object expected but found: %s", val)); } } private void extractHeadesr(UnRAVL current, ObjectNode val, ApiCall call) throws UnRAVLException { for (Map.Entry<String, JsonNode> e : Json.fields(val)) { String varName = e.getKey(); JsonNode node = e.getValue(); ArrayNode a = null; String headerName; if (node.isTextual()) headerName = node.textValue(); else { a = Json.array(node); for (int i = 0; i < a.size(); i++) { if (!a.get(i).isTextual()) throw new UnRAVLException("headers extractor " + val + " must be all strings"); if (i > 1) { call.unbind(a.get(i).textValue()); } } if (a.size() < 2) throw new UnRAVLException( "Header pattern array must contain at least two values."); headerName = a.get(0).textValue(); } Header header = call.getResponseHeader(headerName); if (header == null) throw new UnRAVLException("header not found for binding " + a); logger.trace("header " + header.getName() + ":" + header.getValue()); String headerValue = header.getValue(); getScript().bind(varName, headerValue); if (a != null) bindHeaderByPattern(current, a, headerName, headerValue, 0); } } private void bindHeaderByPattern(UnRAVL current, ArrayNode a, String headerName, String headerValue, int offset) throws UnRAVLAssertionException { String varName; { String regex = current.expand(a.get(offset + 1).textValue()); Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(headerValue); if (matcher.matches()) { for (int i = 1, v = offset + 2; i <= matcher.groupCount() && v < a.size(); i++, v++) { varName = a.get(v).textValue(); String value = matcher.group(i); current.bind(varName, value); } } else throw new UnRAVLAssertionException("header pattern " + regex + " does not match " + headerName + " value " + headerValue); } } }