MessageUtil.java
- /*
- * Copyright 2015-2016 Providence Authors
- *
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 net.morimekta.providence.util;
- import net.morimekta.providence.PEnumValue;
- import net.morimekta.providence.PMessage;
- import net.morimekta.providence.PMessageBuilder;
- import net.morimekta.providence.PMessageOrBuilder;
- import net.morimekta.providence.PType;
- import net.morimekta.providence.descriptor.PDescriptor;
- import net.morimekta.providence.descriptor.PEnumDescriptor;
- import net.morimekta.providence.descriptor.PField;
- import net.morimekta.providence.descriptor.PList;
- import net.morimekta.providence.descriptor.PMap;
- import net.morimekta.providence.descriptor.PMessageDescriptor;
- import net.morimekta.providence.descriptor.PSet;
- import net.morimekta.util.Binary;
- import javax.annotation.Nonnull;
- import javax.annotation.Nullable;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Objects;
- import java.util.Optional;
- import java.util.Set;
- import java.util.SortedMap;
- import java.util.SortedSet;
- import java.util.TreeMap;
- import java.util.TreeSet;
- import java.util.regex.Pattern;
- import java.util.stream.Collectors;
- import static java.util.stream.Collectors.toList;
- import static net.morimekta.util.Binary.fromBase64;
- import static net.morimekta.util.Strings.isNullOrEmpty;
- /**
- * Convenience methods for handling providence messages.
- */
- public class MessageUtil {
- /**
- * Build all items of the collection containing message-or-builders. The list must not
- * contain any null items.
- *
- * @param builders List of message-or-builders.
- * @param <M> The message type.
- * @param <V> The actual value type.
- * @return List of messages or null if null input.
- */
- public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>>
- List<M> toMessageAll(Collection<V> builders) {
- if (builders == null) {
- return null;
- }
- return builders.stream()
- .map(PMessageOrBuilder::toMessage)
- .collect(toList());
- }
- /**
- * Mutate all items of the collection containing messages. The list must not
- * contain any null items.
- *
- * @param messages List of messages
- * @param <M> The message type.
- * @param <V> The actual value type.
- * @param <B> The builder type.
- * @return List of builders or null if null input.
- */
- @SuppressWarnings("unchecked")
- public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
- List<B> toBuilderAll(Collection<V> messages) {
- if (messages == null) {
- return null;
- }
- return messages.stream()
- .map(mob -> (B) mob.toBuilder())
- .collect(toList());
- }
- /**
- * Mutate all values of the map containing message-or-builder values. The map must not
- * contain any null items.
- *
- * @param messages Map with message-or-builder values.
- * @param <K> The map key type.
- * @param <M> The message type.
- * @param <V> The actual value type.
- * @param <B> The builder type.
- * @return Map with builder values or null on null input.
- */
- @SuppressWarnings("unchecked")
- public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
- Map<K, B> toBuilderValues(Map<K, V> messages) {
- if (messages == null) {
- return null;
- }
- return messages.entrySet()
- .stream()
- .collect(Collectors.toMap(Map.Entry::getKey,
- e -> (B) e.getValue().toBuilder()));
- }
- /**
- * Mutate all items of the collection containing messages. The list must not
- * contain any null items.
- *
- * @param messages List of messages
- * @param <K> The map key type.
- * @param <M> The message type.
- * @param <V> The actual value type.
- * @return Map with message values, or null on null input.
- */
- public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>>
- Map<K, M> toMessageValues(Map<K, V> messages) {
- if (messages == null) {
- return null;
- }
- return messages.entrySet()
- .stream()
- .collect(Collectors.toMap(Map.Entry::getKey,
- e -> e.getValue().toMessage()));
- }
- /**
- * Build the message from builder if it is not null.
- *
- * @param mob The builder to build.
- * @param <M> The message type.
- * @return The message or null if null input.
- */
- public static <M extends PMessage<M>>
- M toMessageIfNotNull(PMessageOrBuilder<M> mob) {
- if (mob == null) {
- return null;
- }
- return mob.toMessage();
- }
- /**
- * Mutate the message if it is not null.
- *
- * @param mob Message or builder to mutate.
- * @param <M> The message type.
- * @param <B> The builder type.
- * @return The builder or null if null input.
- */
- @SuppressWarnings("unchecked")
- public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
- B toBuilderIfNonNull(PMessageOrBuilder<M> mob) {
- if (mob == null) {
- return null;
- }
- return (B) mob.toBuilder();
- }
- /**
- * Casting utility to make into a collection of message-or-builders.
- * This is basically a pure cast, but looks better than doing the cast
- * all over the place.
- *
- * @param items Collection of items to be generic cast.
- * @param <M> Message type.
- * @param <MB> Message-or-builder type.
- * @return The collection of message-or-builder type.
- */
- @SuppressWarnings("unchecked")
- public static <M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
- Collection<PMessageOrBuilder<M>> toMessageOrBuilders(@Nonnull Collection<MB> items) {
- return (Collection<PMessageOrBuilder<M>>) items;
- }
- /**
- * Casting utility to make into a map of message-or-builders.
- * This is basically a pure cast, but looks better than doing the cast
- * all over the place.
- *
- * @param items Map of items to be generic cast.
- * @param <K> Map key type.
- * @param <M> Message type.
- * @param <MB> Message or builder type.
- * @return The map of message-or-builder type.
- */
- @SuppressWarnings("unchecked")
- public static <K, M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
- Map<K, PMessageOrBuilder<M>> toMessageOrBuilderValues(@Nonnull Map<K, MB> items) {
- return (Map<K, PMessageOrBuilder<M>>) items;
- }
- /**
- * Make a builder of the target message with all differences between
- * source and target marked as modifications.
- *
- * @param source The source message for changes.
- * @param target The message to apply said changes to.
- * @param <M> The message type.
- * @param <B> The builder result type.
- * @return Builder of target with marked modifications.
- */
- @SuppressWarnings("unchecked,rawtypes")
- public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
- B getTargetModifications(PMessageOrBuilder<M> source, PMessageOrBuilder<M> target) {
- B builder;
- if (target instanceof PMessageBuilder) {
- builder = (B) ((PMessageBuilder<M>) target).build().mutate();
- } else {
- builder = (B) ((PMessage<M>) target).mutate();
- }
- for (PField field : source.descriptor().getFields()) {
- if (source.has(field) != target.has(field) ||
- !Objects.equals(source.get(field), target.get(field))) {
- builder.set(field, target.get(field));
- }
- }
- return builder;
- }
- /**
- * Convert a key path to a list of consecutive fields for recursive lookup.
- *
- * @param rootDescriptor The root message descriptor.
- * @param key The '.' joined field name key.
- * @return Array of fields.
- */
- @Nonnull
- public static PField<?>[] keyPathToFields(@Nonnull PMessageDescriptor<?> rootDescriptor, @Nonnull String key) {
- ArrayList<PField<?>> fields = new ArrayList<>();
- String[] parts = key.split("\\.", Byte.MAX_VALUE);
- for (int i = 0; i < (parts.length - 1); ++i) {
- String name = parts[i];
- if (name.isEmpty()) {
- throw new IllegalArgumentException("Empty field name in '" + key + "'");
- }
- PField<?> field = rootDescriptor.findFieldByName(name);
- if (field == null) {
- throw new IllegalArgumentException(
- "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
- }
- if (field.getType() != PType.MESSAGE) {
- throw new IllegalArgumentException(
- "Field '" + name + "' is not of message type in " + rootDescriptor.getQualifiedName());
- }
- fields.add(field);
- rootDescriptor = (PMessageDescriptor<?>) field.getDescriptor();
- }
- String name = parts[parts.length - 1];
- if (name.isEmpty()) {
- throw new IllegalArgumentException("Empty field name in '" + key + "'");
- }
- PField<?> field = rootDescriptor.findFieldByName(name);
- if (field == null) {
- throw new IllegalArgumentException(
- "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
- }
- fields.add(field);
- return fields.toArray(new PField[0]);
- }
- /**
- * Append field to the given path.
- *
- * @param fields Fields to make key path of.
- * @return The new appended key path.
- */
- public static String keyPath(@Nonnull PField<?>... fields) {
- if (fields.length == 0) throw new IllegalArgumentException("No field arguments");
- return Arrays.stream(fields).map(PField::getName).collect(Collectors.joining("."));
- }
- /**
- * Append field to the given path.
- *
- * @param path The path to be appended to.
- * @param field The field who's name should be appended.
- * @return The new appended key path.
- */
- public static String keyPathAppend(@Nullable String path, @Nonnull PField<?> field) {
- if (isNullOrEmpty(path)) {
- return field.getName();
- }
- return path + "." + field.getName();
- }
- /**
- * Look up a key in the message structure. If the key is not found, return the
- * default value for that field, and iterate over the fields until the last one.
- * <p>
- * This differs form {@link #optionalInMessage(PMessageOrBuilder, PField...)} by handling
- * the fields' default values.
- * <p>
- * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
- * constant field enums, in that case you should use optional of the getter
- * and map until you have the last value, which should always return the
- * same, but is compile-time type safe. E.g.:
- *
- * <pre>{@code
- * Optional.ofNullable(message.getFirst())
- * .map(First::getSecond)
- * .map(Second::getThird)
- * .orElse(myDefault);
- * }</pre>
- *
- * @param message The message to look up into.
- * @param fields Field to get in order.
- * @param <T> The expected leaf value type.
- * @return The value found or null.
- * @throws IllegalArgumentException When unable to get value from message.
- */
- @Nonnull
- @SuppressWarnings("unchecked,rawtypes")
- public static <T> Optional<T> getInMessage(@Nullable PMessageOrBuilder<?> message, @Nonnull PField<?>... fields) {
- if (fields.length == 0) {
- throw new IllegalArgumentException("No fields arguments");
- }
- PField<?> field = fields[0];
- if (fields.length > 1) {
- if (field.getType() != PType.MESSAGE) {
- throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
- }
- return getInMessage(message == null ?
- (PMessage<?>) field.getDefaultValue() :
- Optional.ofNullable((PMessage<?>) message.get(field.getId()))
- .orElse((PMessage) field.getDefaultValue()),
- Arrays.copyOfRange(fields, 1, fields.length));
- } else {
- return Optional.ofNullable(message == null ?
- (T) field.getDefaultValue() :
- Optional.ofNullable((T) message.get(field.getId()))
- .orElseGet(() -> (T) field.getDefaultValue()));
- }
- }
- /**
- * Get a field value from a message with optional chaining. If the field is
- * not set, or any message in the chain leading up to the last message is
- * missing, it will return an empty optional, otherwise the leaf field value.
- * Note that this will only check for MESSAGE type if the message is present
- * and needs to be looked up in.
- * <p>
- * This differs from {@link #getInMessage(PMessageOrBuilder, PField...)} by explicitly
- * <b>NOT</b> handling fields' default values.
- * <p>
- * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
- * constant field enums, in that case you should use the optional getter
- * and flatMap until you have the last value, which should always return
- * the same, but is compile-time type safe. E.g.:
- *
- * <pre>{@code
- * message.optionalFirst()
- * .flatMap(First::optionalSecond)
- * .flatMap(Second::optionalThird)
- * .orElse(myDefault);
- * }</pre>
- *
- * @param message The message to start looking up field values in.
- * @param fields Fields to look up in the message.
- * @param <T> The expected leaf value type.
- * @return Optional field value.
- */
- @Nonnull
- @SuppressWarnings("unchecked")
- public static <T> Optional<T> optionalInMessage(
- @Nullable PMessageOrBuilder<?> message,
- @Nonnull PField<?>... fields) {
- if (fields.length == 0) {
- throw new IllegalArgumentException("No fields arguments");
- }
- if (message == null) {
- return Optional.empty();
- }
- @SuppressWarnings("rawtypes")
- PField field = fields[0];
- if (!message.has(field.getId())) {
- return Optional.empty();
- }
- if (fields.length > 1) {
- if (field.getType() != PType.MESSAGE) {
- throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
- }
- // Required to preserve generic typing.
- return optionalInMessage((PMessage<?>) message.get(field), Arrays.copyOfRange(fields, 1, fields.length));
- } else {
- return Optional.of(message.get(field.getId()));
- }
- }
- /**
- * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
- * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
- * the message, so message fields will also be transformed, and values in maps and collection will too.
- * <p>
- * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
- *
- * @param message The message to be transformed.
- * @return The native map representing the message.
- * @deprecated Use {@link #messageToMap(PMessageOrBuilder)}. Will be removed in future major release.
- * Function is renamed t
- */
- @Nonnull
- @Deprecated
- public static Map<String, Object> toMap(@Nonnull PMessageOrBuilder<?> message) {
- return messageToMap(message);
- }
- /**
- * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
- * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
- * the message, so message fields will also be transformed, and values in maps and collection will too.
- * <p>
- * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
- *
- * @param message The message to be transformed.
- * @return The native map representing the message.
- */
- @Nonnull
- @SuppressWarnings("unchecked,rawtypes")
- public static Map<String, Object> messageToMap(@Nonnull PMessageOrBuilder<?> message) {
- TreeMap<String, Object> out = new TreeMap<>();
- for (PField field : message.descriptor().getFields()) {
- if (message.has(field)) {
- switch (field.getType()) {
- case MESSAGE: {
- // Required to preserve generic typing.
- out.put(field.getName(), messageToMap((PMessage<?>) message.get(field)));
- break;
- }
- case SET:
- case LIST: {
- // Required to preserve generic typing.
- out.put(field.getName(), toCollectionInternal((Collection<Object>) message.get(field)));
- break;
- }
- case MAP: {
- // Required to preserve generic typing.
- out.put(field.getName(), toMapInternal((Map<Object, Object>) message.get(field)));
- break;
- }
- default: {
- // Everything else is already using the best representative
- // native value.
- out.put(field.getName(), message.get(field));
- break;
- }
- }
- }
- }
- return out;
- }
- /**
- * Coerce value to match the given type descriptor.
- *
- * @param valueType The value type to coerce to.
- * @param value The value to be coerced.
- * @return The coerced value.
- */
- public static Optional<Object> coerce(@Nonnull PDescriptor valueType, Object value) {
- return coerceInternal(valueType, value, false);
- }
- /**
- * Coerce value to match the given type descriptor using struct type
- * checking. This means some loose coercion transitions are not allowed.
- *
- * @param valueType The value type to coerce to.
- * @param value The value to be coerced.
- * @return The coerced value.
- */
- public static Optional<Object> coerceStrict(@Nonnull PDescriptor valueType, Object value) {
- return coerceInternal(valueType, value, true);
- }
- // --------------------
- // --- INTERNAL ---
- // --------------------
- @SuppressWarnings("unchecked")
- private static Collection<Object> toCollectionInternal(Collection<Object> collection) {
- Collection<Object> out;
- if (collection instanceof SortedSet) {
- out = new TreeSet<>(((SortedSet<Object>) collection).comparator());
- } else if (collection instanceof Set) {
- out = new HashSet<>();
- } else {
- out = new ArrayList<>();
- }
- for (Object item : collection) {
- if (item instanceof PMessageOrBuilder) {
- out.add(messageToMap((PMessageOrBuilder<?>) item));
- } else if (item instanceof Collection) {
- out.add(toCollectionInternal((Collection<Object>) item));
- } else if (item instanceof Map) {
- out.add(toMapInternal((Map<Object, Object>) item));
- } else {
- out.add(item);
- }
- }
- return out;
- }
- // Visible for testing.
- @SuppressWarnings("unchecked")
- static Map<Object, Object> toMapInternal(Map<Object, Object> collection) {
- Map<Object, Object> out;
- if (collection instanceof SortedMap) {
- out = new TreeMap<>(((SortedMap<Object, Object>) collection).comparator());
- } else {
- out = new HashMap<>();
- }
- for (Map.Entry<Object, Object> item : collection.entrySet()) {
- Object value;
- if (item.getValue() instanceof PMessageOrBuilder) {
- value = toMap((PMessageOrBuilder<?>) item.getValue());
- } else if (item.getValue() instanceof Collection) {
- value = toCollectionInternal((Collection<Object>) item.getValue());
- } else if (item.getValue() instanceof Map) {
- value = toMapInternal((Map<Object, Object>) item.getValue());
- } else {
- value = item.getValue();
- }
- // Do not transform the key, as both sorting and equality are easily
- // messed up.
- out.put(item.getKey(), value);
- }
- return out;
- }
- @SuppressWarnings("unchecked")
- private static Optional<Object> coerceInternal(@Nonnull PDescriptor valueType, Object val, boolean strict) {
- if (val == null) {
- return Optional.empty();
- }
- switch (valueType.getType()) {
- case VOID:
- // Void does'nt really care about the value.
- if (val == Boolean.TRUE) {
- return Optional.of(Boolean.TRUE);
- } else if (val == Boolean.FALSE) {
- throw new IllegalArgumentException("Invalid void value " + val.toString());
- }
- break;
- case BOOL:
- if (val instanceof Boolean) {
- return Optional.of(val);
- } else if (val instanceof Number && !(val instanceof Float) && !(val instanceof Double)) {
- return Optional.of(((Number) val).longValue() != 0L);
- } else if (val instanceof PEnumValue) { // e.g. enum
- return Optional.of(((PEnumValue<?>) val).asInteger() != 0);
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- switch (val.toString().toLowerCase(Locale.US)) {
- case "true":
- case "t":
- case "yes":
- case "y":
- case "1":
- return Optional.of(Boolean.TRUE);
- case "false":
- case "f":
- case "no":
- case "n":
- case "0":
- return Optional.of(Boolean.FALSE);
- }
- throw new IllegalArgumentException("Unknown boolean value for string '" + val + "'");
- }
- break;
- case BYTE:
- if (val instanceof Number) {
- return Optional.of((byte) asInteger(valueType, (Number) val, Byte.MIN_VALUE, Byte.MAX_VALUE));
- } else if (val instanceof Boolean) {
- return Optional.of((Boolean) val ? (byte) 1 : (byte) 0);
- } else if (val instanceof PEnumValue) { // e.g. enum
- return Optional.of((byte) asInteger(valueType,
- ((PEnumValue<?>) val).asInteger(),
- Byte.MIN_VALUE,
- Byte.MAX_VALUE));
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- try {
- CharSequence cs = (CharSequence) val;
- if (HEX.matcher(cs).matches()) {
- return Optional.of(Byte.parseByte(cs.subSequence(2, cs.length()).toString(), 16));
- } else {
- return Optional.of(Byte.parseByte(val.toString()));
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid string value '" + val + "' for type byte", e);
- }
- }
- break;
- case I16:
- if (val instanceof Number) {
- return Optional.of((short) asInteger(valueType, (Number) val, Short.MIN_VALUE, Short.MAX_VALUE));
- } else if (val instanceof Boolean) {
- return Optional.of((Boolean) val ? (short) 1 : (short) 0);
- } else if (val instanceof PEnumValue) { // e.g. enum
- return Optional.of((short) asInteger(valueType,
- ((PEnumValue<?>) val).asInteger(),
- Short.MIN_VALUE,
- Short.MAX_VALUE));
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- try {
- CharSequence cs = (CharSequence) val;
- if (HEX.matcher(cs).matches()) {
- return Optional.of(Short.parseShort(cs.subSequence(2, cs.length()).toString(), 16));
- } else {
- return Optional.of(Short.parseShort(val.toString()));
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid string value '" + val + "' for type i16", e);
- }
- }
- break;
- case I32:
- if (val instanceof Number) {
- return Optional.of(asInteger(valueType, (Number) val, Integer.MIN_VALUE, Integer.MAX_VALUE));
- } else if (val instanceof Boolean) {
- return Optional.of((Boolean) val ? 1 : 0);
- } else if (val instanceof PEnumValue) { // e.g. enum
- return Optional.of(((PEnumValue<?>) val).asInteger());
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- try {
- CharSequence cs = (CharSequence) val;
- if (HEX.matcher(cs).matches()) {
- return Optional.of(Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16));
- } else {
- return Optional.of(Integer.parseInt(val.toString()));
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid string value '" + val + "' for type i32", e);
- }
- }
- break;
- case I64:
- if (val instanceof Float || val instanceof Double) {
- long l = ((Number) val).longValue();
- if ((double) l != ((Number) val).doubleValue()) {
- throw new IllegalArgumentException("Truncating long decimals from " + val.toString());
- }
- return Optional.of(l);
- } else if (val instanceof Number) {
- return Optional.of(((Number) val).longValue());
- } else if (val instanceof Boolean) {
- return Optional.of((Boolean) val ? 1L : 0L);
- } else if (val instanceof PEnumValue) { // e.g. enum
- return Optional.of((long) ((PEnumValue<?>) val).asInteger());
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- try {
- CharSequence cs = (CharSequence) val;
- if (HEX.matcher(cs).matches()) {
- return Optional.of(Long.parseLong(cs.subSequence(2, cs.length()).toString(), 16));
- } else {
- return Optional.of(Long.parseLong(val.toString()));
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid string value '" + val + "' for type i64", e);
- }
- }
- break;
- case DOUBLE:
- if (val instanceof Number) {
- return Optional.of(((Number) val).doubleValue());
- } else if (strict) {
- break;
- } else if (val instanceof PEnumValue) {
- return Optional.of((double) ((PEnumValue<?>) val).asInteger());
- }
- break;
- case STRING:
- if (val instanceof CharSequence) {
- return Optional.of(val.toString());
- } else if (strict) {
- break;
- } else if (val instanceof PEnumValue) {
- return Optional.of(((PEnumValue<?>) val).asString());
- } else {
- return Optional.of(val.toString());
- }
- case BINARY:
- if (val instanceof Binary) {
- return Optional.of(val);
- } else if (strict) {
- break;
- } else if (val instanceof CharSequence) {
- return Optional.of(fromBase64(val.toString()));
- }
- break;
- case ENUM: {
- PEnumDescriptor<?> ed = (PEnumDescriptor<?>) valueType;
- if (val instanceof PEnumValue) {
- PEnumValue<?> verified = ((PEnumDescriptor<?>) valueType).findById(((PEnumValue<?>) val).asInteger());
- if (val.equals(verified)) {
- return Optional.of(verified);
- }
- } else if (val instanceof Number && !(val instanceof Double) && !(val instanceof Float)) {
- int i = ((Number) val).intValue();
- PEnumValue<?> ev = ed.findById(i);
- if (ev != null) {
- return Optional.of(ev);
- }
- throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
- " value for id " + val.toString());
- } else if (val instanceof CharSequence) {
- CharSequence cs = (CharSequence) val;
- if (!strict && UNSIGNED.matcher(cs).matches()) {
- int i = Integer.parseInt(val.toString());
- PEnumValue<?> ev = ed.findById(i);
- if (ev != null) {
- return Optional.of(ev);
- }
- } else if (!strict && HEX.matcher(cs).matches()) {
- int i = Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16);
- PEnumValue<?> ev = ed.findById(i);
- if (ev != null) {
- return Optional.of(ev);
- }
- } else {
- PEnumValue<?> ev = ed.findByName(val.toString());
- if (ev != null) {
- return Optional.of(ev);
- }
- }
- throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
- " value for string '" + val.toString() + "'");
- }
- throw new IllegalArgumentException(
- "Invalid value type " + val.getClass() + " for enum " + valueType.toString());
- }
- case MESSAGE: {
- if (val instanceof PMessage) {
- if (valueType.equals(((PMessage<?>) val).descriptor())) {
- return Optional.of(val);
- } else {
- throw new IllegalArgumentException(
- "Unable to cast message type " +
- ((PMessage<?>) val).descriptor().getQualifiedName() + " to " +
- valueType.getQualifiedName());
- }
- } else if (val instanceof PMessageBuilder) {
- if (valueType.equals(((PMessageBuilder<?>) val).descriptor())) {
- return Optional.of(((PMessageBuilder<?>) val).build());
- } else {
- throw new IllegalArgumentException(
- "Unable to cast message type " +
- ((PMessageBuilder<?>) val).descriptor().getQualifiedName() + " to " +
- valueType.getQualifiedName());
- }
- } else if (!strict && val instanceof Map) {
- PMessageDescriptor<?> md = (PMessageDescriptor<?>) valueType;
- PMessageBuilder<?> builder = md.builder();
- for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
- if (!(entry.getKey() instanceof CharSequence)) {
- throw new IllegalArgumentException("Invalid message map key: " + entry.getKey().toString());
- }
- PField<?> field = md.findFieldByName(entry.getKey().toString());
- if (field == null) {
- throw new IllegalArgumentException(
- "No such field " + entry.getKey() + " in " + md.getQualifiedName());
- }
- builder.set(field.getId(),
- coerceInternal(field.getDescriptor(), entry.getValue(), false).orElse(null));
- }
- return Optional.of(builder.build());
- }
- throw new IllegalArgumentException(
- "Invalid value type " + val.getClass() + " for message " + valueType.toString());
- }
- case LIST: {
- if (val instanceof Collection) {
- PList<Object> pl = (PList<Object>) valueType;
- PList.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
- for (Object o : (Collection<?>) val) {
- Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
- if (value != null) {
- builder.add(value);
- } else if (strict) {
- throw new IllegalArgumentException("Null value in list");
- }
- }
- return Optional.of(builder.build());
- }
- throw new IllegalArgumentException(
- "Invalid value type " + val.getClass() + " for " + valueType.toString());
- }
- case SET:
- if (val instanceof Collection) {
- PSet<Object> pl = (PSet<Object>) valueType;
- PSet.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
- for (Object o : (Collection<?>) val) {
- Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
- if (value != null) {
- builder.add(value);
- } else if (strict) {
- throw new IllegalArgumentException("Null value in set");
- }
- }
- return Optional.of(builder.build());
- }
- throw new IllegalArgumentException(
- "Invalid value type " + val.getClass() + " for " + valueType.toString());
- case MAP:
- if (val instanceof Map) {
- PMap<Object, Object> pl = (PMap<Object, Object>) valueType;
- PMap.Builder<Object, Object> builder = pl.builder(((Map<?,?>) val).size());
- for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
- Object key = coerceInternal(pl.keyDescriptor(), entry.getKey(), strict).orElse(null);
- Object value = coerceInternal(pl.itemDescriptor(), entry.getValue(), strict).orElse(null);
- if (key != null && value != null) {
- builder.put(key, value);
- } else if (strict) {
- throw new IllegalArgumentException("Null key or value in map");
- }
- }
- return Optional.of(builder.build());
- }
- throw new IllegalArgumentException(
- "Invalid value type " + val.getClass() + " for " + valueType.toString());
- }
- throw new IllegalArgumentException("Invalid value type " + val.getClass() + " for type " + valueType.getType());
- }
- private static int asInteger(PDescriptor descriptor, Number value, int min, int max) {
- if (value instanceof Float || value instanceof Double) {
- long l = value.longValue();
- if ((double) l != value.doubleValue()) {
- throw new IllegalArgumentException(
- "Truncating " + descriptor.getName() + " decimals from " + value.toString());
- }
- return validateInRange(descriptor.getName(), l, min, max);
- } else {
- return validateInRange(descriptor.getName(), value.longValue(), min, max);
- }
- }
- private static int validateInRange(String type, long l, int min, int max) throws IllegalArgumentException {
- if (l < min) {
- throw new IllegalArgumentException(type + " value outside of bounds: " + l + " < " + min);
- } else if (l > max) {
- throw new IllegalArgumentException(type + " value outside of bounds: " + l + " > " + max);
- }
- return (int) l;
- }
- private static final Pattern UNSIGNED = Pattern.compile("(0|[1-9][0-9]*)");
- private static final Pattern HEX = Pattern.compile("0x[0-9a-fA-F]+");
- private MessageUtil() {}
- }