/* * Copyright (C) 2015 The Android Open Source Project * * 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.android.ide.common.blame; import com.google.common.collect.BiMap; import com.google.common.collect.EnumHashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.File; import java.io.IOException; /** * Class to handle json serialization and deserialization of messages. * * Reads json objects of the form: * * <pre> * { * "kind":"ERROR", * "text":"errorText", * "original":"unparsed error text: Error in some intermediate file", * "sources": [{ * "file":"/path/to/source.java", * "position":{ * "startLine":1, * "startColumn":2, * "startOffset":3, * "endLine":4, * "endColumn":5, * "endOffset":6 * } * }] * }</pre> * * All fields, other than text, may be omitted. They have the following defaults: * * <table> * <tr><th>Property</th> <th>Default</th> <th>Notes</th></tr> * <tr><td>kind (ERROR, WARNING, * INFO, UNKNOWN)</td> <td>UNKNOWN</td> <td></td></tr> * <tr><td>text</td> <td><i>Empty String</i></td> <td>Should not be omitted.</td></tr> * <tr><td>file (Absolute)</td> <td>{} <i>[unknown]</i></td> <td>See {@link SourceFileJsonTypeAdapter}</td></tr> * <tr><td>position</td> <td>UNKNOWN</td> <td></td></tr> * <tr><td>startLine, * startColumn, * startOffset</td> <td>-1 <i>[unknown]</i></td> <td rowspan="2">0-based</td></tr> * <tr><td>endLine, * endColumn, * endOffset</td> <td>startLine, startColumn, * startOffset</td></tr> * </table> * * <h4>Notes</h4> * <ul> * <li>Offset need not be included, if needed by the consumer of the message it can * be derived from the file, line and column.</li> * <li>If line is included and column is not the message will be considered to apply * to the whole line.</li> * <li>A message can have multiple sources.</li> * </ul> * It also can read legacy serialized objects of the form: * * <pre>{ * "kind":"ERROR", * "text":"errorText", * "sourcePath": "/path/to/source.java", * "position":{ * "startLine":1, * "startColumn":2, * "startOffset":3, * "endLine":4, * "endColumn":5, * "endOffset":6 * } * }</pre> * * These serializers are implemented using the lower-level TypeAdapter gson API which gives much * more control and allow changes to be made without breaking backward compatibility. */ public class MessageJsonSerializer extends TypeAdapter<Message> { private static final String KIND = "kind"; private static final String TEXT = "text"; private static final String SOURCE_FILE_POSITIONS = "sources"; private static final String RAW_MESSAGE = "original"; private static final String LEGACY_SOURCE_PATH = "sourcePath"; private static final String LEGACY_POSITION = "position"; private static final BiMap<Message.Kind, String> KIND_STRING_ENUM_MAP; static { EnumHashBiMap<Message.Kind, String> map = EnumHashBiMap.create(Message.Kind.class); map.put(Message.Kind.ERROR, "error"); map.put(Message.Kind.WARNING, "warning"); map.put(Message.Kind.INFO, "info"); map.put(Message.Kind.STATISTICS, "statistics"); map.put(Message.Kind.UNKNOWN, "unknown"); map.put(Message.Kind.SIMPLE, "simple"); KIND_STRING_ENUM_MAP = Maps.unmodifiableBiMap(map); } private final SourceFilePositionJsonSerializer mSourceFilePositionTypeAdapter; private final SourcePositionJsonTypeAdapter mSourcePositionTypeAdapter; public MessageJsonSerializer() { mSourceFilePositionTypeAdapter = new SourceFilePositionJsonSerializer(); mSourcePositionTypeAdapter = mSourceFilePositionTypeAdapter.getSourcePositionTypeAdapter(); } @Override public void write(JsonWriter out, Message message) throws IOException { out.beginObject() .name(KIND).value(KIND_STRING_ENUM_MAP.get(message.getKind())) .name(TEXT).value(message.getText()) .name(SOURCE_FILE_POSITIONS).beginArray(); for (SourceFilePosition position : message.getSourceFilePositions()) { mSourceFilePositionTypeAdapter.write(out, position); } out.endArray(); if (!message.getRawMessage().equals(message.getText())) { out.name(RAW_MESSAGE).value(message.getRawMessage()); } out.endObject(); } @Override public Message read(JsonReader in) throws IOException { in.beginObject(); Message.Kind kind = Message.Kind.UNKNOWN; String text = ""; String rawMessage = null; ImmutableList.Builder<SourceFilePosition> positions = new ImmutableList.Builder<SourceFilePosition>(); SourceFile legacyFile = SourceFile.UNKNOWN; SourcePosition legacyPosition = SourcePosition.UNKNOWN; while (in.hasNext()) { String name = in.nextName(); if (name.equals(KIND)) { //noinspection StringToUpperCaseOrToLowerCaseWithoutLocale Message.Kind theKind = KIND_STRING_ENUM_MAP.inverse() .get(in.nextString().toLowerCase()); kind = (theKind != null) ? theKind : Message.Kind.UNKNOWN; } else if (name.equals(TEXT)) { text = in.nextString(); } else if (name.equals(RAW_MESSAGE)) { rawMessage = in.nextString(); } else if (name.equals(SOURCE_FILE_POSITIONS)) { switch (in.peek()) { case BEGIN_ARRAY: in.beginArray(); while(in.hasNext()) { positions.add(mSourceFilePositionTypeAdapter.read(in)); } in.endArray(); break; case BEGIN_OBJECT: positions.add(mSourceFilePositionTypeAdapter.read(in)); break; default: in.skipValue(); break; } } else if (name.equals(LEGACY_SOURCE_PATH)) { legacyFile = new SourceFile(new File(in.nextString())); } else if (name.equals(LEGACY_POSITION)) { legacyPosition = mSourcePositionTypeAdapter.read(in); } else { in.skipValue(); } } in.endObject(); if (legacyFile != SourceFile.UNKNOWN || legacyPosition != SourcePosition.UNKNOWN) { positions.add(new SourceFilePosition(legacyFile, legacyPosition)); } if (rawMessage == null) { rawMessage = text; } ImmutableList<SourceFilePosition> sourceFilePositions = positions.build(); if (!sourceFilePositions.isEmpty()) { return new Message(kind, text, rawMessage, sourceFilePositions); } else { return new Message(kind, text, rawMessage, ImmutableList.of(SourceFilePosition.UNKNOWN)); } } public static void registerTypeAdapters(GsonBuilder builder) { builder.registerTypeAdapter(SourceFile.class, new SourceFileJsonTypeAdapter()); builder.registerTypeAdapter(SourcePosition.class, new SourcePositionJsonTypeAdapter()); builder.registerTypeAdapter(SourceFilePosition.class, new SourceFilePositionJsonSerializer()); builder.registerTypeAdapter(Message.class, new MessageJsonSerializer()); } }