| /* |
| * 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.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.gson.Gson; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import com.google.gson.JsonPrimitive; |
| import com.google.gson.stream.JsonReader; |
| import com.google.gson.stream.JsonToken; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.StringReader; |
| 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 implements AutoCloseable { |
| |
| private final Gson gson; |
| private final JsonReader reader; |
| |
| /** |
| * 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. |
| */ |
| private final boolean isServerMode; |
| |
| /** |
| * @param jsonReader That contains the JSON data. |
| */ |
| public BuildFileToJsonParser(Reader jsonReader, boolean isServerMode) { |
| this.gson = new Gson(); |
| this.reader = new JsonReader(jsonReader); |
| this.isServerMode = isServerMode; |
| |
| // This is used to read one line at a time. |
| reader.setLenient(true); |
| } |
| |
| @VisibleForTesting |
| public BuildFileToJsonParser(String json, boolean isServerMode) { |
| this(new StringReader(json), isServerMode); |
| } |
| |
| /** |
| * 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 The parsed JSON, represented as Java collections. Ideally, we would use Gson's object |
| * model directly to avoid the overhead of converting between object models. That would |
| * require updating all code that depends on this method, which may be a lot of work. Also, |
| * bear in mind that using the Java collections decouples clients of this method from the JSON |
| * parser that we use. |
| */ |
| @SuppressWarnings("unchecked") |
| List<Map<String, Object>> nextRules() throws IOException { |
| List<Map<String, Object>> items = Lists.newArrayList(); |
| |
| if (isServerMode) { |
| reader.beginArray(); |
| |
| while (reader.hasNext()) { |
| JsonObject json = gson.fromJson(reader, JsonObject.class); |
| items.add((Map<String, Object>) toRawTypes(json)); |
| } |
| |
| reader.endArray(); |
| } else { |
| while (reader.peek() != JsonToken.END_DOCUMENT) { |
| JsonObject json = gson.fromJson(reader, JsonObject.class); |
| items.add((Map<String, Object>) toRawTypes(json)); |
| } |
| } |
| |
| return items; |
| } |
| |
| /** |
| * @return One of: String, Boolean, Long, Number, List<Object>, Map<String, Object>. |
| */ |
| @VisibleForTesting |
| static Object toRawTypes(JsonElement json) { |
| // Cases are ordered from most common to least common. |
| if (json.isJsonPrimitive()) { |
| JsonPrimitive primitive = json.getAsJsonPrimitive(); |
| if (primitive.isString()) { |
| // On a large project, without invoking intern(), we have seen `buck targets` OOM. When this |
| // happened, according to the .hprof file generated using -XX:+HeapDumpOnOutOfMemoryError, |
| // 39.6% of the memory was spent on char[] objects while 14.5% was spent on Strings. |
| // (Another 10.5% was spent on java.util.HashMap$Entry.) Introducing intern() stopped the |
| // OOM from happening. |
| return primitive.getAsString().intern(); |
| } else if (primitive.isBoolean()) { |
| return primitive.getAsBoolean(); |
| } else if (primitive.isNumber()) { |
| Number number = primitive.getAsNumber(); |
| // Number is likely an instance of class com.google.gson.internal.LazilyParsedNumber. |
| if (number.longValue() == number.doubleValue()) { |
| return number.longValue(); |
| } else { |
| return number; |
| } |
| } else { |
| throw new IllegalStateException("Unknown primitive type: " + primitive); |
| } |
| } else if (json.isJsonArray()) { |
| JsonArray array = json.getAsJsonArray(); |
| List<Object> out = Lists.newArrayListWithCapacity(array.size()); |
| for (JsonElement item : array) { |
| out.add(toRawTypes(item)); |
| } |
| return out; |
| } else if (json.isJsonObject()) { |
| Map<String, Object> out = Maps.newHashMap(); |
| for (Map.Entry<String, JsonElement> entry : json.getAsJsonObject().entrySet()) { |
| out.put(entry.getKey(), toRawTypes(entry.getValue())); |
| } |
| return out; |
| } else if (json.isJsonNull()) { |
| return null; |
| } else { |
| throw new IllegalStateException("Unknown type: " + json); |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| reader.close(); |
| } |
| } |