LCOV - code coverage report
Current view: top level - server/notedb - ChangeNoteJson.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 68 69 98.6 %
Date: 2022-11-19 15:00:39 Functions: 21 21 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750