blob: 2a8e8eb946db0b1672ab506962abbeef839c9246 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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 com.facebook.buck.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Map;
/**
* This is a special JSON parser that is customized to consume the JSON output of buck.py.
* Object values may be one of: null, a string, or an array of strings. This means that no
* sort of nested arrays or objects are allowed in the output as Parser is implemented
* today. This simplification makes it easier to leverage Jackson's streaming JSON API.
*/
public class BuildFileToJsonParser {
private final JsonParser parser;
public BuildFileToJsonParser(String json) throws IOException {
JsonFactory jsonFactory = new JsonFactory();
this.parser = jsonFactory.createJsonParser(json);
}
public BuildFileToJsonParser(InputStream json) throws IOException {
JsonFactory jsonFactory = new JsonFactory();
this.parser = jsonFactory.createJsonParser(json);
}
public BuildFileToJsonParser(Reader json) throws IOException {
JsonFactory jsonFactory = new JsonFactory();
this.parser = jsonFactory.createJsonParser(json);
}
/**
* Access the next set of rules from the build file processor. Note that for non-server
* invocations, this will collect all of the rules into one enormous list.
*
* @return List of rules expressed as a <em>very</em> simple mapping of JSON field names
* to Java primitives.
*/
public List<Map<String, Object>> nextRules() throws IOException {
// The parser below uses these objects for stateful purposes with the ultimate goal
// of populating the parsed rules into `currentObjects`.
//
// The parser is expecting two different styles of output:
// 1. Server mode: [{"key": "value"}, {"key": "value"}, ...]
// 2. Regular mode: {"key": "value"}, {"key": "value"}, ...
//
// Server mode output is a necessary short-term step to keep logic in the main Parser
// consistent (expecting to be able to correlate a set of rules with the specific BUCK file
// that generated them). This requirement creates an unnecessary performance weakness
// in this design where we cannot parallelize buck.py's parsing of BUCK files with buck's
// processing of the result into a DAG. Once this can be addressed, server mode should be
// eliminated.
List<Map<String, Object>> currentObjects = null;
String currentFieldName = null;
List<String> currentArray = null;
Map<String, Object> currentObject = null;
while (true) {
JsonToken token = parser.nextToken();
if (token == null) {
if (currentObject != null) {
throw new EOFException("unexpected end-of-stream");
} else if (currentObjects == null) {
// This happens when buck.py failed to produce any output for this build rule (python
// parse error or raised exception, I bet).
throw new EOFException("missing build rules");
} else {
return currentObjects;
}
}
switch (token) {
case START_OBJECT:
// The syntax differs very slightly between server mode and not. If we encounter
// an object not inside of an array, we aren't in server mode and so we can just read
// all the objects until exhaustion.
if (currentObjects == null) {
currentObjects = Lists.newArrayList();
}
currentObject = Maps.newHashMap();
break;
case END_OBJECT:
currentObjects.add(currentObject);
currentObject = null;
break;
case START_ARRAY:
// Heuristic to detect whether we are in the above server mode or regular mode. If we
// encounter START_ARRAY before START_OBJECT, it must be server mode so we should build
// `currentObjects` now. Otherwise, this is the start of a new sub-array within
// an object.
if (currentObjects == null) {
currentObjects = Lists.newArrayList();
} else {
currentArray = Lists.newArrayList();
}
break;
case END_ARRAY:
if (currentArray != null) {
currentObject.put(currentFieldName, currentArray);
currentArray = null;
currentFieldName = null;
break;
} else {
// Must be in server mode and we finished parsing a single BUCK file.
return currentObjects;
}
case FIELD_NAME:
currentFieldName = parser.getText();
break;
case VALUE_STRING:
if (currentArray == null) {
currentObject.put(currentFieldName, parser.getText());
currentFieldName = null;
} else {
currentArray.add(parser.getText());
}
break;
case VALUE_TRUE:
case VALUE_FALSE:
Preconditions.checkState(currentArray == null, "Unexpected boolean in JSON array");
currentObject.put(currentFieldName, token == JsonToken.VALUE_TRUE);
currentFieldName = null;
break;
case VALUE_NUMBER_INT:
Preconditions.checkState(currentArray == null, "Unexpected int in JSON array");
currentObject.put(currentFieldName, parser.getLongValue());
currentFieldName = null;
break;
case VALUE_NULL:
if (currentArray == null) {
currentObject.put(currentFieldName, null);
currentFieldName = null;
} else {
currentArray.add(null);
}
break;
default:
throw new JsonParseException("Unexpected token: " + token, parser.getCurrentLocation());
}
}
}
}