// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// http://code.google.com/p/protobuf/
//
// 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.github.protobufel.grammar;
import static java.lang.Math.min;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.github.protobufel.grammar.AbstractMessageUtils.FieldFilter.Result;
import com.google.protobuf.ByteString;
import com.google.protobuf.DescriptorProtos.DescriptorProto;
import com.google.protobuf.DescriptorProtos.DescriptorProto.ExtensionRange;
import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
import com.google.protobuf.DescriptorProtos.EnumOptions;
import com.google.protobuf.DescriptorProtos.EnumValueDescriptorProto;
import com.google.protobuf.DescriptorProtos.EnumValueOptions;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldOptions;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileOptions;
import com.google.protobuf.DescriptorProtos.MessageOptions;
import com.google.protobuf.DescriptorProtos.MethodDescriptorProto;
import com.google.protobuf.DescriptorProtos.MethodOptions;
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;
import com.google.protobuf.DescriptorProtos.ServiceOptions;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import com.google.protobuf.Message;
import com.google.protobuf.UnknownFieldSet;
import com.google.protobuf.UnknownFieldSet.Field;
/**
* Utilities for filtered Message equality. Large portions taken with minimal modifications from
* {@link com.google.protobuf.AbstractMessage}.
*
* @author protobufel@gmail.com David Tesler
*/
@NonNullByDefault
final class AbstractMessageUtils {
private AbstractMessageUtils() {}
/**
* Compares the filtered field's values conforming the equals contract, or returns NOT_FILTERED.
* The regular comparison is assumed in case of NOT_FILTERED, to be done by {@link compareField}.
*/
public interface FieldFilter {
public static enum Result {
EQUAL, NOT_EQUAL, NOT_FILTERED
}
/**
* Compares the filtered field's values conforming the equals contract, or returns NOT_FILTERED.
* The regular comparison is assumed in case of NOT_FILTERED, to be done by {@link compareField}
* .
*/
Result isEqual(FieldDescriptor field, Object value1, Object value2);
Result isEqual(Descriptor type, UnknownFieldSet value1, UnknownFieldSet value2);
}
private static final FieldFilter IDENTITY_FIELD_FILTER = new FieldFilter() {
@Override
public Result isEqual(final FieldDescriptor field, final Object value1, final Object value2) {
return Result.NOT_FILTERED;
}
@Override
public Result isEqual(final Descriptor type, final UnknownFieldSet value1,
final UnknownFieldSet value2) {
return Result.NOT_FILTERED;
}
};
/**
* Returns the identity filter, which always returns NOT_FILTERED.
*/
public static FieldFilter identityFilter() {
return IDENTITY_FIELD_FILTER;
}
public static final class ProtoFilter implements FieldFilter {
@SuppressWarnings("null")
private static final FieldDescriptor RANGE_END_FIELD = ExtensionRange.getDescriptor()
.findFieldByNumber(ExtensionRange.END_FIELD_NUMBER);
@SuppressWarnings("null")
private static final String DESCRIPTOR_DEFAULT_FIELD = FieldDescriptorProto.getDescriptor()
.findFieldByNumber(FieldDescriptorProto.DEFAULT_VALUE_FIELD_NUMBER).getFullName();
private static final Set<Descriptor> ALL_OPTION_DESCRIPTORS = new HashSet<>(Arrays.asList(
FileOptions.getDescriptor(), MessageOptions.getDescriptor(), EnumOptions.getDescriptor(),
FieldOptions.getDescriptor(), EnumValueOptions.getDescriptor(),
ServiceOptions.getDescriptor(), MethodOptions.getDescriptor()));
private final Set<String> skipFields;
public ProtoFilter() {
skipFields = new HashSet<String>(Arrays.asList(DESCRIPTOR_DEFAULT_FIELD));
}
public ProtoFilter(final ProtoFilter other) {
skipFields = new HashSet<String>(other.skipFields);
}
protected ProtoFilter addSkipField(final String skipField) {
skipFields.add(skipField);
return this;
}
protected ProtoFilter addSkipFields(final String... skipFields) {
if (skipFields.length > 0) {
for (final String skipField : skipFields) {
this.skipFields.add(skipField);
}
}
return this;
}
@SuppressWarnings("null")
@Override
public Result isEqual(final FieldDescriptor field, final Object value1, final Object value2) {
if (skipFields.contains(field.getFullName())) {
return Result.EQUAL;
}
if (field == RANGE_END_FIELD) {
if (Objects.equals(min(ProtoFileParser.MAX_FIELD_NUMBER + 1, (Integer) value1),
min(ProtoFileParser.MAX_FIELD_NUMBER + 1, (Integer) value2))) {
return Result.EQUAL;
} else {
return Result.NOT_EQUAL;
}
}
if (field.getJavaType() != JavaType.MESSAGE) {
return Result.NOT_FILTERED;
}
if (field.getMessageType() == FieldDescriptorProto.getDescriptor()) {
if (field.isRepeated()) {
@SuppressWarnings("unchecked")
final List<Message> list1 = (List<Message>) value1;
final List<?> list2 = (List<?>) value1;
if (list1.size() != list2.size()) {
return Result.NOT_EQUAL;
}
for (int i = 0; i < list1.size(); i++) {
if (!isSingleFieldProtoEqual(list1.get(i), list2.get(i))) {
return Result.NOT_EQUAL;
}
}
} else if (!isSingleFieldProtoEqual(value1, value2)) {
return Result.NOT_EQUAL;
}
return Result.EQUAL;
}
return Result.NOT_FILTERED;
}
private boolean isSingleFieldProtoEqual(final Object value1, @Nullable final Object value2) {
final FieldDescriptorProto fieldProto1 = (FieldDescriptorProto) value1;
final FieldDescriptorProto fieldProto2 = (FieldDescriptorProto) value2;
final ProtoFilter filter = new ProtoFilter();
switch (fieldProto1.getType()) {
default:
break;
}
return compareMessage(fieldProto1, fieldProto2, filter);
}
@SuppressWarnings("null")
@Override
public Result isEqual(final Descriptor type, final UnknownFieldSet value1,
final UnknownFieldSet value2) {
if (value1 == value2) {
return Result.EQUAL;
}
if (!ALL_OPTION_DESCRIPTORS.contains(type)) {
return Result.NOT_FILTERED;
}
final Map<Integer, Field> map1 = value1.asMap();
final Map<Integer, Field> map2 = value2.asMap();
if (map1.size() != map2.size()) {
return Result.NOT_EQUAL;
}
for (final Entry<Integer, Field> entry : map1.entrySet()) {
final Field field1 = entry.getValue();
final Field field2 = map2.get(entry.getKey());
if (field2 == null
|| !Objects.equals(getUnknownFieldValue(field1), getUnknownFieldValue(field2))) {
return Result.NOT_EQUAL;
}
}
return Result.EQUAL;
}
private @Nullable Object getUnknownFieldValue(final Field field) {
if (!field.getFixed32List().isEmpty()) {
return field.getFixed32List().get(field.getFixed32List().size() - 1);
}
if (!field.getFixed64List().isEmpty()) {
return field.getFixed64List().get(field.getFixed64List().size() - 1);
}
if (!field.getGroupList().isEmpty()) {
return field.getGroupList().get(field.getGroupList().size() - 1);
}
if (!field.getLengthDelimitedList().isEmpty()) {
return field.getLengthDelimitedList().get(field.getLengthDelimitedList().size() - 1);
}
if (!field.getVarintList().isEmpty()) {
return field.getVarintList().get(field.getVarintList().size() - 1);
}
return null;
}
}
public static FilteredProtoTextComparator filteredUnsupportedProtoComparator() {
final Iterable<String> skipRegexes =
Arrays.asList(
// skip default values
Pattern.quote(FieldDescriptorProto.getDescriptor()
.findFieldByNumber(FieldDescriptorProto.DEFAULT_VALUE_FIELD_NUMBER).getFullName()),
// skip extension range max
Pattern.quote(DescriptorProto.ExtensionRange.getDescriptor()
.findFieldByNumber(DescriptorProto.ExtensionRange.END_FIELD_NUMBER).getFullName()));
return FilteredProtoTextComparator.filteredByRegexes(skipRegexes);
}
public static final class FilteredProtoTextComparator implements Comparator<FileDescriptorProto> {
private static final FilteredProtoTextComparator IDENTITY_COMPARATOR =
new FilteredProtoTextComparator();
private final List<Pattern> skipPatterns;
@SuppressWarnings("null")
private FilteredProtoTextComparator() {
skipPatterns = Collections.emptyList();
}
@SuppressWarnings("null")
FilteredProtoTextComparator(final Iterable<Pattern> skipPatterns) {
final List<Pattern> list = new ArrayList<>();
for (final Pattern pattern : skipPatterns) {
list.add(Objects.requireNonNull(pattern));
}
this.skipPatterns = Collections.unmodifiableList(list);
}
public static FilteredProtoTextComparator indentity() {
return IDENTITY_COMPARATOR;
}
public static FilteredProtoTextComparator filteredByRegexes(final Iterable<String> skipRegexes) {
return new FilteredProtoTextComparator(getSkipPatterns(skipRegexes));
}
public static FilteredProtoTextComparator filteredByPatterns(
final Iterable<Pattern> skipPatterns) {
return new FilteredProtoTextComparator(skipPatterns);
}
@NonNullByDefault(false)
@Override
public int compare(final FileDescriptorProto o1, final FileDescriptorProto o2) {
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
final String filteredProto1 = buildFilteredProtoByPatterns(o1, skipPatterns).toString();
final String filteredProto2 = buildFilteredProtoByPatterns(o2, skipPatterns).toString();
return filteredProto1.compareTo(filteredProto2);
}
}
private FileDescriptorProto buildFilteredProtoByPatterns(final FileDescriptorProto original,
final Collection<Pattern> skipPatterns) {
Objects.requireNonNull(original);
if (Objects.requireNonNull(skipPatterns).isEmpty()) {
return original;
}
return getMessage(original, skipPatterns);
}
private static List<Pattern> getSkipPatterns(final Iterable<String> skipRegexes) {
final List<Pattern> skipPatterns = new ArrayList<Pattern>();
for (final String regex : Objects.requireNonNull(skipRegexes)) {
skipPatterns.add(Pattern.compile(Objects.requireNonNull(regex)));
}
return skipPatterns;
}
@SuppressWarnings({"null", "unchecked"})
private boolean addField(final Message.Builder protoBuilder, final FieldDescriptor field,
final Object value, final Iterable<Pattern> skipPatterns) {
if (isSkip(field.getFullName(), skipPatterns)) {
return false;
}
if (field.getJavaType() == JavaType.MESSAGE) {
if (field.isRepeated()) {
for (final Message message : (List<Message>) value) {
protoBuilder.addRepeatedField(field, getMessage(message, skipPatterns));
}
} else {
protoBuilder.setField(field, getMessage((Message) value, skipPatterns));
}
} else {
protoBuilder.setField(field, value);
}
return true;
}
@SuppressWarnings({"null", "unchecked"})
private <MType extends Message> MType getMessage(final MType message,
final Iterable<Pattern> skipPatterns) {
final Message.Builder builder = message.toBuilder();
for (final Entry<FieldDescriptor, Object> entry : message.getAllFields().entrySet()) {
addField(builder, entry.getKey(), entry.getValue(), skipPatterns);
}
return (MType) builder.buildPartial();
}
private boolean isSkip(final String name, final Iterable<Pattern> skipPatterns) {
for (final Pattern pattern : skipPatterns) {
if (pattern.matcher(name).matches()) {
return true;
}
}
return false;
}
}
// FIXME remove when no longer needed!
public static final class FileProtoEqualComparator implements Comparator<FileDescriptorProto> {
private static final FileProtoEqualComparator INSTANCE = new FileProtoEqualComparator();
private FileProtoEqualComparator() {}
public static FileProtoEqualComparator of() {
return INSTANCE;
}
@NonNullByDefault(false)
@Override
public int compare(final FileDescriptorProto o1, final FileDescriptorProto o2) {
return o1 == o2 ? 0 : o1 == null ? -1 : compareProto(o1, o2) ? 0 : -1;
}
}
public static boolean compareProto(final FileDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final DescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final EnumDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final EnumValueDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final FieldDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final ServiceDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
public static boolean compareProto(final MethodDescriptorProto me, final @Nullable Object other) {
return compareMessage(me, other, new ProtoFilter());
}
/**
* A helper method for Message equality. Compares two messages using the filter. Filtered fields
* will be compared directly by the filter, otherwise regularly. If field is of Message type, then
* it will be compared by this method recursively.
*
* @param me "this" Message
* @param other a Message to compare with
* @param filter a {@link FieldFilter} to compare fields with
* @return true iif messages are equal, according to equals contract
*/
@SuppressWarnings("null")
public static boolean compareMessage(final Message me, final @Nullable Object other,
final FieldFilter filter) {
if (me == other) {
return true;
}
if (!(other instanceof Message)) {
return false;
}
final Message notMe = (Message) other;
if (me.getDescriptorForType() != notMe.getDescriptorForType()) {
return false;
}
final Result filterResult =
filter.isEqual(me.getDescriptorForType(), me.getUnknownFields(), notMe.getUnknownFields());
switch (filterResult) {
case EQUAL:
break;
case NOT_EQUAL:
return false;
default:
if (!me.getUnknownFields().equals(notMe.getUnknownFields())) {
return false;
}
}
final Map<FieldDescriptor, Object> myFields = me.getAllFields();
final Map<FieldDescriptor, Object> otherFields = notMe.getAllFields();
if (myFields.size() != otherFields.size()) {
return false;
}
for (final Entry<FieldDescriptor, Object> myEntry : myFields.entrySet()) {
final FieldDescriptor field = myEntry.getKey();
final Object otherValue = otherFields.get(field);
if (otherValue == null) {
return false;
}
if (!compareField(field, myEntry.getValue(), otherValue, filter)) {
return false;
}
}
return true;
}
/**
* Compares two field values using the filter. Takes special care of bytes fields because
* immutable messages and mutable messages use different Java type to reprensent a bytes field and
* this method should be able to compare immutable messages, mutable messages and also an
* immutable message to a mutable message.
*
* @param compareUnknown
*/
@SuppressWarnings("null")
public static boolean compareField(final FieldDescriptor descriptor, final Object value1,
final Object value2, final FieldFilter filter) {
final Result filterResult = filter.isEqual(descriptor, value1, value2);
switch (filterResult) {
case EQUAL:
return true;
case NOT_EQUAL:
return false;
default:
break;
}
if (descriptor.getType() == FieldDescriptor.Type.BYTES) {
if (descriptor.isRepeated()) {
final List<?> list1 = (List<?>) value1;
final List<?> list2 = (List<?>) value2;
if (list1.size() != list2.size()) {
return false;
}
for (int i = 0; i < list1.size(); i++) {
if (!compareBytes(list1.get(i), list2.get(i))) {
return false;
}
}
} else {
// Compares a singular bytes field.
if (!compareBytes(value1, value2)) {
return false;
}
}
} else {
// Compare non-bytes fields.
if (descriptor.getJavaType() == JavaType.MESSAGE) {
if (descriptor.isRepeated()) {
@SuppressWarnings("unchecked")
final List<Message> list1 = (List<Message>) value1;
final List<?> list2 = (List<?>) value2;
if (list1.size() != list2.size()) {
return false;
}
int i = -1;
for (final Message message1 : list1) {
if (!compareMessage(message1, list2.get(++i), filter)) {
return false;
}
}
} else if (!compareMessage((Message) value1, value2, filter)) {
return false;
}
} else if (!value1.equals(value2)) {
return false;
}
}
return true;
}
/**
* Compares two bytes fields. The parameters must be either a byte array or a ByteString object.
* They can be of different type though.
*/
private static boolean compareBytes(final Object a, final Object b) {
if (a instanceof byte[] && b instanceof byte[]) {
return Arrays.equals((byte[]) a, (byte[]) b);
}
return toByteString(a).equals(toByteString(b));
}
@SuppressWarnings("null")
private static ByteString toByteString(final Object value) {
if (value instanceof byte[]) {
return ByteString.copyFrom((byte[]) value);
} else {
return (ByteString) value;
}
}
}