Exemple #1
0
  @Test
  public void structuredAndUnstructuredOptions() throws Exception {
    // From https://developers.google.com/protocol-buffers/docs/proto#options
    Schema schema =
        new SchemaBuilder()
            .add(
                "foo.proto",
                ""
                    + "import \"google/protobuf/descriptor.proto\";\n"
                    + "message FooOptions {\n"
                    + "  optional int32 opt1 = 1;\n"
                    + "  optional string opt2 = 2;\n"
                    + "}\n"
                    + "\n"
                    + "extend google.protobuf.FieldOptions {\n"
                    + "  optional FooOptions foo_options = 1234;\n"
                    + "}\n"
                    + "\n"
                    + "message Bar {\n"
                    + "  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = \"baz\"];\n"
                    + "  optional int32 b = 2 [(foo_options) = { opt1: 456 opt2: \"quux\" }];\n"
                    + "}\n")
            .add("google/protobuf/descriptor.proto")
            .build();

    ProtoMember fooOptions = ProtoMember.get(Options.FIELD_OPTIONS, "foo_options");
    ProtoMember opt1 = ProtoMember.get(ProtoType.get("FooOptions"), "opt1");
    ProtoMember opt2 = ProtoMember.get(ProtoType.get("FooOptions"), "opt2");

    MessageType bar = (MessageType) schema.getType("Bar");
    assertThat(bar.field("a").options().map())
        .isEqualTo(ImmutableMap.of(fooOptions, ImmutableMap.of(opt1, "123", opt2, "baz")));
    assertThat(bar.field("b").options().map())
        .isEqualTo(ImmutableMap.of(fooOptions, ImmutableMap.of(opt1, "456", opt2, "quux")));
  }
Exemple #2
0
 @Test
 public void fullyQualifiedOptionFields() throws Exception {
   Schema schema =
       new SchemaBuilder()
           .add(
               "a/b/more_options.proto",
               ""
                   + "syntax = \"proto2\";\n"
                   + "package a.b;\n"
                   + "\n"
                   + "import \"google/protobuf/descriptor.proto\";\n"
                   + "\n"
                   + "extend google.protobuf.MessageOptions {\n"
                   + "  optional MoreOptions more_options = 17000;\n"
                   + "}\n"
                   + "\n"
                   + "message MoreOptions {\n"
                   + "  extensions 100 to 200;\n"
                   + "}\n")
           .add(
               "a/c/event_more_options.proto",
               ""
                   + "syntax = \"proto2\";\n"
                   + "package a.c;\n"
                   + "\n"
                   + "import \"a/b/more_options.proto\";\n"
                   + "\n"
                   + "extend a.b.MoreOptions {\n"
                   + "  optional EvenMoreOptions even_more_options = 100;\n"
                   + "}\n"
                   + "\n"
                   + "message EvenMoreOptions {\n"
                   + "  optional string string_option = 1;\n"
                   + "}\n")
           .add(
               "a/d/message.proto",
               ""
                   + "syntax = \"proto2\";\n"
                   + "package a.d;\n"
                   + "\n"
                   + "import \"a/b/more_options.proto\";\n"
                   + "import \"a/c/event_more_options.proto\";\n"
                   + "\n"
                   + "message Message {\n"
                   + "  option (a.b.more_options) = {\n"
                   + "    [a.c.even_more_options]: {string_option: \"foo\"}\n"
                   + "  };\n"
                   + "}\n")
           .add("google/protobuf/descriptor.proto")
           .build();
   ProtoType moreOptionsType = ProtoType.get("a.b.MoreOptions");
   ProtoType evenMoreOptionsType = ProtoType.get("a.c.EvenMoreOptions");
   ProtoMember moreOptions = ProtoMember.get(Options.MESSAGE_OPTIONS, "a.b.more_options");
   ProtoMember evenMoreOptions = ProtoMember.get(moreOptionsType, "a.c.even_more_options");
   ProtoMember stringOption = ProtoMember.get(evenMoreOptionsType, "string_option");
   MessageType message = (MessageType) schema.getType("a.d.Message");
   assertThat(message.options().map())
       .isEqualTo(
           ImmutableMap.of(
               moreOptions,
               ImmutableMap.of(evenMoreOptions, ImmutableMap.of(stringOption, "foo"))));
 }
