| // Protocol Buffers - Google's data interchange format |
| // Copyright 2008 Google Inc. All rights reserved. |
| // https://developers.google.com/protocol-buffers/ |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package com.google.protobuf.util; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import com.google.common.base.CaseFormat; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Splitter; |
| import com.google.common.primitives.Ints; |
| import com.google.protobuf.Descriptors.Descriptor; |
| import com.google.protobuf.Descriptors.FieldDescriptor; |
| import com.google.protobuf.FieldMask; |
| import com.google.protobuf.Internal; |
| import com.google.protobuf.Message; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Utility helper functions to work with {@link com.google.protobuf.FieldMask}. |
| */ |
| public class FieldMaskUtil { |
| private static final String FIELD_PATH_SEPARATOR = ","; |
| private static final String FIELD_PATH_SEPARATOR_REGEX = ","; |
| private static final String FIELD_SEPARATOR_REGEX = "\\."; |
| |
| private FieldMaskUtil() {} |
| |
| /** |
| * Converts a FieldMask to a string. |
| */ |
| public static String toString(FieldMask fieldMask) { |
| // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead. |
| StringBuilder result = new StringBuilder(); |
| boolean first = true; |
| for (String value : fieldMask.getPathsList()) { |
| if (value.isEmpty()) { |
| // Ignore empty paths. |
| continue; |
| } |
| if (first) { |
| first = false; |
| } else { |
| result.append(FIELD_PATH_SEPARATOR); |
| } |
| result.append(value); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * Parses from a string to a FieldMask. |
| */ |
| public static FieldMask fromString(String value) { |
| // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. |
| return fromStringList(null, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); |
| } |
| |
| /** |
| * Parses from a string to a FieldMask and validates all field paths. |
| * |
| * @throws IllegalArgumentException if any of the field path is invalid. |
| */ |
| public static FieldMask fromString(Class<? extends Message> type, String value) { |
| // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. |
| return fromStringList(type, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); |
| } |
| |
| /** |
| * Constructs a FieldMask for a list of field paths in a certain type. |
| * |
| * @throws IllegalArgumentException if any of the field path is not valid. |
| */ |
| // TODO(xiaofeng): Consider renaming fromStrings() |
| public static FieldMask fromStringList(Class<? extends Message> type, Iterable<String> paths) { |
| FieldMask.Builder builder = FieldMask.newBuilder(); |
| for (String path : paths) { |
| if (path.isEmpty()) { |
| // Ignore empty field paths. |
| continue; |
| } |
| if (type != null && !isValid(type, path)) { |
| throw new IllegalArgumentException(path + " is not a valid path for " + type); |
| } |
| builder.addPaths(path); |
| } |
| return builder.build(); |
| } |
| |
| /** |
| * Constructs a FieldMask from the passed field numbers. |
| * |
| * @throws IllegalArgumentException if any of the fields are invalid for the message. |
| */ |
| public static FieldMask fromFieldNumbers(Class<? extends Message> type, int... fieldNumbers) { |
| return fromFieldNumbers(type, Ints.asList(fieldNumbers)); |
| } |
| |
| /** |
| * Constructs a FieldMask from the passed field numbers. |
| * |
| * @throws IllegalArgumentException if any of the fields are invalid for the message. |
| */ |
| public static FieldMask fromFieldNumbers( |
| Class<? extends Message> type, Iterable<Integer> fieldNumbers) { |
| Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); |
| |
| FieldMask.Builder builder = FieldMask.newBuilder(); |
| for (Integer fieldNumber : fieldNumbers) { |
| FieldDescriptor field = descriptor.findFieldByNumber(fieldNumber); |
| checkArgument( |
| field != null, |
| String.format("%s is not a valid field number for %s.", fieldNumber, type)); |
| builder.addPaths(field.getName()); |
| } |
| return builder.build(); |
| } |
| |
| /** |
| * Converts a field mask to a Proto3 JSON string, that is converting from snake case to camel |
| * case and joining all paths into one string with commas. |
| */ |
| public static String toJsonString(FieldMask fieldMask) { |
| List<String> paths = new ArrayList<String>(fieldMask.getPathsCount()); |
| for (String path : fieldMask.getPathsList()) { |
| if (path.isEmpty()) { |
| continue; |
| } |
| paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); |
| } |
| return Joiner.on(FIELD_PATH_SEPARATOR).join(paths); |
| } |
| |
| /** |
| * Converts a field mask from a Proto3 JSON string, that is splitting the paths along commas and |
| * converting from camel case to snake case. |
| */ |
| public static FieldMask fromJsonString(String value) { |
| Iterable<String> paths = Splitter.on(FIELD_PATH_SEPARATOR).split(value); |
| FieldMask.Builder builder = FieldMask.newBuilder(); |
| for (String path : paths) { |
| if (path.isEmpty()) { |
| continue; |
| } |
| builder.addPaths(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, path)); |
| } |
| return builder.build(); |
| } |
| |
| /** |
| * Checks whether paths in a given fields mask are valid. |
| */ |
| public static boolean isValid(Class<? extends Message> type, FieldMask fieldMask) { |
| Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); |
| |
| return isValid(descriptor, fieldMask); |
| } |
| |
| /** |
| * Checks whether paths in a given fields mask are valid. |
| */ |
| public static boolean isValid(Descriptor descriptor, FieldMask fieldMask) { |
| for (String path : fieldMask.getPathsList()) { |
| if (!isValid(descriptor, path)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Checks whether a given field path is valid. |
| */ |
| public static boolean isValid(Class<? extends Message> type, String path) { |
| Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); |
| |
| return isValid(descriptor, path); |
| } |
| |
| /** |
| * Checks whether paths in a given fields mask are valid. |
| */ |
| public static boolean isValid(Descriptor descriptor, String path) { |
| String[] parts = path.split(FIELD_SEPARATOR_REGEX); |
| if (parts.length == 0) { |
| return false; |
| } |
| for (String name : parts) { |
| if (descriptor == null) { |
| return false; |
| } |
| FieldDescriptor field = descriptor.findFieldByName(name); |
| if (field == null) { |
| return false; |
| } |
| if (!field.isRepeated() && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { |
| descriptor = field.getMessageType(); |
| } else { |
| descriptor = null; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Converts a FieldMask to its canonical form. In the canonical form of a |
| * FieldMask, all field paths are sorted alphabetically and redundant field |
| * paths are moved. |
| */ |
| public static FieldMask normalize(FieldMask mask) { |
| return new FieldMaskTree(mask).toFieldMask(); |
| } |
| |
| /** |
| * Creates a union of two or more FieldMasks. |
| */ |
| public static FieldMask union( |
| FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) { |
| FieldMaskTree maskTree = new FieldMaskTree(firstMask).mergeFromFieldMask(secondMask); |
| for (FieldMask mask : otherMasks) { |
| maskTree.mergeFromFieldMask(mask); |
| } |
| return maskTree.toFieldMask(); |
| } |
| |
| /** |
| * Calculates the intersection of two FieldMasks. |
| */ |
| public static FieldMask intersection(FieldMask mask1, FieldMask mask2) { |
| FieldMaskTree tree = new FieldMaskTree(mask1); |
| FieldMaskTree result = new FieldMaskTree(); |
| for (String path : mask2.getPathsList()) { |
| tree.intersectFieldPath(path, result); |
| } |
| return result.toFieldMask(); |
| } |
| |
| /** |
| * Options to customize merging behavior. |
| */ |
| public static final class MergeOptions { |
| private boolean replaceMessageFields = false; |
| private boolean replaceRepeatedFields = false; |
| // TODO(b/28277137): change the default behavior to always replace primitive fields after |
| // fixing all failing TAP tests. |
| private boolean replacePrimitiveFields = false; |
| |
| /** |
| * Whether to replace message fields (i.e., discard existing content in |
| * destination message fields) when merging. |
| * Default behavior is to merge the source message field into the |
| * destination message field. |
| */ |
| public boolean replaceMessageFields() { |
| return replaceMessageFields; |
| } |
| |
| /** |
| * Whether to replace repeated fields (i.e., discard existing content in |
| * destination repeated fields) when merging. |
| * Default behavior is to append elements from source repeated field to the |
| * destination repeated field. |
| */ |
| public boolean replaceRepeatedFields() { |
| return replaceRepeatedFields; |
| } |
| |
| /** |
| * Whether to replace primitive (non-repeated and non-message) fields in |
| * destination message fields with the source primitive fields (i.e., if the |
| * field is set in the source, the value is copied to the |
| * destination; if the field is unset in the source, the field is cleared |
| * from the destination) when merging. |
| * |
| * <p>Default behavior is to always set the value of the source primitive |
| * field to the destination primitive field, and if the source field is |
| * unset, the default value of the source field is copied to the |
| * destination. |
| */ |
| public boolean replacePrimitiveFields() { |
| return replacePrimitiveFields; |
| } |
| |
| public void setReplaceMessageFields(boolean value) { |
| replaceMessageFields = value; |
| } |
| |
| public void setReplaceRepeatedFields(boolean value) { |
| replaceRepeatedFields = value; |
| } |
| |
| public void setReplacePrimitiveFields(boolean value) { |
| replacePrimitiveFields = value; |
| } |
| } |
| |
| /** |
| * Merges fields specified by a FieldMask from one message to another with the |
| * specified merge options. |
| */ |
| public static void merge( |
| FieldMask mask, Message source, Message.Builder destination, MergeOptions options) { |
| new FieldMaskTree(mask).merge(source, destination, options); |
| } |
| |
| /** |
| * Merges fields specified by a FieldMask from one message to another. |
| */ |
| public static void merge(FieldMask mask, Message source, Message.Builder destination) { |
| merge(mask, source, destination, new MergeOptions()); |
| } |
| } |