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.TreeMap; 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.datamodel.answers.AnswerElement; import org.batfish.datamodel.questions.Question; import org.batfish.question.QuestionPlugin; import org.batfish.question.jsonpath.JsonPathResult.JsonPathResultEntry; import org.batfish.question.NodesQuestionPlugin.NodesAnswerer; import org.batfish.question.NodesQuestionPlugin.NodesQuestion; 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 NodesPathQuestionPlugin extends QuestionPlugin { public static class NodesPathAnswerElement implements AnswerElement { private SortedMap<Integer, JsonPathResult> _results; public NodesPathAnswerElement() { _results = new TreeMap<>(); } public SortedMap<Integer, JsonPathResult> getResults() { return _results; } @Override public String prettyPrint() { return JsonPathQuestionPlugin.JsonPathAnswerElement .prettyPrint(_results); } public void setResults(SortedMap<Integer, JsonPathResult> results) { _results = results; } } public static class NodesPathAnswerer extends Answerer { public NodesPathAnswerer(Question question, IBatfish batfish) { super(question, batfish); } @Override public NodesPathAnswerElement answer() { ConfigurationBuilder b = new ConfigurationBuilder(); b.jsonProvider(new JacksonJsonNodeJsonProvider()); final Configuration c = b.build(); NodesPathQuestion question = (NodesPathQuestion) _question; List<JsonPathQuery> paths = question.getPaths(); _batfish.checkConfigurations(); NodesQuestion nodesQuestion = new NodesQuestion(); nodesQuestion.setSummary(false); NodesAnswerer nodesAnswerer = new NodesAnswerer(nodesQuestion, _batfish); AnswerElement nodesAnswer = nodesAnswerer.answer(); BatfishObjectMapper mapper = new BatfishObjectMapper(); String nodesAnswerStr = null; try { nodesAnswerStr = mapper.writeValueAsString(nodesAnswer); } 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(); }); NodesPathAnswerElement answerElement = new NodesPathAnswerElement(); answerElement.getResults().putAll(results); return answerElement; } @Override public AnswerElement answerDiff() { _batfish.pushBaseEnvironment(); _batfish.checkEnvironmentExists(); _batfish.popEnvironment(); _batfish.pushDeltaEnvironment(); _batfish.checkEnvironmentExists(); _batfish.popEnvironment(); _batfish.pushBaseEnvironment(); NodesPathAnswerer beforeAnswerer = (NodesPathAnswerer) create( _question, _batfish); NodesPathAnswerElement before = beforeAnswerer.answer(); _batfish.popEnvironment(); _batfish.pushDeltaEnvironment(); NodesPathAnswerer afterAnswerer = (NodesPathAnswerer) create(_question, _batfish); NodesPathAnswerElement after = afterAnswerer.answer(); _batfish.popEnvironment(); return new NodesPathDiffAnswerElement(before, after); } } public static class NodesPathDiffAnswerElement implements AnswerElement { private SortedMap<Integer, JsonPathDiffResult> _results; @JsonCreator public NodesPathDiffAnswerElement() { } public NodesPathDiffAnswerElement(NodesPathAnswerElement before, NodesPathAnswerElement 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 JsonPathQuestionPlugin.JsonPathDiffAnswerElement .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 NodesPath 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 NodesPathQuestion extends Question { private static final String PATHS_VAR = "paths"; private List<JsonPathQuery> _paths; public NodesPathQuestion() { _paths = Collections.emptyList(); } @Override public boolean getDataPlane() { return false; } @Override public String getName() { return "nodespath"; } @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\"", getName(), prettyPrintBase(), PATHS_VAR, _paths); return retString; } @JsonProperty(PATHS_VAR) public void setPaths(List<JsonPathQuery> paths) { _paths = paths; } } @Override protected Answerer createAnswerer(Question question, IBatfish batfish) { return new NodesPathAnswerer(question, batfish); } @Override protected Question createQuestion() { return new NodesPathQuestion(); } }