Exemple #3
0
/**
 * A set of options declared on a message declaration, field declaration, enum declaration, enum
 * constant declaration, service declaration, RPC method declaration, or proto file declaration.
 * Options values may be arbitrary protocol buffer messages, but must be valid protocol buffer
 * messages.
 */
public final class Options {
  public static final ProtoType FILE_OPTIONS = ProtoType.get("google.protobuf.FileOptions");
  public static final ProtoType MESSAGE_OPTIONS = ProtoType.get("google.protobuf.MessageOptions");
  public static final ProtoType FIELD_OPTIONS = ProtoType.get("google.protobuf.FieldOptions");
  public static final ProtoType ENUM_OPTIONS = ProtoType.get("google.protobuf.EnumOptions");
  public static final ProtoType ENUM_VALUE_OPTIONS =
      ProtoType.get("google.protobuf.EnumValueOptions");
  public static final ProtoType SERVICE_OPTIONS = ProtoType.get("google.protobuf.ServiceOptions");
  public static final ProtoType METHOD_OPTIONS = ProtoType.get("google.protobuf.MethodOptions");

  private final ProtoType optionType;
  private final ImmutableList<OptionElement> optionElements;
  private ImmutableMap<ProtoMember, Object> map;

  public Options(ProtoType optionType, List<OptionElement> elements) {
    this.optionType = optionType;
    this.optionElements = ImmutableList.copyOf(elements);
  }

  /**
   * Returns a map with the values for these options. Map values may be either a single entry, like
   * {@code {deprecated: "true"}}, or more sophisticated, with nested maps and lists.
   *
   * <p>The map keys are always {@link ProtoMember} instances, even for nested maps. The values are
   * always either lists, maps, or strings.
   */
  public Map<ProtoMember, Object> map() {
    return map;
  }

  public Object get(ProtoMember protoMember) {
    checkNotNull(protoMember, "protoMember");
    return map.get(protoMember);
  }

  /**
   * Returns true if any of the options in {@code options} matches both of the regular expressions
   * provided: its name matches the option's name and its value matches the option's value.
   */
  public boolean optionMatches(String namePattern, String valuePattern) {
    Matcher nameMatcher = Pattern.compile(namePattern).matcher("");
    Matcher valueMatcher = Pattern.compile(valuePattern).matcher("");
    for (Map.Entry<ProtoMember, Object> entry : map.entrySet()) {
      if (nameMatcher.reset(entry.getKey().member()).matches()
          && valueMatcher.reset(String.valueOf(entry.getValue())).matches()) {
        return true;
      }
    }
    return false;
  }

  ImmutableList<OptionElement> toElements() {
    return optionElements;
  }

  void link(Linker linker) {
    ImmutableMap<ProtoMember, Object> map = ImmutableMap.of();
    for (OptionElement option : optionElements) {
      Map<ProtoMember, Object> canonicalOption = canonicalizeOption(linker, optionType, option);
      if (canonicalOption != null) {
        map = union(linker, map, canonicalOption);
      }
    }

    this.map = map;
  }

