LabelTypes: Return an Optional to make clear that value can be absent

When asking LabelTypes for a LabelType by name or ID, that LabelType
is only returned if the project has a label with name or ID
configured.

Most places in Gerrit correctly handled this implicit behavior, but
new code for SubmitRequirements (ChangeField:getMaxMinAnyLabels) did
not which made it fail with a NullPointerException in case a label
that was voted on the change was removed from project.config
since the vote was granted.

In this commit, we fix the NPE by reworking LabelTypes to return
an Optional which also makes it so that this programmer error
can't occur in the future.

Change-Id: I9e0f3a940ce21ef1e46b3dbb1675267ae1d76459
diff --git a/java/com/google/gerrit/entities/LabelTypes.java b/java/com/google/gerrit/entities/LabelTypes.java
index 1c38c59..55a9976 100644
--- a/java/com/google/gerrit/entities/LabelTypes.java
+++ b/java/com/google/gerrit/entities/LabelTypes.java
@@ -20,6 +20,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public class LabelTypes {
   protected List<LabelType> labelTypes;
@@ -36,12 +37,12 @@
     return labelTypes;
   }
 
-  public LabelType byLabel(LabelId labelId) {
-    return byLabel().get(labelId.get().toLowerCase());
+  public Optional<LabelType> byLabel(LabelId labelId) {
+    return Optional.ofNullable(byLabel().get(labelId.get().toLowerCase()));
   }
 
-  public LabelType byLabel(String labelName) {
-    return byLabel().get(labelName.toLowerCase());
+  public Optional<LabelType> byLabel(String labelName) {
+    return Optional.ofNullable(byLabel().get(labelName.toLowerCase()));
   }
 
   private Map<String, LabelType> byLabel() {
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index d60bc8f..326ddf4 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -151,8 +151,10 @@
 
     ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
     for (PatchSetApproval ap : approvalsUtil.byPatchSet(notes, change.currentPatchSetId())) {
-      LabelType type = projectState.getLabelTypes(notes).byLabel(ap.label());
-      if (type != null && ap.value() == 1 && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
+      Optional<LabelType> type = projectState.getLabelTypes(notes).byLabel(ap.label());
+      if (type.isPresent()
+          && ap.value() == 1
+          && type.get().getFunction() == LabelFunction.PATCH_SET_LOCK) {
         return true;
       }
     }
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 1efbd37..8d409e5 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -52,6 +52,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -397,14 +398,14 @@
       if (resultByUser.contains(psa.label(), psa.accountId())) {
         continue;
       }
-      LabelType type = labelTypes.byLabel(psa.labelId());
+      Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
       // Only compute modified files if there is a relevant label, since this is expensive.
       if (modifiedFiles == null
-          && type != null
-          && type.isCopyAllScoresIfListOfFilesDidNotChange()) {
+          && type.isPresent()
+          && type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
         modifiedFiles = listModifiedFiles(project, ps, priorPatchSet);
       }
-      if (type == null) {
+      if (!type.isPresent()) {
         logger.atFine().log(
             "approval %d on label %s of patch set %d of change %d cannot be copied"
                 + " to patch set %d because the label no longer exists on project %s",
@@ -416,8 +417,8 @@
             project.getName());
         continue;
       }
-      if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type, modifiedFiles)
-          && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type, kind)) {
+      if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type.get(), modifiedFiles)
+          && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type.get(), kind)) {
         continue;
       }
       resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index b1e85e9..a1cdd99 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -61,6 +61,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -299,8 +300,12 @@
     List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
     Date ts = update.getWhen();
     for (Map.Entry<String, Short> vote : approvals.entrySet()) {
-      LabelType lt = labelTypes.byLabel(vote.getKey());
-      cells.add(newApproval(ps.id(), user, lt.getLabelId(), vote.getValue(), ts).build());
+      Optional<LabelType> lt = labelTypes.byLabel(vote.getKey());
+      if (!lt.isPresent()) {
+        throw new BadRequestException(
+            String.format("label \"%s\" is not a configured label", vote.getKey()));
+      }
+      cells.add(newApproval(ps.id(), user, lt.get().getLabelId(), vote.getValue(), ts).build());
     }
     for (PatchSetApproval psa : cells) {
       update.putApproval(psa.label(), psa.value());
@@ -310,11 +315,11 @@
 
   public static void checkLabel(LabelTypes labelTypes, String name, Short value)
       throws BadRequestException {
-    LabelType label = labelTypes.byLabel(name);
-    if (label == null) {
+    Optional<LabelType> label = labelTypes.byLabel(name);
+    if (!label.isPresent()) {
       throw new BadRequestException(String.format("label \"%s\" is not a configured label", name));
     }
-    if (label.getValue(value) == null) {
+    if (label.get().getValue(value) == null) {
       throw new BadRequestException(
           String.format("label \"%s\": %d is not a valid value", name, value));
     }
diff --git a/java/com/google/gerrit/server/change/LabelNormalizer.java b/java/com/google/gerrit/server/change/LabelNormalizer.java
index 30343d4..b5527d7 100644
--- a/java/com/google/gerrit/server/change/LabelNormalizer.java
+++ b/java/com/google/gerrit/server/change/LabelNormalizer.java
@@ -33,6 +33,7 @@
 import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Normalizes votes on labels according to project config.
@@ -101,12 +102,12 @@
         unchanged.add(psa);
         continue;
       }
-      LabelType label = labelTypes.byLabel(psa.labelId());
-      if (label == null) {
+      Optional<LabelType> label = labelTypes.byLabel(psa.labelId());
+      if (!label.isPresent()) {
         deleted.add(psa);
         continue;
       }
-      PatchSetApproval copy = applyTypeFloor(label, psa);
+      PatchSetApproval copy = applyTypeFloor(label.get(), psa);
       if (copy.value() != psa.value()) {
         updated.add(copy);
       } else {
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index acff03c..5ce121b 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -57,6 +57,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -103,9 +104,9 @@
     for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels != null) {
         for (SubmitRecord.Label r : rec.labels) {
-          LabelType type = labelTypes.byLabel(r.label);
-          if (type != null && (!isMerged || type.isAllowPostSubmit())) {
-            toCheck.put(type.getName(), type);
+          Optional<LabelType> type = labelTypes.byLabel(r.label);
+          if (type.isPresent() && (!isMerged || type.get().isAllowPostSubmit())) {
+            toCheck.put(type.get().getName(), type.get());
           }
         }
       }
@@ -120,18 +121,18 @@
         continue;
       }
       for (SubmitRecord.Label r : rec.labels) {
-        LabelType type = labelTypes.byLabel(r.label);
-        if (type == null || (isMerged && !type.isAllowPostSubmit())) {
+        Optional<LabelType> type = labelTypes.byLabel(r.label);
+        if (!type.isPresent() || (isMerged && !type.get().isAllowPostSubmit())) {
           continue;
         }
 
-        for (LabelValue v : type.getValues()) {
-          boolean ok = can.contains(new LabelPermission.WithValue(type, v));
+        for (LabelValue v : type.get().getValues()) {
+          boolean ok = can.contains(new LabelPermission.WithValue(type.get(), v));
           if (isMerged) {
             if (labels == null) {
               labels = currentLabels(filterApprovalsBy, cd);
             }
-            short prev = labels.getOrDefault(type.getName(), (short) 0);
+            short prev = labels.getOrDefault(type.get().getName(), (short) 0);
             ok &= v.getValue() >= prev;
           }
           if (ok) {
@@ -176,21 +177,21 @@
       setAllApprovals(accountLoader, cd, labels);
     }
     for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
-      LabelType type = labelTypes.byLabel(e.getKey());
-      if (type == null) {
+      Optional<LabelType> type = labelTypes.byLabel(e.getKey());
+      if (!type.isPresent()) {
         continue;
       }
       if (standard) {
         for (PatchSetApproval psa : cd.currentApprovals()) {
-          if (type.matches(psa)) {
+          if (type.get().matches(psa)) {
             short val = psa.value();
             Account.Id accountId = psa.accountId();
-            setLabelScores(accountLoader, type, e.getValue(), val, accountId);
+            setLabelScores(accountLoader, type.get(), e.getValue(), val, accountId);
           }
         }
       }
       if (detailed) {
-        setLabelValues(type, e.getValue());
+        setLabelValues(type.get(), e.getValue());
       }
     }
     return labels;
@@ -261,9 +262,9 @@
         MultimapBuilder.hashKeys().hashSetValues().build();
     for (PatchSetApproval a : cd.currentApprovals()) {
       allUsers.add(a.accountId());
-      LabelType type = labelTypes.byLabel(a.labelId());
-      if (type != null) {
-        labelNames.add(type.getName());
+      Optional<LabelType> type = labelTypes.byLabel(a.labelId());
+      if (type.isPresent()) {
+        labelNames.add(type.get().getName());
         // Not worth the effort to distinguish between votable/non-votable for 0
         // values on closed changes, since they can't vote anyway.
         current.put(a.accountId(), a);
@@ -292,8 +293,8 @@
 
     if (detailed) {
       labels.entrySet().stream()
-          .filter(e -> labelTypes.byLabel(e.getKey()) != null)
-          .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue()));
+          .filter(e -> labelTypes.byLabel(e.getKey()).isPresent())
+          .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()).get(), e.getValue()));
     }
 
     for (Account.Id accountId : allUsers) {
@@ -308,16 +309,16 @@
         }
       }
       for (PatchSetApproval psa : current.get(accountId)) {
-        LabelType type = labelTypes.byLabel(psa.labelId());
-        if (type == null) {
+        Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
+        if (!type.isPresent()) {
           continue;
         }
 
         short val = psa.value();
-        ApprovalInfo info = byLabel.get(type.getName());
+        ApprovalInfo info = byLabel.get(type.get().getName());
         if (info != null) {
           info.value = Integer.valueOf(val);
-          info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
+          info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
           info.date = psa.granted();
           info.tag = psa.tag().orElse(null);
           if (psa.postSubmit()) {
@@ -328,7 +329,7 @@
           continue;
         }
 
-        setLabelScores(accountLoader, type, labels.get(type.getName()), val, accountId);
+        setLabelScores(accountLoader, type.get(), labels.get(type.get().getName()), val, accountId);
       }
     }
     return labels;
@@ -428,24 +429,24 @@
       PermissionBackend.ForChange perm = permissionBackend.absentUser(accountId).change(cd);
       Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
-        LabelType lt = labelTypes.byLabel(e.getKey());
-        if (lt == null) {
+        Optional<LabelType> lt = labelTypes.byLabel(e.getKey());
+        if (!lt.isPresent()) {
           // Ignore submit record for undefined label; likely the submit rule
           // author didn't intend for the label to show up in the table.
           continue;
         }
         Integer value;
-        VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.getName(), null);
+        VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.get().getName(), null);
         String tag = null;
         Timestamp date = null;
-        PatchSetApproval psa = current.get(accountId, lt.getName());
+        PatchSetApproval psa = current.get(accountId, lt.get().getName());
         if (psa != null) {
           value = Integer.valueOf(psa.value());
           if (value == 0) {
             // This may be a dummy approval that was inserted when the reviewer
             // was added. Explicitly check whether the user can vote on this
             // label.
-            value = perm.test(new LabelPermission(lt)) ? 0 : null;
+            value = perm.test(new LabelPermission(lt.get())) ? 0 : null;
           }
           tag = psa.tag().orElse(null);
           date = psa.granted();
@@ -456,7 +457,7 @@
           // Either the user cannot vote on this label, or they were added as a
           // reviewer but have not responded yet. Explicitly check whether the
           // user can vote on this label.
-          value = perm.test(new LabelPermission(lt)) ? 0 : null;
+          value = perm.test(new LabelPermission(lt.get())) ? 0 : null;
         }
         addApproval(
             e.getValue().label(),
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index d5b74a8..6189708 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -38,6 +38,7 @@
 import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.TreeMap;
 
 @Singleton
@@ -107,10 +108,8 @@
 
     out.approvals = new TreeMap<>(labelTypes.nameComparator());
     for (PatchSetApproval ca : approvals) {
-      LabelType at = labelTypes.byLabel(ca.labelId());
-      if (at != null) {
-        out.approvals.put(at.getName(), formatValue(ca.value()));
-      }
+      Optional<LabelType> at = labelTypes.byLabel(ca.labelId());
+      at.ifPresent(lt -> out.approvals.put(lt.getName(), formatValue(ca.value())));
     }
 
     // Add dummy approvals for all permitted labels for the user even if they
@@ -125,13 +124,13 @@
         }
         for (SubmitRecord.Label label : rec.labels) {
           String name = label.label;
-          LabelType type = labelTypes.byLabel(name);
-          if (out.approvals.containsKey(name) || type == null) {
+          Optional<LabelType> type = labelTypes.byLabel(name);
+          if (out.approvals.containsKey(name) || !type.isPresent()) {
             continue;
           }
 
           try {
-            perm.check(new LabelPermission(type));
+            perm.check(new LabelPermission(type.get()));
             out.approvals.put(name, formatValue((short) 0));
           } catch (AuthException e) {
             // Do nothing.
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 3f988a3..1bb694a 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -71,6 +71,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -535,10 +536,8 @@
     a.grantedOn = approval.granted().getTime() / 1000L;
     a.oldValue = null;
 
-    LabelType lt = labelTypes.byLabel(approval.labelId());
-    if (lt != null) {
-      a.description = lt.getName();
-    }
+    Optional<LabelType> lt = labelTypes.byLabel(approval.labelId());
+    lt.ifPresent(l -> a.description = l.getName());
     return a;
   }
 
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 439f53e..edd1928 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
@@ -202,10 +201,7 @@
         a.oldValue = Short.toString(oldApprovals.get(approval.getKey()));
       }
     }
-    LabelType lt = labelTypes.byLabel(approval.getKey());
-    if (lt != null) {
-      a.description = lt.getName();
-    }
+    labelTypes.byLabel(approval.getKey()).ifPresent(lt -> a.description = lt.getName());
     if (approval.getValue() != null) {
       a.value = Short.toString(approval.getValue());
     }
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 1da14f8..a5ea24d 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -600,11 +600,11 @@
       } else if (isVerified(a.labelId())) {
         tag = "Tested-by";
       } else {
-        final LabelType lt = project.getLabelTypes().byLabel(a.labelId());
-        if (lt == null) {
+        final Optional<LabelType> lt = project.getLabelTypes().byLabel(a.labelId());
+        if (!lt.isPresent()) {
           continue;
         }
-        tag = lt.getName();
+        tag = lt.get().getName();
       }
 
       if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index f00b48eb..b2a31b9 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -465,10 +465,10 @@
           continue;
         }
 
-        LabelType lt = projectState.getLabelTypes().byLabel(a.labelId());
-        if (lt != null) {
-          current.put(lt.getName(), a);
-        }
+        projectState
+            .getLabelTypes()
+            .byLabel(a.labelId())
+            .ifPresent(l -> current.put(l.getName(), a));
       }
     }
     return current;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 810cd4d..1ee12fe 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -605,7 +605,7 @@
     for (PatchSetApproval a : cd.currentApprovals()) {
       if (a.value() != 0 && !a.isLegacySubmit()) {
         allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
-        LabelType labelType = cd.getLabelTypes().byLabel(a.labelId());
+        Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
         allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
         if (owners && cd.change().getOwner().equals(a.accountId())) {
           allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
@@ -622,13 +622,15 @@
   }
 
   private static List<String> getMaxMinAnyLabels(
-      String label, short labelVal, LabelType labelType, @Nullable Account.Id accountId) {
+      String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
     List<String> labels = new ArrayList<>();
-    if (labelVal == labelType.getMaxPositive()) {
-      labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
-    }
-    if (labelVal == labelType.getMaxNegative()) {
-      labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+    if (labelType.isPresent()) {
+      if (labelVal == labelType.get().getMaxPositive()) {
+        labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
+      }
+      if (labelVal == labelType.get().getMaxNegative()) {
+        labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+      }
     }
     labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId));
     return labels;
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index 6af2345..56528df 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -79,14 +79,14 @@
       Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
       Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca : args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.id())) {
-        LabelType lt = labelTypes.byLabel(ca.labelId());
-        if (lt == null) {
+        Optional<LabelType> lt = labelTypes.byLabel(ca.labelId());
+        if (!lt.isPresent()) {
           continue;
         }
         if (ca.value() > 0) {
-          pos.put(ca.accountId(), lt.getName(), ca);
+          pos.put(ca.accountId(), lt.get().getName(), ca);
         } else if (ca.value() < 0) {
-          neg.put(ca.accountId(), lt.getName(), ca);
+          neg.put(ca.accountId(), lt.get().getName(), ca);
         }
       }
 
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index e33b261..62cfa47 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.entities.PatchSet;
@@ -43,6 +44,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.Config;
@@ -242,10 +244,9 @@
       if (!patchSetApproval.label().equals(LabelId.CODE_REVIEW)) {
         continue;
       }
-      if (!projectState
-          .getLabelTypes(notes)
-          .byLabel(patchSetApproval.labelId())
-          .isMaxPositive(patchSetApproval)) {
+      Optional<LabelType> lt =
+          projectState.getLabelTypes(notes).byLabel(patchSetApproval.labelId());
+      if (!lt.isPresent() || !lt.get().isMaxPositive(patchSetApproval)) {
         continue;
       }
       if (patchSetApproval.patchSetId().get() > maxPatchSetId.get()) {
diff --git a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
index 2924e6e..326620d 100644
--- a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
+++ b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
@@ -23,6 +23,7 @@
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
 import java.util.Objects;
+import java.util.Optional;
 
 /** Predicate that matches patch set approvals we want to copy based on the value. */
 public class MagicValuePredicate extends ApprovalPredicate {
@@ -47,19 +48,23 @@
 
   @Override
   public boolean match(ApprovalContext ctx) {
+    Optional<LabelType> lt =
+        getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId());
     short pValue;
     switch (value) {
       case ANY:
         return true;
       case MIN:
-        pValue =
-            getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
-                .getMaxNegative();
+        if (!lt.isPresent()) {
+          return false;
+        }
+        pValue = lt.get().getMaxNegative();
         break;
       case MAX:
-        pValue =
-            getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
-                .getMaxPositive();
+        if (!lt.isPresent()) {
+          return false;
+        }
+        pValue = lt.get().getMaxPositive();
         break;
       default:
         throw new IllegalArgumentException("unrecognized label value: " + value);
@@ -67,7 +72,7 @@
     return pValue == ctx.patchSetApproval().value();
   }
 
-  private LabelType getLabelType(Project.NameKey project, LabelId labelId) {
+  private Optional<LabelType> getLabelType(Project.NameKey project, LabelId labelId) {
     return projectCache
         .get(project)
         .orElseThrow(() -> new IllegalStateException(project + " absent"))
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 30d5e2f..ade615c 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -91,8 +91,8 @@
   }
 
   protected static LabelType type(LabelTypes types, String toFind) {
-    if (types.byLabel(toFind) != null) {
-      return types.byLabel(toFind);
+    if (types.byLabel(toFind).isPresent()) {
+      return types.byLabel(toFind).get();
     }
 
     for (LabelType lt : types.getLabelTypes()) {
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index e3c58e47..2c56322 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -87,8 +87,8 @@
   }
 
   protected static LabelType type(LabelTypes types, String toFind) {
-    if (types.byLabel(toFind) != null) {
-      return types.byLabel(toFind);
+    if (types.byLabel(toFind).isPresent()) {
+      return types.byLabel(toFind).get();
     }
 
     for (LabelType lt : types.getLabelTypes()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 84424a8..2c358d0 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -194,7 +194,7 @@
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
               ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
-        if (labelTypes.byLabel(a.labelId()) == null) {
+        if (!labelTypes.byLabel(a.labelId()).isPresent()) {
           continue; // Ignore undefined labels.
         } else if (!a.label().equals(label)) {
           // Populate map for non-matching labels, needed by VoteDeleted.
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 9263971..8c21841 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -64,6 +64,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -265,11 +266,13 @@
           approvalsUtil.byPatchSet(
               ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
         ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-        LabelType type = projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
+        Optional<LabelType> type =
+            projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
         // Only keep veto votes, defined as votes where:
         // 1- the label function allows minimum values to block submission.
         // 2- the vote holds the minimum value.
-        if (type == null || (type.isMaxNegative(psa) && type.getFunction().isBlock())) {
+        if (!type.isPresent()
+            || (type.get().isMaxNegative(psa) && type.get().getFunction().isBlock())) {
           continue;
         }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 6816361..4dbb6ee 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -22,7 +22,6 @@
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
@@ -502,8 +501,8 @@
     Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
-      LabelType type = labelTypes.byLabel(ent.getKey());
-      if (type == null) {
+      Optional<LabelType> type = labelTypes.byLabel(ent.getKey());
+      if (!type.isPresent()) {
         logger.atFine().log("label %s not found", ent.getKey());
         if (strictLabels) {
           throw new BadRequestException(
@@ -518,15 +517,15 @@
         logger.atFine().log(
             "skipping on behalf of permission check for label %s"
                 + " because caller is an internal user",
-            type.getName());
+            type.get().getName());
       } else {
         try {
-          perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type, ent.getValue()));
+          perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type.get(), ent.getValue()));
         } catch (AuthException e) {
           throw new AuthException(
               String.format(
                   "not permitted to modify label \"%s\" on behalf of \"%s\"",
-                  type.getName(), in.onBehalfOf),
+                  type.get().getName(), in.onBehalfOf),
               e);
         }
       }
@@ -558,8 +557,8 @@
     Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
-      LabelType lt = labelTypes.byLabel(ent.getKey());
-      if (lt == null) {
+      Optional<LabelType> lt = labelTypes.byLabel(ent.getKey());
+      if (!lt.isPresent()) {
         logger.atFine().log("label %s not found", ent.getKey());
         if (strictLabels) {
           throw new BadRequestException(
@@ -576,7 +575,7 @@
         continue;
       }
 
-      if (lt.getValue(ent.getValue()) == null) {
+      if (lt.get().getValue(ent.getValue()) == null) {
         logger.atFine().log("label value %s not found", ent.getValue());
         if (strictLabels) {
           throw new BadRequestException(
@@ -590,10 +589,10 @@
 
       short val = ent.getValue();
       try {
-        perm.check(new LabelPermission.WithValue(lt, val));
+        perm.check(new LabelPermission.WithValue(lt.get(), val));
       } catch (AuthException e) {
         throw new AuthException(
-            String.format("Applying label \"%s\": %d is restricted", lt.getName(), val), e);
+            String.format("Applying label \"%s\": %d is restricted", lt.get().getName(), val), e);
       }
     }
   }
@@ -1356,7 +1355,10 @@
       ChangeUpdate update = ctx.getUpdate(psId);
       for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
         String name = ent.getKey();
-        LabelType lt = requireNonNull(labelTypes.byLabel(name), name);
+        LabelType lt =
+            labelTypes
+                .byLabel(name)
+                .orElseThrow(() -> new IllegalStateException("no label config for " + name));
 
         PatchSetApproval c = current.remove(lt.getName());
         String normName = lt.getName();
@@ -1448,7 +1450,10 @@
       List<String> disallowed = new ArrayList<>(labelTypes.getLabelTypes().size());
 
       for (PatchSetApproval psa : del) {
-        LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
+        LabelType lt =
+            labelTypes
+                .byLabel(psa.label())
+                .orElseThrow(() -> new IllegalStateException("no label config for " + psa.label()));
         String normName = lt.getName();
         if (!lt.isAllowPostSubmit()) {
           disallowed.add(normName);
@@ -1460,7 +1465,10 @@
       }
 
       for (PatchSetApproval psa : ups) {
-        LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
+        LabelType lt =
+            labelTypes
+                .byLabel(psa.label())
+                .orElseThrow(() -> new IllegalStateException("no label config for " + psa.label()));
         String normName = lt.getName();
         if (!lt.isAllowPostSubmit()) {
           disallowed.add(normName);
@@ -1508,9 +1516,9 @@
           continue;
         }
 
-        LabelType lt = labelTypes.byLabel(a.labelId());
-        if (lt != null) {
-          current.put(lt.getName(), a);
+        Optional<LabelType> lt = labelTypes.byLabel(a.labelId());
+        if (lt.isPresent()) {
+          current.put(lt.get().getName(), a);
         } else {
           del.add(a);
         }
diff --git a/java/gerrit/PRED__load_commit_labels_1.java b/java/gerrit/PRED__load_commit_labels_1.java
index 5ee292ff..9a656b8 100644
--- a/java/gerrit/PRED__load_commit_labels_1.java
+++ b/java/gerrit/PRED__load_commit_labels_1.java
@@ -16,6 +16,7 @@
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
+import java.util.Optional;
 
 /** Exports list of {@code commit_label( label('Code-Review', 2), user(12345789) )}. */
 class PRED__load_commit_labels_1 extends Predicate.P1 {
@@ -38,13 +39,14 @@
     LabelTypes types = cd.getLabelTypes();
 
     for (PatchSetApproval a : cd.currentApprovals()) {
-      LabelType t = types.byLabel(a.labelId());
-      if (t == null) {
+      Optional<LabelType> t = types.byLabel(a.labelId());
+      if (!t.isPresent()) {
         continue;
       }
 
       StructureTerm labelTerm =
-          new StructureTerm(sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.value()));
+          new StructureTerm(
+              sym_label, SymbolTerm.intern(t.get().getName()), new IntegerTerm(a.value()));
 
       StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.accountId().get()));
 
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
index fc6b412..9cba362 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -72,7 +72,7 @@
 
   @Test
   public void createSchema_Label_CodeReview() throws Exception {
-    LabelType codeReview = getLabelTypes().byLabel("Code-Review");
+    LabelType codeReview = getLabelTypes().byLabel("Code-Review").get();
     assertThat(codeReview).isNotNull();
     assertThat(codeReview.getName()).isEqualTo("Code-Review");
     assertThat(codeReview.getDefaultValue()).isEqualTo(0);
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 35e6449..a28ae59 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 35e6449a517691a880c94e7467bc07360f8e6666
+Subproject commit a28ae590486934690e4e0a95d7eb75f8b60644a6