Line data Source code
1 : // Copyright (C) 2018 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.notedb; 16 : 17 : import com.google.common.collect.ImmutableList; 18 : import com.google.gerrit.entities.EntitiesAdapterFactory; 19 : import com.google.gerrit.entities.SubmitRequirementExpressionResult; 20 : import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status; 21 : import com.google.gerrit.json.EnumTypeAdapterFactory; 22 : import com.google.gerrit.json.OptionalSubmitRequirementExpressionResultAdapterFactory; 23 : import com.google.gerrit.json.OptionalTypeAdapter; 24 : import com.google.gson.Gson; 25 : import com.google.gson.GsonBuilder; 26 : import com.google.gson.JsonElement; 27 : import com.google.gson.JsonObject; 28 : import com.google.gson.JsonParser; 29 : import com.google.gson.TypeAdapter; 30 : import com.google.gson.stream.JsonReader; 31 : import com.google.gson.stream.JsonWriter; 32 : import com.google.inject.Singleton; 33 : import com.google.inject.TypeLiteral; 34 : import java.io.IOException; 35 : import java.sql.Timestamp; 36 : import java.util.Arrays; 37 : import java.util.List; 38 : import java.util.Optional; 39 : import org.eclipse.jgit.lib.ObjectId; 40 : 41 : /** 42 : * Provides {@link Gson} to parse {@link ChangeRevisionNote}, attached to the change update. 43 : * 44 : * <p>Apart from the adapters for the custom JSON format, this class also registers adapters that 45 : * support forward/backward compatibility when modifying {@link ChangeNotes} storage format. 46 : * 47 : * <p>NOTE: All changes to the storage format must be both forward and backward compatible, see 48 : * comment on {@link ChangeNotesParser}. 49 : * 50 : * <p>For JSON, such changes include e.g. modifications to the serialized {@code AutoValue} classes. 51 : */ 52 : @Singleton 53 153 : public class ChangeNoteJson { 54 153 : private final Gson gson = newGson(); 55 : 56 : static Gson newGson() { 57 153 : return new GsonBuilder() 58 153 : .registerTypeAdapter(Optional.class, new OptionalTypeAdapter()) 59 153 : .registerTypeAdapter(Timestamp.class, new CommentTimestampAdapter().nullSafe()) 60 153 : .registerTypeAdapterFactory(new EnumTypeAdapterFactory()) 61 153 : .registerTypeAdapterFactory(EntitiesAdapterFactory.create()) 62 153 : .registerTypeAdapter( 63 153 : new TypeLiteral<ImmutableList<String>>() {}.getType(), 64 153 : new ImmutableListAdapter().nullSafe()) 65 153 : .registerTypeAdapter( 66 153 : new TypeLiteral<Optional<Boolean>>() {}.getType(), 67 153 : new OptionalBooleanAdapter().nullSafe()) 68 153 : .registerTypeAdapterFactory(new OptionalSubmitRequirementExpressionResultAdapterFactory()) 69 153 : .registerTypeAdapter(ObjectId.class, new ObjectIdAdapter()) 70 153 : .registerTypeAdapter( 71 : SubmitRequirementExpressionResult.Status.class, 72 : new SubmitRequirementExpressionResultStatusAdapter()) 73 153 : .setPrettyPrinting() 74 153 : .create(); 75 : } 76 : 77 : public Gson getGson() { 78 65 : return gson; 79 : } 80 : 81 153 : static class OptionalBooleanAdapter extends TypeAdapter<Optional<Boolean>> { 82 : @Override 83 : public void write(JsonWriter out, Optional<Boolean> value) throws IOException { 84 : // Serialize the field using the same format used by the AutoValue's default Gson serializer. 85 3 : out.beginObject(); 86 3 : out.name("value"); 87 3 : if (value.isPresent()) { 88 3 : out.value(value.get()); 89 : } else { 90 3 : out.nullValue(); 91 : } 92 3 : out.endObject(); 93 3 : } 94 : 95 : @Override 96 : public Optional<Boolean> read(JsonReader in) throws IOException { 97 3 : JsonElement parsed = JsonParser.parseReader(in); 98 3 : if (parsed == null) { 99 0 : return Optional.empty(); 100 : } 101 3 : if (parsed.isJsonObject()) { 102 : // If it's not a JSON object, then the boolean value is available directly in the Json 103 : // element. 104 3 : parsed = parsed.getAsJsonObject().get("value"); 105 : } 106 3 : if (parsed == null || parsed.isJsonNull()) { 107 3 : return Optional.empty(); 108 : } 109 3 : return Optional.of(parsed.getAsBoolean()); 110 : } 111 : } 112 : 113 : /** Json serializer for the {@link ObjectId} class. */ 114 153 : static class ObjectIdAdapter extends TypeAdapter<ObjectId> { 115 153 : private static final List<String> legacyFields = Arrays.asList("w1", "w2", "w3", "w4", "w5"); 116 : 117 : @Override 118 : public void write(JsonWriter out, ObjectId value) throws IOException { 119 3 : out.value(value.name()); 120 3 : } 121 : 122 : @Override 123 : public ObjectId read(JsonReader in) throws IOException { 124 3 : JsonElement parsed = JsonParser.parseReader(in); 125 3 : if (parsed.isJsonObject() && isJGitFormat(parsed)) { 126 : // Some object IDs may have been serialized using the JGit format using the five integers 127 : // w1, w2, w3, w4, w5. Detect this case so that we can deserialize properly. 128 1 : int[] raw = 129 1 : legacyFields.stream() 130 1 : .mapToInt(field -> parsed.getAsJsonObject().get(field).getAsInt()) 131 1 : .toArray(); 132 1 : return ObjectId.fromRaw(raw); 133 : } 134 3 : return ObjectId.fromString(parsed.getAsString()); 135 : } 136 : 137 : /** Return true if the json element contains the JGit serialized format of the Object ID. */ 138 : private boolean isJGitFormat(JsonElement elem) { 139 1 : JsonObject asObj = elem.getAsJsonObject(); 140 1 : return legacyFields.stream().allMatch(field -> asObj.has(field)); 141 : } 142 : } 143 : 144 153 : static class ImmutableListAdapter extends TypeAdapter<ImmutableList<String>> { 145 : 146 : @Override 147 : public void write(JsonWriter out, ImmutableList<String> value) throws IOException { 148 3 : out.beginArray(); 149 3 : for (String v : value) { 150 3 : out.value(v); 151 3 : } 152 3 : out.endArray(); 153 3 : } 154 : 155 : @Override 156 : public ImmutableList<String> read(JsonReader in) throws IOException { 157 3 : ImmutableList.Builder<String> builder = ImmutableList.builder(); 158 3 : in.beginArray(); 159 3 : while (in.hasNext()) { 160 3 : builder.add(in.nextString()); 161 : } 162 3 : in.endArray(); 163 3 : return builder.build(); 164 : } 165 : } 166 : 167 : /** 168 : * A Json type adapter for the Status enum in {@link SubmitRequirementExpressionResult}. This 169 : * adapter is able to parse unrecognized values. Unrecognized values are converted to the value 170 : * "ERROR" The adapter is needed to ensure forward compatibility since we want to add more values 171 : * to this enum. We do that to ensure safer rollout in distributed setups where some tasks are 172 : * updated before others. We make sure that tasks running the old binaries are still able to parse 173 : * values written by tasks running the new binaries. 174 : * 175 : * <p>TODO(ghareeb): Remove this adapter. 176 : */ 177 153 : static class SubmitRequirementExpressionResultStatusAdapter 178 : extends TypeAdapter<SubmitRequirementExpressionResult.Status> { 179 : @Override 180 : public void write(JsonWriter jsonWriter, Status status) throws IOException { 181 3 : jsonWriter.value(status.name()); 182 3 : } 183 : 184 : @Override 185 : public Status read(JsonReader jsonReader) throws IOException { 186 3 : String val = jsonReader.nextString(); 187 : try { 188 3 : return SubmitRequirementExpressionResult.Status.valueOf(val); 189 1 : } catch (IllegalArgumentException e) { 190 1 : return SubmitRequirementExpressionResult.Status.ERROR; 191 : } 192 : } 193 : } 194 : }