  Map<ProtoMember, Object> canonicalizeOption(
      Linker linker, ProtoType extensionType, OptionElement option) {
    Type type = linker.get(extensionType);
    if (!(type instanceof MessageType)) {
      return null; // No known extensions for the given extension type.
    }
    MessageType messageType = (MessageType) type;

    String[] path;
    Field field = messageType.field(option.name());
    if (field != null) {
      // This is an option declared by descriptor.proto.
      path = new String[] {option.name()};
    } else {
      // This is an option declared by an extension.
      Map<String, Field> extensionsForType = messageType.extensionFieldsMap();

      path = resolveFieldPath(option.name(), extensionsForType.keySet());
      String packageName = linker.packageName();
      if (path == null && packageName != null) {
        // If the path couldn't be resolved, attempt again by prefixing it with the package name.
        path = resolveFieldPath(packageName + "." + option.name(), extensionsForType.keySet());
      }
      if (path == null) {
        return null; // Unable to find the root of this field path.
      }

      field = extensionsForType.get(path[0]);
    }

    Map<ProtoMember, Object> result = new LinkedHashMap<>();
    Map<ProtoMember, Object> last = result;
    ProtoType lastProtoType = messageType.type();
    for (int i = 1; i < path.length; i++) {
      Map<ProtoMember, Object> nested = new LinkedHashMap<>();
      last.put(ProtoMember.get(lastProtoType, field), nested);
      lastProtoType = field.type();
      last = nested;
      field = linker.dereference(field, path[i]);
      if (field == null) {
        return null; // Unable to dereference this path segment.
      }
    }

    last.put(
        ProtoMember.get(lastProtoType, field), canonicalizeValue(linker, field, option.value()));
    return result;
  }

  /**
   * Given a path like {@code a.b.c.d} and a set of paths like {@code {a.b.c, a.f.g, h.j}}, this
   * returns the original path split on dots such that the first element is in the set. For the
   * above example it would return the array {@code [a.b.c, d]}.
   *
   * <p>Typically the input path is a package name like {@code a.b}, followed by a dot and a
   * sequence of field names. The first field name is an extension field; subsequent field names
   * make a path within that extension.
   *
   * <p>Note that a single input may yield multiple possible answers, such as when package names and
   * field names collide. This method prefers shorter package names though that is an implementation
   * detail.
   */
  static String[] resolveFieldPath(String name, Set<String> fullyQualifiedNames) {
    // Try to resolve a local name.
    for (int i = 0; i < name.length(); i++) {
      i = name.indexOf('.', i);
      if (i == -1) i = name.length();

      String candidate = name.substring(0, i);
      if (fullyQualifiedNames.contains(candidate)) {
        String[] path = name.substring(i).split("\\.", -1);
        path[0] = name.substring(0, i);
        return path;
      }
    }

    return null;
  }

  private Object canonicalizeValue(Linker linker, Field context, Object value) {
    if (value instanceof OptionElement) {
      ImmutableMap.Builder<ProtoMember, Object> result = ImmutableMap.builder();
      OptionElement option = (OptionElement) value;
      Field field = linker.dereference(context, option.name());
      if (field == null) {
        linker.addError("unable to resolve option %s on %s", option.name(), context.type());
      } else {
        ProtoMember protoMember = ProtoMember.get(context.type(), field);
        result.put(protoMember, canonicalizeValue(linker, field, option.value()));
      }
      return coerceValueForField(context, result.build());
    }

    if (value instanceof Map) {
      ImmutableMap.Builder<ProtoMember, Object> result = ImmutableMap.builder();
      for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
        String name = (String) entry.getKey();
        Field field = linker.dereference(context, name);
        if (field == null) {
          linker.addError("unable to resolve option %s on %s", name, context.type());
        } else {
          ProtoMember protoMember = ProtoMember.get(context.type(), field);
          result.put(protoMember, canonicalizeValue(linker, field, entry.getValue()));
        }
      }
      return coerceValueForField(context, result.build());
    }

    if (value instanceof List) {
      ImmutableList.Builder<Object> result = ImmutableList.builder();
      for (Object element : (List<?>) value) {
        result.addAll((List) canonicalizeValue(linker, context, element));
      }
      return coerceValueForField(context, result.build());
    }

    if (value instanceof String) {
      return coerceValueForField(context, value);
    }

