MessageUtil.java

  1. /*
  2.  * Copyright 2015-2016 Providence Authors
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.providence.util;

  22. import net.morimekta.providence.PEnumValue;
  23. import net.morimekta.providence.PMessage;
  24. import net.morimekta.providence.PMessageBuilder;
  25. import net.morimekta.providence.PMessageOrBuilder;
  26. import net.morimekta.providence.PType;
  27. import net.morimekta.providence.descriptor.PDescriptor;
  28. import net.morimekta.providence.descriptor.PEnumDescriptor;
  29. import net.morimekta.providence.descriptor.PField;
  30. import net.morimekta.providence.descriptor.PList;
  31. import net.morimekta.providence.descriptor.PMap;
  32. import net.morimekta.providence.descriptor.PMessageDescriptor;
  33. import net.morimekta.providence.descriptor.PSet;
  34. import net.morimekta.util.Binary;

  35. import javax.annotation.Nonnull;
  36. import javax.annotation.Nullable;
  37. import java.util.ArrayList;
  38. import java.util.Arrays;
  39. import java.util.Collection;
  40. import java.util.HashMap;
  41. import java.util.HashSet;
  42. import java.util.List;
  43. import java.util.Locale;
  44. import java.util.Map;
  45. import java.util.Objects;
  46. import java.util.Optional;
  47. import java.util.Set;
  48. import java.util.SortedMap;
  49. import java.util.SortedSet;
  50. import java.util.TreeMap;
  51. import java.util.TreeSet;
  52. import java.util.regex.Pattern;
  53. import java.util.stream.Collectors;

  54. import static java.util.stream.Collectors.toList;
  55. import static net.morimekta.util.Binary.fromBase64;
  56. import static net.morimekta.util.Strings.isNullOrEmpty;

  57. /**
  58.  * Convenience methods for handling providence messages.
  59.  */
  60. public class MessageUtil {
  61.     /**
  62.      * Build all items of the collection containing message-or-builders. The list must not
  63.      * contain any null items.
  64.      *
  65.      * @param builders List of message-or-builders.
  66.      * @param <M> The message type.
  67.      * @param <V> The actual value type.
  68.      * @return List of messages or null if null input.
  69.      */
  70.     public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>>
  71.     List<M> toMessageAll(Collection<V> builders) {
  72.         if (builders == null) {
  73.             return null;
  74.         }
  75.         return builders.stream()
  76.                        .map(PMessageOrBuilder::toMessage)
  77.                        .collect(toList());
  78.     }

  79.     /**
  80.      * Mutate all items of the collection containing messages. The list must not
  81.      * contain any null items.
  82.      *
  83.      * @param messages List of messages
  84.      * @param <M> The message type.
  85.      * @param <V> The actual value type.
  86.      * @param <B> The builder type.
  87.      * @return List of builders or null if null input.
  88.      */
  89.     @SuppressWarnings("unchecked")
  90.     public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
  91.     List<B> toBuilderAll(Collection<V> messages) {
  92.         if (messages == null) {
  93.             return null;
  94.         }
  95.         return messages.stream()
  96.                        .map(mob -> (B) mob.toBuilder())
  97.                        .collect(toList());
  98.     }

  99.     /**
  100.      * Mutate all values of the map containing message-or-builder values. The map must not
  101.      * contain any null items.
  102.      *
  103.      * @param messages Map with message-or-builder values.
  104.      * @param <K> The map key type.
  105.      * @param <M> The message type.
  106.      * @param <V> The actual value type.
  107.      * @param <B> The builder type.
  108.      * @return Map with builder values or null on null input.
  109.      */
  110.     @SuppressWarnings("unchecked")
  111.     public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
  112.     Map<K, B> toBuilderValues(Map<K, V> messages) {
  113.         if (messages == null) {
  114.             return null;
  115.         }
  116.         return messages.entrySet()
  117.                        .stream()
  118.                        .collect(Collectors.toMap(Map.Entry::getKey,
  119.                                       e -> (B) e.getValue().toBuilder()));
  120.     }

  121.     /**
  122.      * Mutate all items of the collection containing messages. The list must not
  123.      * contain any null items.
  124.      *
  125.      * @param messages List of messages
  126.      * @param <K> The map key type.
  127.      * @param <M> The message type.
  128.      * @param <V> The actual value type.
  129.      * @return Map with message values, or null on null input.
  130.      */
  131.     public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>>
  132.     Map<K, M> toMessageValues(Map<K, V> messages) {
  133.         if (messages == null) {
  134.             return null;
  135.         }
  136.         return messages.entrySet()
  137.                        .stream()
  138.                        .collect(Collectors.toMap(Map.Entry::getKey,
  139.                                       e -> e.getValue().toMessage()));
  140.     }

  141.     /**
  142.      * Build the message from builder if it is not null.
  143.      *
  144.      * @param mob The builder to build.
  145.      * @param <M> The message type.
  146.      * @return The message or null if null input.
  147.      */
  148.     public static <M extends PMessage<M>>
  149.     M toMessageIfNotNull(PMessageOrBuilder<M> mob) {
  150.         if (mob == null) {
  151.             return null;
  152.         }
  153.         return mob.toMessage();
  154.     }

  155.     /**
  156.      * Mutate the message if it is not null.
  157.      *
  158.      * @param mob Message or builder to mutate.
  159.      * @param <M> The message type.
  160.      * @param <B> The builder type.
  161.      * @return The builder or null if null input.
  162.      */
  163.     @SuppressWarnings("unchecked")
  164.     public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
  165.     B toBuilderIfNonNull(PMessageOrBuilder<M> mob) {
  166.         if (mob == null) {
  167.             return null;
  168.         }
  169.         return (B) mob.toBuilder();
  170.     }

  171.     /**
  172.      * Casting utility to make into a collection of message-or-builders.
  173.      * This is basically a pure cast, but looks better than doing the cast
  174.      * all over the place.
  175.      *
  176.      * @param items Collection of items to be generic cast.
  177.      * @param <M> Message type.
  178.      * @param <MB> Message-or-builder type.
  179.      * @return The collection of message-or-builder type.
  180.      */
  181.     @SuppressWarnings("unchecked")
  182.     public static <M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
  183.     Collection<PMessageOrBuilder<M>> toMessageOrBuilders(@Nonnull Collection<MB> items) {
  184.         return (Collection<PMessageOrBuilder<M>>) items;
  185.     }

  186.     /**
  187.      * Casting utility to make into a map of message-or-builders.
  188.      * This is basically a pure cast, but looks better than doing the cast
  189.      * all over the place.
  190.      *
  191.      * @param items Map of items to be generic cast.
  192.      * @param <K> Map key type.
  193.      * @param <M> Message type.
  194.      * @param <MB> Message or builder type.
  195.      * @return The map of message-or-builder type.
  196.      */
  197.     @SuppressWarnings("unchecked")
  198.     public static  <K, M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
  199.     Map<K, PMessageOrBuilder<M>> toMessageOrBuilderValues(@Nonnull Map<K, MB> items) {
  200.         return (Map<K, PMessageOrBuilder<M>>) items;
  201.     }

  202.     /**
  203.      * Make a builder of the target message with all differences between
  204.      * source and target marked as modifications.
  205.      *
  206.      * @param source The source message for changes.
  207.      * @param target The message to apply said changes to.
  208.      * @param <M> The message type.
  209.      * @param <B> The builder result type.
  210.      * @return Builder of target with marked modifications.
  211.      */
  212.     @SuppressWarnings("unchecked,rawtypes")
  213.     public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
  214.     B getTargetModifications(PMessageOrBuilder<M> source, PMessageOrBuilder<M> target) {
  215.         B builder;
  216.         if (target instanceof PMessageBuilder) {
  217.             builder = (B) ((PMessageBuilder<M>) target).build().mutate();
  218.         } else {
  219.             builder = (B) ((PMessage<M>) target).mutate();
  220.         }
  221.         for (PField field : source.descriptor().getFields()) {
  222.             if (source.has(field) != target.has(field) ||
  223.                 !Objects.equals(source.get(field), target.get(field))) {
  224.                 builder.set(field, target.get(field));
  225.             }
  226.         }
  227.         return builder;
  228.     }

  229.     /**
  230.      * Convert a key path to a list of consecutive fields for recursive lookup.
  231.      *
  232.      * @param rootDescriptor The root message descriptor.
  233.      * @param key            The '.' joined field name key.
  234.      * @return Array of fields.
  235.      */
  236.     @Nonnull
  237.     public static PField<?>[] keyPathToFields(@Nonnull PMessageDescriptor<?> rootDescriptor, @Nonnull String key) {
  238.         ArrayList<PField<?>> fields = new ArrayList<>();
  239.         String[]          parts  = key.split("\\.", Byte.MAX_VALUE);
  240.         for (int i = 0; i < (parts.length - 1); ++i) {
  241.             String name = parts[i];
  242.             if (name.isEmpty()) {
  243.                 throw new IllegalArgumentException("Empty field name in '" + key + "'");
  244.             }
  245.             PField<?> field = rootDescriptor.findFieldByName(name);
  246.             if (field == null) {
  247.                 throw new IllegalArgumentException(
  248.                         "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
  249.             }
  250.             if (field.getType() != PType.MESSAGE) {
  251.                 throw new IllegalArgumentException(
  252.                         "Field '" + name + "' is not of message type in " + rootDescriptor.getQualifiedName());
  253.             }
  254.             fields.add(field);
  255.             rootDescriptor = (PMessageDescriptor<?>) field.getDescriptor();
  256.         }

  257.         String name = parts[parts.length - 1];
  258.         if (name.isEmpty()) {
  259.             throw new IllegalArgumentException("Empty field name in '" + key + "'");
  260.         }
  261.         PField<?> field = rootDescriptor.findFieldByName(name);
  262.         if (field == null) {
  263.             throw new IllegalArgumentException(
  264.                     "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
  265.         }
  266.         fields.add(field);
  267.         return fields.toArray(new PField[0]);
  268.     }

  269.     /**
  270.      * Append field to the given path.
  271.      *
  272.      * @param fields Fields to make key path of.
  273.      * @return The new appended key path.
  274.      */
  275.     public static String keyPath(@Nonnull PField<?>... fields) {
  276.         if (fields.length == 0) throw new IllegalArgumentException("No field arguments");
  277.         return Arrays.stream(fields).map(PField::getName).collect(Collectors.joining("."));
  278.     }

  279.     /**
  280.      * Append field to the given path.
  281.      *
  282.      * @param path The path to be appended to.
  283.      * @param field The field who's name should be appended.
  284.      * @return The new appended key path.
  285.      */
  286.     public static String keyPathAppend(@Nullable String path, @Nonnull PField<?> field) {
  287.         if (isNullOrEmpty(path)) {
  288.             return field.getName();
  289.         }
  290.         return path + "." + field.getName();
  291.     }

  292.     /**
  293.      * Look up a key in the message structure. If the key is not found, return the
  294.      * default value for that field, and iterate over the fields until the last one.
  295.      * <p>
  296.      * This differs form {@link #optionalInMessage(PMessageOrBuilder, PField...)} by handling
  297.      * the fields' default values.
  298.      * <p>
  299.      * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
  300.      * constant field enums, in that case you should use optional of the getter
  301.      * and map until you have the last value, which should always return the
  302.      * same, but is compile-time type safe. E.g.:
  303.      *
  304.      * <pre>{@code
  305.      * Optional.ofNullable(message.getFirst())
  306.      *         .map(First::getSecond)
  307.      *         .map(Second::getThird)
  308.      *         .orElse(myDefault);
  309.      * }</pre>
  310.      *
  311.      * @param message The message to look up into.
  312.      * @param fields  Field to get in order.
  313.      * @param <T>     The expected leaf value type.
  314.      * @return The value found or null.
  315.      * @throws IllegalArgumentException When unable to get value from message.
  316.      */
  317.     @Nonnull
  318.     @SuppressWarnings("unchecked,rawtypes")
  319.     public static <T> Optional<T> getInMessage(@Nullable PMessageOrBuilder<?> message, @Nonnull PField<?>... fields) {
  320.         if (fields.length == 0) {
  321.             throw new IllegalArgumentException("No fields arguments");
  322.         }
  323.         PField<?> field = fields[0];
  324.         if (fields.length > 1) {
  325.             if (field.getType() != PType.MESSAGE) {
  326.                 throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
  327.             }
  328.             return getInMessage(message == null ?
  329.                                 (PMessage<?>) field.getDefaultValue() :
  330.                                 Optional.ofNullable((PMessage<?>) message.get(field.getId()))
  331.                                         .orElse((PMessage) field.getDefaultValue()),
  332.                                 Arrays.copyOfRange(fields, 1, fields.length));
  333.         } else {
  334.             return Optional.ofNullable(message == null ?
  335.                                        (T) field.getDefaultValue() :
  336.                                        Optional.ofNullable((T) message.get(field.getId()))
  337.                                                .orElseGet(() -> (T) field.getDefaultValue()));
  338.         }
  339.     }

  340.     /**
  341.      * Get a field value from a message with optional chaining. If the field is
  342.      * not set, or any message in the chain leading up to the last message is
  343.      * missing, it will return an empty optional, otherwise the leaf field value.
  344.      * Note that this will only check for MESSAGE type if the message is present
  345.      * and needs to be looked up in.
  346.      * <p>
  347.      * This differs from {@link #getInMessage(PMessageOrBuilder, PField...)} by explicitly
  348.      * <b>NOT</b> handling fields' default values.
  349.      * <p>
  350.      * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
  351.      * constant field enums, in that case you should use the optional getter
  352.      * and flatMap until you have the last value, which should always return
  353.      * the same, but is compile-time type safe. E.g.:
  354.      *
  355.      * <pre>{@code
  356.      * message.optionalFirst()
  357.      *        .flatMap(First::optionalSecond)
  358.      *        .flatMap(Second::optionalThird)
  359.      *        .orElse(myDefault);
  360.      * }</pre>
  361.      *
  362.      * @param message The message to start looking up field values in.
  363.      * @param fields  Fields to look up in the message.
  364.      * @param <T>     The expected leaf value type.
  365.      * @return Optional field value.
  366.      */
  367.     @Nonnull
  368.     @SuppressWarnings("unchecked")
  369.     public static <T> Optional<T> optionalInMessage(
  370.             @Nullable PMessageOrBuilder<?> message,
  371.             @Nonnull PField<?>... fields) {
  372.         if (fields.length == 0) {
  373.             throw new IllegalArgumentException("No fields arguments");
  374.         }
  375.         if (message == null) {
  376.             return Optional.empty();
  377.         }
  378.         @SuppressWarnings("rawtypes")
  379.         PField field = fields[0];
  380.         if (!message.has(field.getId())) {
  381.             return Optional.empty();
  382.         }

  383.         if (fields.length > 1) {
  384.             if (field.getType() != PType.MESSAGE) {
  385.                 throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
  386.             }
  387.             // Required to preserve generic typing.
  388.             return optionalInMessage((PMessage<?>) message.get(field), Arrays.copyOfRange(fields, 1, fields.length));
  389.         } else {
  390.             return Optional.of(message.get(field.getId()));
  391.         }
  392.     }

  393.     /**
  394.      * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
  395.      * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
  396.      * the message, so message fields will also be transformed, and values in maps and collection will too.
  397.      * <p>
  398.      * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
  399.      *
  400.      * @param message The message to be transformed.
  401.      * @return The native map representing the message.
  402.      * @deprecated Use {@link #messageToMap(PMessageOrBuilder)}. Will be removed in future major release.
  403.      *             Function is renamed t
  404.      */
  405.     @Nonnull
  406.     @Deprecated
  407.     public static Map<String, Object> toMap(@Nonnull PMessageOrBuilder<?> message) {
  408.         return messageToMap(message);
  409.     }

  410.     /**
  411.      * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
  412.      * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
  413.      * the message, so message fields will also be transformed, and values in maps and collection will too.
  414.      * <p>
  415.      * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
  416.      *
  417.      * @param message The message to be transformed.
  418.      * @return The native map representing the message.
  419.      */
  420.     @Nonnull
  421.     @SuppressWarnings("unchecked,rawtypes")
  422.     public static Map<String, Object> messageToMap(@Nonnull PMessageOrBuilder<?> message) {
  423.         TreeMap<String, Object> out = new TreeMap<>();
  424.         for (PField field : message.descriptor().getFields()) {
  425.             if (message.has(field)) {
  426.                 switch (field.getType()) {
  427.                     case MESSAGE: {
  428.                         // Required to preserve generic typing.
  429.                         out.put(field.getName(), messageToMap((PMessage<?>) message.get(field)));
  430.                         break;
  431.                     }
  432.                     case SET:
  433.                     case LIST: {
  434.                         // Required to preserve generic typing.
  435.                         out.put(field.getName(), toCollectionInternal((Collection<Object>) message.get(field)));
  436.                         break;
  437.                     }
  438.                     case MAP: {
  439.                         // Required to preserve generic typing.
  440.                         out.put(field.getName(), toMapInternal((Map<Object, Object>) message.get(field)));
  441.                         break;
  442.                     }
  443.                     default: {
  444.                         // Everything else is already using the best representative
  445.                         // native value.
  446.                         out.put(field.getName(), message.get(field));
  447.                         break;
  448.                     }
  449.                 }
  450.             }
  451.         }
  452.         return out;
  453.     }

  454.     /**
  455.      * Coerce value to match the given type descriptor.
  456.      *
  457.      * @param valueType The value type to coerce to.
  458.      * @param value The value to be coerced.
  459.      * @return The coerced value.
  460.      */
  461.     public static Optional<Object> coerce(@Nonnull PDescriptor valueType, Object value) {
  462.         return coerceInternal(valueType, value, false);
  463.     }

  464.     /**
  465.      * Coerce value to match the given type descriptor using struct type
  466.      * checking. This means some loose coercion transitions are not allowed.
  467.      *
  468.      * @param valueType The value type to coerce to.
  469.      * @param value The value to be coerced.
  470.      * @return The coerced value.
  471.      */
  472.     public static Optional<Object> coerceStrict(@Nonnull PDescriptor valueType, Object value) {
  473.         return coerceInternal(valueType, value, true);
  474.     }

  475.     // --------------------
  476.     // ---   INTERNAL   ---
  477.     // --------------------

  478.     @SuppressWarnings("unchecked")
  479.     private static Collection<Object> toCollectionInternal(Collection<Object> collection) {
  480.         Collection<Object> out;
  481.         if (collection instanceof SortedSet) {
  482.             out = new TreeSet<>(((SortedSet<Object>) collection).comparator());
  483.         } else if (collection instanceof Set) {
  484.             out = new HashSet<>();
  485.         } else {
  486.             out = new ArrayList<>();
  487.         }
  488.         for (Object item : collection) {
  489.             if (item instanceof PMessageOrBuilder) {
  490.                 out.add(messageToMap((PMessageOrBuilder<?>) item));
  491.             } else if (item instanceof Collection) {
  492.                 out.add(toCollectionInternal((Collection<Object>) item));
  493.             } else if (item instanceof Map) {
  494.                 out.add(toMapInternal((Map<Object, Object>) item));
  495.             } else {
  496.                 out.add(item);
  497.             }
  498.         }

  499.         return out;
  500.     }

  501.     // Visible for testing.
  502.     @SuppressWarnings("unchecked")
  503.     static Map<Object, Object> toMapInternal(Map<Object, Object> collection) {
  504.         Map<Object, Object> out;
  505.         if (collection instanceof SortedMap) {
  506.             out = new TreeMap<>(((SortedMap<Object, Object>) collection).comparator());
  507.         } else {
  508.             out = new HashMap<>();
  509.         }

  510.         for (Map.Entry<Object, Object> item : collection.entrySet()) {
  511.             Object value;
  512.             if (item.getValue() instanceof PMessageOrBuilder) {
  513.                 value = toMap((PMessageOrBuilder<?>) item.getValue());
  514.             } else if (item.getValue() instanceof Collection) {
  515.                 value = toCollectionInternal((Collection<Object>) item.getValue());
  516.             } else if (item.getValue() instanceof Map) {
  517.                 value = toMapInternal((Map<Object, Object>) item.getValue());
  518.             } else {
  519.                 value = item.getValue();
  520.             }
  521.             // Do not transform the key, as both sorting and equality are easily
  522.             // messed up.
  523.             out.put(item.getKey(), value);
  524.         }

  525.         return out;
  526.     }

  527.     @SuppressWarnings("unchecked")
  528.     private static Optional<Object> coerceInternal(@Nonnull PDescriptor valueType, Object val, boolean strict) {
  529.         if (val == null) {
  530.             return Optional.empty();
  531.         }
  532.         switch (valueType.getType()) {
  533.             case VOID:
  534.                 // Void does'nt really care about the value.
  535.                 if (val == Boolean.TRUE) {
  536.                     return Optional.of(Boolean.TRUE);
  537.                 } else if (val == Boolean.FALSE) {
  538.                     throw new IllegalArgumentException("Invalid void value " + val.toString());
  539.                 }
  540.                 break;
  541.             case BOOL:
  542.                 if (val instanceof Boolean) {
  543.                     return Optional.of(val);
  544.                 } else if (val instanceof Number && !(val instanceof Float) && !(val instanceof Double)) {
  545.                     return Optional.of(((Number) val).longValue() != 0L);
  546.                 } else if (val instanceof PEnumValue) {  // e.g. enum
  547.                     return Optional.of(((PEnumValue<?>) val).asInteger() != 0);
  548.                 } else if (strict) {
  549.                     break;
  550.                 } else if (val instanceof CharSequence) {
  551.                     switch (val.toString().toLowerCase(Locale.US)) {
  552.                         case "true":
  553.                         case "t":
  554.                         case "yes":
  555.                         case "y":
  556.                         case "1":
  557.                             return Optional.of(Boolean.TRUE);
  558.                         case "false":
  559.                         case "f":
  560.                         case "no":
  561.                         case "n":
  562.                         case "0":
  563.                             return Optional.of(Boolean.FALSE);
  564.                     }
  565.                     throw new IllegalArgumentException("Unknown boolean value for string '" + val + "'");
  566.                 }
  567.                 break;
  568.             case BYTE:
  569.                 if (val instanceof Number) {
  570.                     return Optional.of((byte) asInteger(valueType, (Number) val, Byte.MIN_VALUE, Byte.MAX_VALUE));
  571.                 } else if (val instanceof Boolean) {
  572.                     return Optional.of((Boolean) val ? (byte) 1 : (byte) 0);
  573.                 } else if (val instanceof PEnumValue) {  // e.g. enum
  574.                     return Optional.of((byte) asInteger(valueType,
  575.                                                         ((PEnumValue<?>) val).asInteger(),
  576.                                                         Byte.MIN_VALUE,
  577.                                                         Byte.MAX_VALUE));
  578.                 } else if (strict) {
  579.                     break;
  580.                 } else if (val instanceof CharSequence) {
  581.                     try {
  582.                         CharSequence cs = (CharSequence) val;
  583.                         if (HEX.matcher(cs).matches()) {
  584.                             return Optional.of(Byte.parseByte(cs.subSequence(2, cs.length()).toString(), 16));
  585.                         } else {
  586.                             return Optional.of(Byte.parseByte(val.toString()));
  587.                         }
  588.                     } catch (NumberFormatException e) {
  589.                         throw new IllegalArgumentException("Invalid string value '" + val + "' for type byte", e);
  590.                     }
  591.                 }
  592.                 break;
  593.             case I16:
  594.                 if (val instanceof Number) {
  595.                     return Optional.of((short) asInteger(valueType, (Number) val, Short.MIN_VALUE, Short.MAX_VALUE));
  596.                 } else if (val instanceof Boolean) {
  597.                     return Optional.of((Boolean) val ? (short) 1 : (short) 0);
  598.                 } else if (val instanceof PEnumValue) {  // e.g. enum
  599.                     return Optional.of((short) asInteger(valueType,
  600.                                                          ((PEnumValue<?>) val).asInteger(),
  601.                                                          Short.MIN_VALUE,
  602.                                                          Short.MAX_VALUE));
  603.                 } else if (strict) {
  604.                     break;
  605.                 } else if (val instanceof CharSequence) {
  606.                     try {
  607.                         CharSequence cs = (CharSequence) val;
  608.                         if (HEX.matcher(cs).matches()) {
  609.                             return Optional.of(Short.parseShort(cs.subSequence(2, cs.length()).toString(), 16));
  610.                         } else {
  611.                             return Optional.of(Short.parseShort(val.toString()));
  612.                         }
  613.                     } catch (NumberFormatException e) {
  614.                         throw new IllegalArgumentException("Invalid string value '" + val + "' for type i16", e);
  615.                     }
  616.                 }
  617.                 break;
  618.             case I32:
  619.                 if (val instanceof Number) {
  620.                     return Optional.of(asInteger(valueType, (Number) val, Integer.MIN_VALUE, Integer.MAX_VALUE));
  621.                 } else if (val instanceof Boolean) {
  622.                     return Optional.of((Boolean) val ? 1 : 0);
  623.                 } else if (val instanceof PEnumValue) {  // e.g. enum
  624.                     return Optional.of(((PEnumValue<?>) val).asInteger());
  625.                 } else if (strict) {
  626.                     break;
  627.                 } else if (val instanceof CharSequence) {
  628.                     try {
  629.                         CharSequence cs = (CharSequence) val;
  630.                         if (HEX.matcher(cs).matches()) {
  631.                             return Optional.of(Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16));
  632.                         } else {
  633.                             return Optional.of(Integer.parseInt(val.toString()));
  634.                         }
  635.                     } catch (NumberFormatException e) {
  636.                         throw new IllegalArgumentException("Invalid string value '" + val + "' for type i32", e);
  637.                     }
  638.                 }
  639.                 break;
  640.             case I64:
  641.                 if (val instanceof Float || val instanceof Double) {
  642.                     long l = ((Number) val).longValue();
  643.                     if ((double) l != ((Number) val).doubleValue()) {
  644.                         throw new IllegalArgumentException("Truncating long decimals from " + val.toString());
  645.                     }
  646.                     return Optional.of(l);
  647.                 } else if (val instanceof Number) {
  648.                     return Optional.of(((Number) val).longValue());
  649.                 } else if (val instanceof Boolean) {
  650.                     return Optional.of((Boolean) val ? 1L : 0L);
  651.                 } else if (val instanceof PEnumValue) {  // e.g. enum
  652.                     return Optional.of((long) ((PEnumValue<?>) val).asInteger());
  653.                 } else if (strict) {
  654.                     break;
  655.                 } else if (val instanceof CharSequence) {
  656.                     try {
  657.                         CharSequence cs = (CharSequence) val;
  658.                         if (HEX.matcher(cs).matches()) {
  659.                             return Optional.of(Long.parseLong(cs.subSequence(2, cs.length()).toString(), 16));
  660.                         } else {
  661.                             return Optional.of(Long.parseLong(val.toString()));
  662.                         }
  663.                     } catch (NumberFormatException e) {
  664.                         throw new IllegalArgumentException("Invalid string value '" + val + "' for type i64", e);
  665.                     }
  666.                 }
  667.                 break;
  668.             case DOUBLE:
  669.                 if (val instanceof Number) {
  670.                     return Optional.of(((Number) val).doubleValue());
  671.                 } else if (strict) {
  672.                     break;
  673.                 } else if (val instanceof PEnumValue) {
  674.                     return Optional.of((double) ((PEnumValue<?>) val).asInteger());
  675.                 }
  676.                 break;
  677.             case STRING:
  678.                 if (val instanceof CharSequence) {
  679.                     return Optional.of(val.toString());
  680.                 } else if (strict) {
  681.                     break;
  682.                 } else if (val instanceof PEnumValue) {
  683.                     return Optional.of(((PEnumValue<?>) val).asString());
  684.                 } else {
  685.                     return Optional.of(val.toString());
  686.                 }
  687.             case BINARY:
  688.                 if (val instanceof Binary) {
  689.                     return Optional.of(val);
  690.                 } else if (strict) {
  691.                     break;
  692.                 } else if (val instanceof CharSequence) {
  693.                     return Optional.of(fromBase64(val.toString()));
  694.                 }
  695.                 break;
  696.             case ENUM: {
  697.                 PEnumDescriptor<?> ed = (PEnumDescriptor<?>) valueType;
  698.                 if (val instanceof PEnumValue) {
  699.                     PEnumValue<?> verified = ((PEnumDescriptor<?>) valueType).findById(((PEnumValue<?>) val).asInteger());
  700.                     if (val.equals(verified)) {
  701.                         return Optional.of(verified);
  702.                     }
  703.                 } else if (val instanceof Number && !(val instanceof Double) && !(val instanceof Float)) {
  704.                     int        i  = ((Number) val).intValue();
  705.                     PEnumValue<?> ev = ed.findById(i);
  706.                     if (ev != null) {
  707.                         return Optional.of(ev);
  708.                     }
  709.                     throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
  710.                                                        " value for id " + val.toString());
  711.                 } else if (val instanceof CharSequence) {
  712.                     CharSequence cs = (CharSequence) val;
  713.                     if (!strict && UNSIGNED.matcher(cs).matches()) {
  714.                         int        i  = Integer.parseInt(val.toString());
  715.                         PEnumValue<?> ev = ed.findById(i);
  716.                         if (ev != null) {
  717.                             return Optional.of(ev);
  718.                         }
  719.                     } else if (!strict && HEX.matcher(cs).matches()) {
  720.                         int        i  = Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16);
  721.                         PEnumValue<?> ev = ed.findById(i);
  722.                         if (ev != null) {
  723.                             return Optional.of(ev);
  724.                         }
  725.                     } else {
  726.                         PEnumValue<?> ev = ed.findByName(val.toString());
  727.                         if (ev != null) {
  728.                             return Optional.of(ev);
  729.                         }
  730.                     }
  731.                     throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
  732.                                                        " value for string '" + val.toString() + "'");
  733.                 }
  734.                 throw new IllegalArgumentException(
  735.                         "Invalid value type " + val.getClass() + " for enum " + valueType.toString());
  736.             }
  737.             case MESSAGE: {
  738.                 if (val instanceof PMessage) {
  739.                     if (valueType.equals(((PMessage<?>) val).descriptor())) {
  740.                         return Optional.of(val);
  741.                     } else {
  742.                         throw new IllegalArgumentException(
  743.                                 "Unable to cast message type " +
  744.                                 ((PMessage<?>) val).descriptor().getQualifiedName() + " to " +
  745.                                 valueType.getQualifiedName());

  746.                     }
  747.                 } else if (val instanceof PMessageBuilder) {
  748.                     if (valueType.equals(((PMessageBuilder<?>) val).descriptor())) {
  749.                         return Optional.of(((PMessageBuilder<?>) val).build());
  750.                     } else {
  751.                         throw new IllegalArgumentException(
  752.                                 "Unable to cast message type " +
  753.                                 ((PMessageBuilder<?>) val).descriptor().getQualifiedName() + " to " +
  754.                                 valueType.getQualifiedName());
  755.                     }
  756.                 } else if (!strict && val instanceof Map) {
  757.                     PMessageDescriptor<?> md      = (PMessageDescriptor<?>) valueType;
  758.                     PMessageBuilder<?>    builder = md.builder();
  759.                     for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
  760.                         if (!(entry.getKey() instanceof CharSequence)) {
  761.                             throw new IllegalArgumentException("Invalid message map key: " + entry.getKey().toString());
  762.                         }
  763.                         PField<?> field = md.findFieldByName(entry.getKey().toString());
  764.                         if (field == null) {
  765.                             throw new IllegalArgumentException(
  766.                                     "No such field " + entry.getKey() + " in " + md.getQualifiedName());
  767.                         }
  768.                         builder.set(field.getId(),
  769.                                     coerceInternal(field.getDescriptor(), entry.getValue(), false).orElse(null));
  770.                     }
  771.                     return Optional.of(builder.build());
  772.                 }
  773.                 throw new IllegalArgumentException(
  774.                         "Invalid value type " + val.getClass() + " for message " + valueType.toString());
  775.             }
  776.             case LIST: {
  777.                 if (val instanceof Collection) {
  778.                     PList<Object>         pl      = (PList<Object>) valueType;
  779.                     PList.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
  780.                     for (Object o : (Collection<?>) val) {
  781.                         Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
  782.                         if (value != null) {
  783.                             builder.add(value);
  784.                         } else if (strict) {
  785.                             throw new IllegalArgumentException("Null value in list");
  786.                         }
  787.                     }
  788.                     return Optional.of(builder.build());
  789.                 }
  790.                 throw new IllegalArgumentException(
  791.                         "Invalid value type " + val.getClass() + " for " + valueType.toString());
  792.             }
  793.             case SET:
  794.                 if (val instanceof Collection) {
  795.                     PSet<Object>         pl      = (PSet<Object>) valueType;
  796.                     PSet.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
  797.                     for (Object o : (Collection<?>) val) {
  798.                         Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
  799.                         if (value != null) {
  800.                             builder.add(value);
  801.                         } else if (strict) {
  802.                             throw new IllegalArgumentException("Null value in set");
  803.                         }
  804.                     }
  805.                     return Optional.of(builder.build());
  806.                 }
  807.                 throw new IllegalArgumentException(
  808.                         "Invalid value type " + val.getClass() + " for " + valueType.toString());
  809.             case MAP:
  810.                 if (val instanceof Map) {
  811.                     PMap<Object, Object>         pl      = (PMap<Object, Object>) valueType;
  812.                     PMap.Builder<Object, Object> builder = pl.builder(((Map<?,?>) val).size());
  813.                     for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
  814.                         Object key   = coerceInternal(pl.keyDescriptor(), entry.getKey(), strict).orElse(null);
  815.                         Object value = coerceInternal(pl.itemDescriptor(), entry.getValue(), strict).orElse(null);
  816.                         if (key != null && value != null) {
  817.                             builder.put(key, value);
  818.                         } else if (strict) {
  819.                             throw new IllegalArgumentException("Null key or value in map");
  820.                         }
  821.                     }
  822.                     return Optional.of(builder.build());
  823.                 }
  824.                 throw new IllegalArgumentException(
  825.                         "Invalid value type " + val.getClass() + " for " + valueType.toString());
  826.         }
  827.         throw new IllegalArgumentException("Invalid value type " + val.getClass() + " for type " + valueType.getType());
  828.     }

  829.     private static int asInteger(PDescriptor descriptor, Number value, int min, int max) {
  830.         if (value instanceof Float || value instanceof Double) {
  831.             long l = value.longValue();
  832.             if ((double) l != value.doubleValue()) {
  833.                 throw new IllegalArgumentException(
  834.                         "Truncating " + descriptor.getName() + " decimals from " + value.toString());
  835.             }
  836.             return validateInRange(descriptor.getName(), l, min, max);
  837.         } else {
  838.             return validateInRange(descriptor.getName(), value.longValue(), min, max);
  839.         }
  840.     }

  841.     private static int validateInRange(String type, long l, int min, int max) throws IllegalArgumentException {
  842.         if (l < min) {
  843.             throw new IllegalArgumentException(type + " value outside of bounds: " + l + " < " + min);
  844.         } else if (l > max) {
  845.             throw new IllegalArgumentException(type + " value outside of bounds: " + l + " > " + max);
  846.         }
  847.         return (int) l;
  848.     }

  849.     private static final Pattern UNSIGNED = Pattern.compile("(0|[1-9][0-9]*)");
  850.     private static final Pattern HEX      = Pattern.compile("0x[0-9a-fA-F]+");
  851.     private MessageUtil() {}
  852. }