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() {}
}