FieldUtil.java
/*
* Copyright 2020 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 net.morimekta.proto.utils;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import net.morimekta.collect.SetBuilder;
import net.morimekta.collect.UnmodifiableList;
import net.morimekta.collect.UnmodifiableMap;
import net.morimekta.collect.UnmodifiableSet;
import net.morimekta.proto.ProtoEnum;
import net.morimekta.proto.ProtoMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
import static net.morimekta.proto.utils.ValueUtil.toJavaValue;
/**
* Utilities regarding handling fields and field types.
*/
public final class FieldUtil {
/**
* @param field A map type field descriptor.
* @return The key field type for the map.
*/
public static Descriptors.FieldDescriptor getMapKeyDescriptor(Descriptors.FieldDescriptor field) {
requireNonNull(field, "field == null");
if (!field.isMapField()) {
throw new IllegalArgumentException(
"Not a map field: " + field.getFullName());
}
return requireNonNull(field.getMessageType().findFieldByNumber(1), "keyDescriptor == null");
}
/**
* @param field A map type field descriptor.
* @return The value field type for the map.
*/
public static Descriptors.FieldDescriptor getMapValueDescriptor(Descriptors.FieldDescriptor field) {
requireNonNull(field, "field == null");
if (!field.isMapField()) {
throw new IllegalArgumentException(
"Not a map field: " + field.getFullName());
}
return requireNonNull(field.getMessageType().findFieldByNumber(2), "valueDescriptor == null");
}
/**
* @param field A field descriptor.
* @return The default value for the type for the field.
*/
public static Object getDefaultTypeValue(Descriptors.FieldDescriptor field) {
requireNonNull(field, "field == null");
if (field.isRepeated()) {
if (field.isMapField()) {
return UnmodifiableMap.mapOf();
}
return UnmodifiableList.listOf();
} else {
switch (field.getType().getJavaType()) {
case MESSAGE: {
return ProtoMessage.getDefaultInstance(field.getMessageType());
}
case ENUM: {
return ProtoEnum.getEnumDescriptor(field.getEnumType())
.getDefaultValue()
.orElse(null);
}
case STRING:
return "";
case INT:
return 0;
case LONG:
return 0L;
case FLOAT:
return 0.0F;
case DOUBLE:
return 0.0D;
case BOOLEAN:
return Boolean.FALSE;
case BYTE_STRING:
return ByteString.EMPTY;
default:
// not testable, should be impossible to happen.
return null;
}
}
}
/**
* @param field A field descriptor.
* @return The default value for the field.
*/
public static Object getDefaultFieldValue(Descriptors.FieldDescriptor field) {
if (field.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
return getDefaultTypeValue(field);
} else {
return Optional.ofNullable(toJavaValue(field, field.getDefaultValue()))
.orElseGet(() -> getDefaultTypeValue(field));
}
}
/**
* Convert a key path to a list of consecutive fields for recursive lookup.
*
* @param rootDescriptor The root message descriptor.
* @param path The '.' joined field name key.
* @return Array of fields.
*/
public static List<Descriptors.FieldDescriptor> fieldPathToFields(
Descriptors.Descriptor rootDescriptor,
String path) {
requireNonNull(rootDescriptor, "rootDescriptor == null");
requireNonNull(path, "path == null");
if (path.isEmpty()) {
throw new IllegalArgumentException("Empty path");
}
Descriptors.Descriptor descriptor = rootDescriptor;
ArrayList<Descriptors.FieldDescriptor> fields = new ArrayList<>();
String[] parts = path.split("\\.", Byte.MAX_VALUE);
for (int i = 0; i < (parts.length - 1); ++i) {
String name = parts[i];
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty field name in '" + path + "'");
}
Descriptors.FieldDescriptor field = descriptor.findFieldByName(name);
if (field == null) {
throw new IllegalArgumentException(
"Message " + descriptor.getFullName() + " has no field named " + name);
}
if (field.isMapField()) {
var valueField = FieldUtil.getMapValueDescriptor(field);
if (valueField.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {
throw new IllegalArgumentException(
"Intermediate map field '" + field.getFullName() + "' does not have message value type");
}
fields.add(field);
descriptor = valueField.getMessageType();
} else {
if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {
throw new IllegalArgumentException(
"Intermediate field '" + field.getFullName() + "' is not a message");
}
fields.add(field);
descriptor = field.getMessageType();
}
}
String name = parts[parts.length - 1];
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty field name in '" + path + "'");
}
Descriptors.FieldDescriptor field = descriptor.findFieldByName(name);
if (field == null) {
throw new IllegalArgumentException(
"Message " + descriptor.getFullName() + " has no field named " + name);
}
fields.add(field);
return fields;
}
/**
* Append field to the given path.
*
* @param fields Fields to make key path of.
* @return The new appended key path.
*/
public static String fieldPath(List<Descriptors.FieldDescriptor> fields) {
requireNonNull(fields, "fields == null");
if (fields.isEmpty()) {
throw new IllegalArgumentException("No field arguments");
}
return fields.stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.joining("."));
}
/**
* Append field to the given path.
*
* @param path The path to be appended to.
* @param field The field who's name should be appended.
* @return The new appended key path.
*/
public static String fieldPathAppend(
String path,
Descriptors.FieldDescriptor field) {
if (path == null || path.isEmpty()) {
return field.getName();
}
return path + "." + field.getName();
}
/**
* Filter a set of field descriptor paths to only contain the continuations
* of paths starting with the given field.
*
* @param fieldDesc The set of field descriptor paths.
* @param fieldsUnder The field to get paths under.
* @return The filtered set of field paths.
*/
public static Set<String> filterFields(Set<String> fieldDesc, Descriptors.FieldDescriptor fieldsUnder) {
// extensions are not covered by this, so an extension will have no
// fields undet it.
if (fieldsUnder.isExtension()) {
return Set.of();
}
SetBuilder<String> out = UnmodifiableSet.newBuilder();
var subPathPrefix = fieldsUnder.getName() + ".";
for (var fd : fieldDesc) {
if (fd.startsWith(subPathPrefix)) {
out.add(fd.substring(subPathPrefix.length()));
}
}
return out.build();
}
}