package org.batfish.question.jsonpath; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.batfish.common.Answerer; import org.batfish.common.BatfishException; import org.batfish.common.plugin.IBatfish; import org.batfish.common.util.BatfishObjectMapper; import org.batfish.common.util.CommonUtil; import org.batfish.datamodel.answers.AnswerElement; import org.batfish.datamodel.questions.Question; import org.batfish.question.QuestionPlugin; import org.batfish.question.jsonpath.JsonPathResult.JsonPathResultEntry; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.Configuration.ConfigurationBuilder; import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; public class JsonPathQuestionPlugin extends QuestionPlugin { public static class JsonPathAnswerElement implements AnswerElement { private static final String RESULTS_VAR = "results"; static String prettyPrint(SortedMap<Integer, JsonPathResult> results) { StringBuilder sb = new StringBuilder("Results for nodespath\n"); for (Integer index : results.keySet()) { JsonPathResult result = results.get(index); sb.append(String.format(" [%d]: %d results for %s\n", index, result.getNumResults(), result.getPath().toString())); for (JsonPathResultEntry resultEntry : result.getResult() .values()) { ConcreteJsonPath path = resultEntry.getConcretePath(); JsonNode suffix = resultEntry.getSuffix(); String pathString = path.toString(); if (suffix != null) { sb.append(String.format(" %s : %s\n", pathString, suffix.toString())); } else { sb.append(String.format(" %s\n", pathString)); } } } return sb.toString(); } private SortedMap<Integer, JsonPathResult> _results; public JsonPathAnswerElement() { _results = new TreeMap<>(); } @JsonProperty(RESULTS_VAR) public SortedMap<Integer, JsonPathResult> getResults() { return _results; } @Override public String prettyPrint() { return prettyPrint(_results); } @JsonProperty(RESULTS_VAR) public void setResults(SortedMap<Integer, JsonPathResult> results) { _results = results; } } public static class JsonPathAnswerer extends Answerer { public JsonPathAnswerer(Question question, IBatfish batfish) { super(question, batfish); } @Override public JsonPathAnswerElement answer() { ConfigurationBuilder b = new ConfigurationBuilder(); b.jsonProvider(new JacksonJsonNodeJsonProvider()); final Configuration c = b.build(); JsonPathQuestion question = (JsonPathQuestion) _question; List<JsonPathQuery> paths = question.getPaths(); _batfish.checkConfigurations(); Question innerQuestion = question._innerQuestion; String innerQuestionName = innerQuestion.getName(); Answerer innerAnswerer = _batfish.getAnswererCreators() .get(innerQuestionName).apply(innerQuestion, _batfish); AnswerElement innerAnswer = innerAnswerer.answer(); BatfishObjectMapper mapper = new BatfishObjectMapper(); String nodesAnswerStr = null; try { nodesAnswerStr = mapper.writeValueAsString(innerAnswer); } catch (IOException e) { throw new BatfishException( "Could not get JSON string from nodes answer", e); } Object jsonObject = JsonPath.parse(nodesAnswerStr, c).json(); Map<Integer, JsonPathResult> results = new ConcurrentHashMap<>(); List<Integer> indices = new ArrayList<>(); for (int i = 0; i < paths.size(); i++) { indices.add(i); } AtomicInteger completed = _batfish.newBatch("NodesPath queries", indices.size()); indices.parallelStream().forEach(i -> { JsonPathQuery nodesPath = paths.get(i); String path = nodesPath.getPath(); ConfigurationBuilder prefixCb = new ConfigurationBuilder(); prefixCb.mappingProvider(c.mappingProvider()); prefixCb.jsonProvider(c.jsonProvider()); prefixCb.evaluationListener(c.getEvaluationListeners()); prefixCb.options(c.getOptions()); prefixCb.options(Option.ALWAYS_RETURN_LIST); prefixCb.options(Option.AS_PATH_LIST); Configuration prefixC = prefixCb.build(); ConfigurationBuilder suffixCb = new ConfigurationBuilder(); suffixCb.mappingProvider(c.mappingProvider()); suffixCb.jsonProvider(c.jsonProvider()); suffixCb.evaluationListener(c.getEvaluationListeners()); suffixCb.options(c.getOptions()); suffixCb.options(Option.ALWAYS_RETURN_LIST); Configuration suffixC = suffixCb.build(); ArrayNode prefixes = null; ArrayNode suffixes = null; JsonPath jsonPath = JsonPath.compile(path); try { prefixes = jsonPath.read(jsonObject, prefixC); suffixes = jsonPath.read(jsonObject, suffixC); } catch (PathNotFoundException e) { suffixes = JsonNodeFactory.instance.arrayNode(); prefixes = JsonNodeFactory.instance.arrayNode(); } catch (Exception e) { throw new BatfishException("Error reading JSON path: " + path, e); } int numResults = prefixes.size(); JsonPathResult nodePathResult = new JsonPathResult(); nodePathResult.setPath(nodesPath); nodePathResult.setNumResults(numResults); boolean includeSuffix = nodesPath.getSuffix(); if (!nodesPath.getSummary()) { SortedMap<String, JsonPathResultEntry> result = new TreeMap<>(); Iterator<JsonNode> p = prefixes.iterator(); Iterator<JsonNode> s = suffixes.iterator(); while (p.hasNext()) { JsonNode prefix = p.next(); JsonNode suffix = includeSuffix ? s.next() : null; String prefixStr = prefix.textValue(); if (prefixStr == null) { throw new BatfishException("Did not expect null value"); } ConcreteJsonPath concretePath = new ConcreteJsonPath( prefixStr); result.put(concretePath.toString(), new JsonPathResultEntry(concretePath, suffix)); } nodePathResult.setResult(result); } results.put(i, nodePathResult); completed.incrementAndGet(); }); JsonPathAnswerElement answerElement = new JsonPathAnswerElement(); answerElement.getResults().putAll(results); return answerElement; } @Override public AnswerElement answerDiff() { _batfish.pushBaseEnvironment(); _batfish.checkEnvironmentExists(); _batfish.popEnvironment(); _batfish.pushDeltaEnvironment(); _batfish.checkEnvironmentExists(); _batfish.popEnvironment(); _batfish.pushBaseEnvironment(); JsonPathAnswerer beforeAnswerer = (JsonPathAnswerer) create(_question, _batfish); JsonPathAnswerElement before = beforeAnswerer.answer(); _batfish.popEnvironment(); _batfish.pushDeltaEnvironment(); JsonPathAnswerer afterAnswerer = (JsonPathAnswerer) create(_question, _batfish); JsonPathAnswerElement after = afterAnswerer.answer(); _batfish.popEnvironment(); return new JsonPathDiffAnswerElement(before, after); } } public static class JsonPathDiffAnswerElement implements AnswerElement { static String prettyPrint( SortedMap<Integer, JsonPathDiffResult> results) { StringBuilder sb = new StringBuilder(); results.forEach((index, diff) -> { SortedMap<String, JsonPathResultEntry> added = diff.getAdded(); SortedMap<String, JsonPathResultEntry> removed = diff.getRemoved(); sb.append(String.format(" [%d]: %d added and %d removed for %s\n", index, added.size(), removed.size(), diff.getPath().toString())); SortedSet<String> allKeys = CommonUtil.union(added.keySet(), removed.keySet(), TreeSet::new); for (String key : allKeys) { if (removed.containsKey(key)) { JsonNode removedNode = removed.get(key).getSuffix(); if (removedNode != null) { sb.append(String.format("- %s : %s\n", key.toString(), removedNode.toString())); } else { sb.append(String.format("- %s\n", key.toString())); } } if (added.containsKey(key)) { JsonNode addedNode = added.get(key).getSuffix(); if (addedNode != null) { sb.append(String.format("+ %s : %s\n", key.toString(), addedNode.toString())); } else { sb.append(String.format("+ %s\n", key.toString())); } } } }); String result = sb.toString(); return result; } private SortedMap<Integer, JsonPathDiffResult> _results; @JsonCreator public JsonPathDiffAnswerElement() { } public JsonPathDiffAnswerElement(JsonPathAnswerElement before, JsonPathAnswerElement after) { _results = new TreeMap<>(); for (Integer index : before._results.keySet()) { JsonPathResult nprBefore = before._results.get(index); JsonPathResult nprAfter = after._results.get(index); JsonPathDiffResult diff = new JsonPathDiffResult(nprBefore, nprAfter); _results.put(index, diff); } } public SortedMap<Integer, JsonPathDiffResult> getResults() { return _results; } @Override public String prettyPrint() { return prettyPrint(_results); } public void setResults(SortedMap<Integer, JsonPathDiffResult> results) { _results = results; } } // <question_page_comment> /** * Runs JsonPath <https://github.com/jayway/JsonPath> queries on the JSON * data model that is the output of the 'Nodes' question. * <p> * This query can be used to perform server-side queries for the presence or * absence of specified patterns in the data model induced by the * configurations supplied in the test-rig. * * @type JsonPath onefile * * @param paths * A JSON list of path queries, each of which is a JSON object * containing the remaining documented fields (path, suffix, * summary). For each specified path query, the question returns a * list of paths in the data model matching the criteria of the * query. * * @hparam path (Property of each element of 'paths') The JsonPath query to * execute. * * @hparam suffix (Property of each element of 'paths') Defaults to false. If * true, then each path in the returned list will map to the * remaining content of the datamodel at the end of that path. This * can be useful for debugging, but can also be very verbose. If * false, then each path will map to a null value. * * @hparam summary (Property of each element of 'paths') Defaults to false. * If true, then instead of outputting each matching path, only the * count of matching paths will be output. * * @example bf_answer("NodesPath",paths=[{"path":"$.nodes[*].interfaces[*][?(@.mtu!=1500)].mtu"}]) * Return all interfaces with MTUs not equal to 1500 * */ public static class JsonPathQuestion extends Question { private static final String PATHS_VAR = "paths"; private Question _innerQuestion; private List<JsonPathQuery> _paths; public JsonPathQuestion() { _paths = Collections.emptyList(); } @Override public boolean getDataPlane() { return false; } @JsonProperty(INNER_QUESTION_VAR) public Question getInnerQuestion() { return _innerQuestion; } @Override public String getName() { return "jsonpath"; } @JsonProperty(PATHS_VAR) public List<JsonPathQuery> getPaths() { return _paths; } @Override public boolean getTraffic() { return false; } @Override public String prettyPrint() { String retString = String.format("%s %s%s=\"%s\" %s=\"%s\"", getName(), prettyPrintBase(), PATHS_VAR, _paths, INNER_QUESTION_VAR, _innerQuestion.prettyPrint()); return retString; } @JsonProperty(INNER_QUESTION_VAR) public void setInnerQuestion(Question innerQuestion) { _innerQuestion = innerQuestion; } @JsonProperty(PATHS_VAR) public void setPaths(List<JsonPathQuery> paths) { _paths = paths; } } @Override protected Answerer createAnswerer(Question question, IBatfish batfish) { return new JsonPathAnswerer(question, batfish); } @Override protected Question createQuestion() { return new JsonPathQuestion(); } }