package com.philemonworks.critter.proto;
import com.google.common.base.Optional;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.UnknownFieldSet;
import com.squareup.protoparser.DataType;
import com.squareup.protoparser.FieldElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
/**
* Inspector can produces a String representation of a value
* found by navigating a datastructure using a JSON path notation:
* <p/>
* each path starts with a dot, the root
* next in the path is either a field name or an index into an array-like field
* <p/>
* <p/>
* Created by emicklei on 01/03/16.
*/
public class Inspector {
private static final Logger log = LoggerFactory.getLogger(Inspector.class);
static final int NoIndex = -1;
public static final String InvalidPath = "** invalid path **";
Definitions messageDefinitions;
UnknownFieldSet fieldSet;
String messageName;
String fieldName = ""; // if not empty then the valueString will show the value of the field in the fieldset.
int fieldIndex = NoIndex;
/**
* Reads one protocol buffers message.
*
* @param data
* @return whether message is valid.
*/
public boolean read(byte[] data) {
try {
this.fieldSet = UnknownFieldSet.parseFrom(data);
return true;
} catch (InvalidProtocolBufferException ex) {
return false;
}
}
/**
* Return the String value by navigating a path in the message structure.
*
* @param dottedPath , always start with .
* @return
*/
public String path(String dottedPath) {
if (dottedPath == null || dottedPath.length() == 0) {
return InvalidPath;
}
String[] tokens = dottedPath.split("\\.");
Optional<Inspector> sub = this.pathFindIn(1, tokens); // because of .dot start with 1
if (!sub.isPresent()) {
return InvalidPath;
}
return sub.get().toString();
}
/**
* pathFindIn returns an Inspector on the object referred to by navigating the tokens (field names and indices).
*
* @param index
* @param tokens
* @return
*/
private Optional<Inspector> pathFindIn(int index, String[] tokens) {
if (index == tokens.length) {
return Optional.of(this);
}
if (index > tokens.length) {
return Optional.absent();
}
String token = tokens[index];
if (token.length() == 0) {
return Optional.absent();
}
this.fieldIndex = this.tokenAsIndex(index, tokens);
if (NoIndex != this.fieldIndex) {
// found index
return this.pathFindIn(index + 1, tokens);
}
this.fieldName = token;
Optional<FieldElement> element = this.messageDefinitions.fieldElementNamed(this.messageName, token);
if (!element.isPresent()) {
log.debug("no fieldelement for {} # {} in {}", this.messageName, token, this.messageDefinitions);
return Optional.absent();
}
UnknownFieldSet.Field field = this.fieldSet.getField(element.get().tag());
if (element.get().type() instanceof DataType.NamedType) {
String qName = this.messageDefinitions.qualifiedNameInNamespaceOf(((DataType.NamedType) element.get().type()).name(), this.messageName);
Inspector sub = this.messageDefinitions.newInspector(qName);
try {
sub.readAll(field.getLengthDelimitedList());
} catch (IOException e) {
log.debug("cannot read field {} ({})", element.get().name(), qName);
return Optional.absent();
}
return sub.pathFindIn(index + 1, tokens);
}
return this.pathFindIn(index + 1, tokens);
}
/**
* If tokens[index] represents an index in an array type field (because
*
* @param index , must be 0 < tokens.length
* @param tokens
* @return
*/
private int tokenAsIndex(int index, String[] tokens) {
// token is either digits (index) or string (label)
try {
return Integer.parseInt(tokens[index]);
} catch (NumberFormatException ex) {
return NoIndex;
}
}
/**
* return the String representation of the current fieldName in the field Set.
* if not fieldName was set then return a verbose explanation of the message.
*
* @return
*/
public String toString() {
if (this.fieldIndex != NoIndex) {
Optional<FieldElement> element = this.messageDefinitions.fieldElementNamed(this.messageName, this.fieldName);
if (!element.isPresent()) {
log.debug("no field element for {} # {}", this.messageName, this.fieldName);
return InvalidPath;
}
UnknownFieldSet.Field list = this.fieldSet.getField(element.get().tag());
return this.toStringOf(element.get().type(), list, this.fieldIndex);
}
if (this.fieldName.length() > 0) {
Optional<FieldElement> element = this.messageDefinitions.fieldElementNamed(this.messageName, this.fieldName);
if (!element.isPresent()) {
log.debug("no field element for {} # {}", this.messageName, this.fieldName);
return InvalidPath;
}
UnknownFieldSet.Field field = this.fieldSet.getField(element.get().tag());
if (element.get().type() instanceof DataType.ScalarType) {
return this.toStringOf((DataType.ScalarType) element.get().type(), field);
}
// non-scalar-type , the path is probably too short
}
return InvalidPath;
}
private String toStringOf(DataType type, UnknownFieldSet.Field list, int index) {
if (type instanceof DataType.ScalarType) {
switch ((DataType.ScalarType) type) {
case INT64:
return String.valueOf(list.getVarintList().get(index));
case STRING:
return list.getLengthDelimitedList().get(index).toStringUtf8();
}
}
return type.toString();
}
/**
* create a new field set by reading bytes from a list of ByteString.
*
* @param lengthDelimited
* @throws IOException
*/
private void readAll(List<ByteString> lengthDelimited) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
for (ByteString each : lengthDelimited) {
each.writeTo(bos);
}
read(bos.toByteArray());
}
/**
* return the String representation of a scalar value.
*
* @param type
* @param field
* @return
*/
private String toStringOf(DataType.ScalarType type, UnknownFieldSet.Field field) {
switch (type) {
case BOOL:
return String.valueOf(field.getVarintList().get(0) == 1);
case INT32:
case INT64:
case UINT32:
case UINT64:
case SINT32:
case SINT64:
return String.valueOf(field.getVarintList().get(0));
case FLOAT:
return String.valueOf(Float.intBitsToFloat(field.getFixed32List().get(0)));
case STRING:
return field.getLengthDelimitedList().get(0).toStringUtf8();
default:
return this.explainField(field);
}
}
/**
* return an explanation of the field ; this is used when the protobufpath is invalid.
*
* @param field
* @return
*/
private String explainField(UnknownFieldSet.Field field) {
if (!field.getFixed32List().isEmpty()) {
if (field.getFixed32List().size() == 1) {
return String.valueOf(field.getFixed32List().get(0)) + "(fixed32)";
}
return field.getFixed32List().toString();
}
if (!field.getFixed64List().isEmpty()) {
if (field.getFixed64List().size() == 1) {
return String.valueOf(field.getFixed64List().get(0)) + "(fixed64)";
}
return field.getFixed64List().toString();
}
if (!field.getGroupList().isEmpty()) {
// TODO
return "(group)";
}
if (!field.getVarintList().isEmpty()) {
if (field.getVarintList().size() == 1) {
return String.valueOf(field.getVarintList().get(0)) + "(varint)";
}
return field.getVarintList().toString();
}
if (!field.getLengthDelimitedList().isEmpty()) {
if (field.getLengthDelimitedList().size() == 1) {
return field.getLengthDelimitedList().get(0).toStringUtf8() + " (length delimited)";
}
return "" + field.getLengthDelimitedList().size() + " (length delimited list size)";
}
return "(can't explain this)";
}
}