/* * Copyright (C) 2016 Google Inc. * * Licensed 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 com.google.api.tools.framework.model.testing; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.protobuf.Any; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.TextFormat; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * A protobuf text formatter which prints instances of the {@code google.protobuf.Any} type * in clear text, for use in tests. * */ public class TextFormatForTest { public static final TextFormatForTest INSTANCE = new TextFormatForTest(); private final Map<String, Message> anyConverterRegistry = Maps.newHashMap(); /** * Registers a type URL for Any together with a default instance. Only instances * which are registered here are rendered in clear text. */ public TextFormatForTest registerAnyInstance(String typeUrl, Message defaultInstance) { anyConverterRegistry.put(typeUrl, defaultInstance); return this; } /** * Appends message string to output. */ public void print(MessageOrBuilder message, Appendable output) throws IOException { output.append(printToString(message)); } /** * Converts a message into a string. */ public String printToString(MessageOrBuilder message) { StringBuilder result = new StringBuilder(); for (FieldDescriptor field : getFieldsInNumberOrder(message.getDescriptorForType())) { // Skip empty fields. if ((field.isRepeated() && message.getRepeatedFieldCount(field) == 0) || (!field.isRepeated() && !message.hasField(field))) { continue; } // Normalize repeated and singleton fields. Object rawValue = message.getField(field); @SuppressWarnings("unchecked") List<Object> values = field.isMapField() ? sortMapEntries(field, rawValue) : field.isRepeated() ? (List<Object>) rawValue : ImmutableList.of(rawValue); // Print field values. for (Object value : values) { result.append(printFieldToString(field, value)); } } return result.toString(); } private String printFieldToString(FieldDescriptor field, Object value) { StringBuilder result = new StringBuilder(); result.append(field.getName()); // If this is a google.protobuf.Any instance, attempt to replace value with the parsed // content, so we get clear text for it. Message anyValue = maybeUnpackAnyType(field, value); if (anyValue != null) { result.append(String.format(" [instance of %s]", anyValue.getDescriptorForType().getFullName())); value = anyValue; } // Render the value. if (field.getType() == FieldDescriptor.Type.MESSAGE) { result.append(String.format(" {%n")); String content = printToString((Message) value).trim(); if (!content.isEmpty()) { String indentedContent = content.replace("\n", "\n "); result.append(" " + indentedContent); result.append(String.format("%n}%n")); } else { result.append(String.format("}%n")); } } else { result.append(": "); try { TextFormat.printFieldValue(field, value, result); } catch (IOException e) { throw new RuntimeException(e); } result.append(String.format("%n")); } return result.toString(); } /** * Attempt to unpack if its an any instance. Returns null if not unpacked. */ @Nullable private Message maybeUnpackAnyType(FieldDescriptor field, Object value) { if (field.getType() == FieldDescriptor.Type.MESSAGE && field.getMessageType().getFullName().equals(Any.getDescriptor().getFullName())) { Any any = (Any) value; Message defaultInstance = anyConverterRegistry.get(any.getTypeUrl()); if (defaultInstance != null) { try { return defaultInstance.toBuilder().mergeFrom(any.getValue()).build(); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } } } return null; } /** * Get fields in field number order. */ private static Iterable<FieldDescriptor> getFieldsInNumberOrder(Descriptor descriptor) { List<FieldDescriptor> fields = new ArrayList<>(); fields.addAll(descriptor.getFields()); Collections.sort(fields, new Comparator<FieldDescriptor>() { @Override public int compare(FieldDescriptor f1, FieldDescriptor f2) { return f1.getNumber() - f2.getNumber(); } }); return fields; } /** * Sorts entries for a map field by key. */ @SuppressWarnings("unchecked") private static List<Object> sortMapEntries(FieldDescriptor field, Object value) { List<Message> entries = (List<Message>) value; List<Message> sortedEntries = new ArrayList<>(); sortedEntries.addAll(entries); final FieldDescriptor keyField = field.getMessageType().findFieldByNumber(1); Collections.sort(sortedEntries, new Comparator<Message>() { @Override public int compare(Message m1, Message m2) { return m1.getField(keyField).toString().compareTo(m2.getField(keyField).toString()); } }); return (List<Object>) (Object) sortedEntries; } }