FieldUtil.java

  1. /*
  2.  * Copyright 2020 Providence Authors
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.proto.utils;

  22. import com.google.protobuf.ByteString;
  23. import com.google.protobuf.Descriptors;
  24. import net.morimekta.collect.SetBuilder;
  25. import net.morimekta.collect.UnmodifiableList;
  26. import net.morimekta.collect.UnmodifiableMap;
  27. import net.morimekta.collect.UnmodifiableSet;
  28. import net.morimekta.proto.ProtoEnum;
  29. import net.morimekta.proto.ProtoMessage;

  30. import java.util.ArrayList;
  31. import java.util.List;
  32. import java.util.Optional;
  33. import java.util.Set;
  34. import java.util.stream.Collectors;

  35. import static java.util.Objects.requireNonNull;
  36. import static net.morimekta.proto.utils.ValueUtil.toJavaValue;

  37. /**
  38.  * Utilities regarding handling fields and field types.
  39.  */
  40. public final class FieldUtil {
  41.     /**
  42.      * @param field A map type field descriptor.
  43.      * @return The key field type for the map.
  44.      */
  45.     public static Descriptors.FieldDescriptor getMapKeyDescriptor(Descriptors.FieldDescriptor field) {
  46.         requireNonNull(field, "field == null");
  47.         if (!field.isMapField()) {
  48.             throw new IllegalArgumentException(
  49.                     "Not a map field: " + field.getFullName());
  50.         }
  51.         return requireNonNull(field.getMessageType().findFieldByNumber(1), "keyDescriptor == null");
  52.     }

  53.     /**
  54.      * @param field A map type field descriptor.
  55.      * @return The value field type for the map.
  56.      */
  57.     public static Descriptors.FieldDescriptor getMapValueDescriptor(Descriptors.FieldDescriptor field) {
  58.         requireNonNull(field, "field == null");
  59.         if (!field.isMapField()) {
  60.             throw new IllegalArgumentException(
  61.                     "Not a map field: " + field.getFullName());
  62.         }
  63.         return requireNonNull(field.getMessageType().findFieldByNumber(2), "valueDescriptor == null");
  64.     }

  65.     /**
  66.      * @param field A field descriptor.
  67.      * @return The default value for the type for the field.
  68.      */
  69.     public static Object getDefaultTypeValue(Descriptors.FieldDescriptor field) {
  70.         requireNonNull(field, "field == null");
  71.         if (field.isRepeated()) {
  72.             if (field.isMapField()) {
  73.                 return UnmodifiableMap.mapOf();
  74.             }
  75.             return UnmodifiableList.listOf();
  76.         } else {
  77.             switch (field.getType().getJavaType()) {
  78.                 case MESSAGE: {
  79.                     return ProtoMessage.getDefaultInstance(field.getMessageType());
  80.                 }
  81.                 case ENUM: {
  82.                     return ProtoEnum.getEnumDescriptor(field.getEnumType())
  83.                                     .getDefaultValue()
  84.                                     .orElse(null);
  85.                 }
  86.                 case STRING:
  87.                     return "";
  88.                 case INT:
  89.                     return 0;
  90.                 case LONG:
  91.                     return 0L;
  92.                 case FLOAT:
  93.                     return 0.0F;
  94.                 case DOUBLE:
  95.                     return 0.0D;
  96.                 case BOOLEAN:
  97.                     return Boolean.FALSE;
  98.                 case BYTE_STRING:
  99.                     return ByteString.EMPTY;
  100.                 default:
  101.                     // not testable, should be impossible to happen.
  102.                     return null;
  103.             }
  104.         }
  105.     }

  106.     /**
  107.      * @param field A field descriptor.
  108.      * @return The default value for the field.
  109.      */
  110.     public static Object getDefaultFieldValue(Descriptors.FieldDescriptor field) {
  111.         if (field.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
  112.             return getDefaultTypeValue(field);
  113.         } else {
  114.             return Optional.ofNullable(toJavaValue(field, field.getDefaultValue()))
  115.                            .orElseGet(() -> getDefaultTypeValue(field));
  116.         }
  117.     }

  118.     /**
  119.      * Convert a key path to a list of consecutive fields for recursive lookup.
  120.      *
  121.      * @param rootDescriptor The root message descriptor.
  122.      * @param path           The '.' joined field name key.
  123.      * @return Array of fields.
  124.      */
  125.     public static List<Descriptors.FieldDescriptor> fieldPathToFields(
  126.             Descriptors.Descriptor rootDescriptor,
  127.             String path) {
  128.         requireNonNull(rootDescriptor, "rootDescriptor == null");
  129.         requireNonNull(path, "path == null");
  130.         if (path.isEmpty()) {
  131.             throw new IllegalArgumentException("Empty path");
  132.         }
  133.         Descriptors.Descriptor descriptor = rootDescriptor;

  134.         ArrayList<Descriptors.FieldDescriptor> fields = new ArrayList<>();
  135.         String[] parts = path.split("\\.", Byte.MAX_VALUE);
  136.         for (int i = 0; i < (parts.length - 1); ++i) {
  137.             String name = parts[i];
  138.             if (name.isEmpty()) {
  139.                 throw new IllegalArgumentException("Empty field name in '" + path + "'");
  140.             }
  141.             Descriptors.FieldDescriptor field = descriptor.findFieldByName(name);
  142.             if (field == null) {
  143.                 throw new IllegalArgumentException(
  144.                         "Message " + descriptor.getFullName() + " has no field named " + name);
  145.             }
  146.             if (field.isMapField()) {
  147.                 var valueField = FieldUtil.getMapValueDescriptor(field);
  148.                 if (valueField.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {
  149.                     throw new IllegalArgumentException(
  150.                             "Intermediate map field '" + field.getFullName() + "' does not have message value type");
  151.                 }
  152.                 fields.add(field);
  153.                 descriptor = valueField.getMessageType();
  154.             } else {
  155.                 if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {
  156.                     throw new IllegalArgumentException(
  157.                             "Intermediate field '" + field.getFullName() + "' is not a message");
  158.                 }
  159.                 fields.add(field);
  160.                 descriptor = field.getMessageType();
  161.             }
  162.         }

  163.         String name = parts[parts.length - 1];
  164.         if (name.isEmpty()) {
  165.             throw new IllegalArgumentException("Empty field name in '" + path + "'");
  166.         }
  167.         Descriptors.FieldDescriptor field = descriptor.findFieldByName(name);
  168.         if (field == null) {
  169.             throw new IllegalArgumentException(
  170.                     "Message " + descriptor.getFullName() + " has no field named " + name);
  171.         }
  172.         fields.add(field);
  173.         return fields;
  174.     }

  175.     /**
  176.      * Append field to the given path.
  177.      *
  178.      * @param fields Fields to make key path of.
  179.      * @return The new appended key path.
  180.      */
  181.     public static String fieldPath(List<Descriptors.FieldDescriptor> fields) {
  182.         requireNonNull(fields, "fields == null");
  183.         if (fields.isEmpty()) {
  184.             throw new IllegalArgumentException("No field arguments");
  185.         }
  186.         return fields.stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.joining("."));
  187.     }

  188.     /**
  189.      * Append field to the given path.
  190.      *
  191.      * @param path  The path to be appended to.
  192.      * @param field The field who's name should be appended.
  193.      * @return The new appended key path.
  194.      */
  195.     public static String fieldPathAppend(
  196.             String path,
  197.             Descriptors.FieldDescriptor field) {
  198.         if (path == null || path.isEmpty()) {
  199.             return field.getName();
  200.         }
  201.         return path + "." + field.getName();
  202.     }

  203.     /**
  204.      * Filter a set of field descriptor paths to only contain the continuations
  205.      * of paths starting with the given field.
  206.      *
  207.      * @param fieldDesc   The set of field descriptor paths.
  208.      * @param fieldsUnder The field to get paths under.
  209.      * @return The filtered set of field paths.
  210.      */
  211.     public static Set<String> filterFields(Set<String> fieldDesc, Descriptors.FieldDescriptor fieldsUnder) {
  212.         // extensions are not covered by this, so an extension will have no
  213.         // fields undet it.
  214.         if (fieldsUnder.isExtension()) {
  215.             return Set.of();
  216.         }
  217.         SetBuilder<String> out = UnmodifiableSet.newBuilder();
  218.         var subPathPrefix = fieldsUnder.getName() + ".";
  219.         for (var fd : fieldDesc) {
  220.             if (fd.startsWith(subPathPrefix)) {
  221.                 out.add(fd.substring(subPathPrefix.length()));
  222.             }
  223.         }
  224.         return out.build();
  225.     }
  226. }