    throw new IllegalArgumentException("Unexpected option value: " + value);
  }

  private Object coerceValueForField(Field context, Object value) {
    if (context.isRepeated()) {
      return value instanceof List ? value : ImmutableList.of(value);
    } else {
      return value instanceof List ? getOnlyElement((List) value) : value;
    }
  }

  /** Combine values for the same key, resolving conflicts based on their type. */
  @SuppressWarnings("unchecked")
  private Object union(Linker linker, Object a, Object b) {
    if (a instanceof List) {
      return union((List<?>) a, (List<?>) b);
    } else if (a instanceof Map) {
      return union(linker, (Map<ProtoMember, Object>) a, (Map<ProtoMember, Object>) b);
    } else {
      linker.addError("conflicting options: %s, %s", a, b);
      return a; // Just return any placeholder.
    }
  }

  private ImmutableMap<ProtoMember, Object> union(
      Linker linker, Map<ProtoMember, Object> a, Map<ProtoMember, Object> b) {
    Map<ProtoMember, Object> result = new LinkedHashMap<>(a);
    for (Map.Entry<ProtoMember, Object> entry : b.entrySet()) {
      Object aValue = result.get(entry.getKey());
      Object bValue = entry.getValue();
      Object union = aValue != null ? union(linker, aValue, bValue) : bValue;
      result.put(entry.getKey(), union);
    }
    return ImmutableMap.copyOf(result);
  }

  private ImmutableList<Object> union(List<?> a, List<?> b) {
    return ImmutableList.builder().addAll(a).addAll(b).build();
  }

  Multimap<ProtoType, ProtoMember> fields() {
    Multimap<ProtoType, ProtoMember> result = LinkedHashMultimap.create();
    gatherFields(result, optionType, map);
    return result;
  }

  private void gatherFields(Multimap<ProtoType, ProtoMember> sink, ProtoType type, Object o) {
    if (o instanceof Map) {
      for (Map.Entry<?, ?> entry : ((Map<?, ?>) o).entrySet()) {
        ProtoMember protoMember = (ProtoMember) entry.getKey();
        sink.put(type, protoMember);
        gatherFields(sink, protoMember.type(), entry.getValue());
      }
    } else if (o instanceof List) {
      for (Object e : (List) o) {
        gatherFields(sink, type, e);
      }
    }
  }

  Options retainAll(Schema schema, MarkSet markSet) {
    if (map.isEmpty()) return this; // Nothing to prune.
    Options result = new Options(optionType, optionElements);
    Object mapOrNull = retainAll(schema, markSet, optionType, map);
    result.map =
        mapOrNull != null
            ? (ImmutableMap<ProtoMember, Object>) mapOrNull
            : ImmutableMap.<ProtoMember, Object>of();
    return result;
  }

  /** Returns an object of the same type as {@code o}, or null if it is not retained. */
  private Object retainAll(Schema schema, MarkSet markSet, ProtoType type, Object o) {
    if (!markSet.contains(type)) {
      return null; // Prune this type.

    } else if (o instanceof Map) {
      ImmutableMap.Builder<ProtoMember, Object> builder = ImmutableMap.builder();
      for (Map.Entry<?, ?> entry : ((Map<?, ?>) o).entrySet()) {
        ProtoMember protoMember = (ProtoMember) entry.getKey();
        if (!markSet.contains(protoMember)) continue; // Prune this field.
        Field field = schema.getField(protoMember);
        Object retainedValue = retainAll(schema, markSet, field.type(), entry.getValue());
        if (retainedValue != null) {
          builder.put(protoMember, retainedValue); // This retained field is non-empty.
        }
      }
      ImmutableMap<ProtoMember, Object> map = builder.build();
      return !map.isEmpty() ? map : null;

    } else if (o instanceof List) {
      ImmutableList.Builder<Object> builder = ImmutableList.builder();
      for (Object value : ((List) o)) {
        Object retainedValue = retainAll(schema, markSet, type, value);
        if (retainedValue != null) {
          builder.add(retainedValue); // This retained value is non-empty.
        }
      }
      ImmutableList<Object> list = builder.build();
      return !list.isEmpty() ? list : null;

    } else {
      return o;
    }
  }
}