com.indeed.proctor.common.model.Payload Maven / Gradle / Ivy
package com.indeed.proctor.common.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
import com.indeed.proctor.common.PayloadType;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Models a payload value for a bucket in a test, generally meant to have one kind of value per
* bucket.
*
* @author pwp
* NOTE: if you add a payload type here, also please add it to proctor webapp's buckets.js
* indeed.proctor.editor.BucketsEditor.prototype.prettyPrintPayloadValue_
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Payload {
@Nullable private Double doubleValue;
@Nullable private Double[] doubleArray;
@Nullable private Long longValue;
@Nullable private Long[] longArray;
@Nullable private String stringValue;
@Nullable private String[] stringArray;
@Nullable private Map map;
@Nullable private JsonNode json;
// Used for returning something when we can't return a null.
public static final Payload EMPTY_PAYLOAD = new Payload();
// Error message for invalid user input
public static final String PAYLOAD_OVERWRITE_EXCEPTION =
"Expected all properties to be empty: ";
public Payload(final String value) {
this.stringValue = value;
}
public Payload(final Double value) {
this.doubleValue = value;
}
public Payload(final Long value) {
this.longValue = value;
}
public Payload(final Map value) {
this.map = new HashMap<>(value);
}
public Payload(final JsonNode value) {
this.json = value.deepCopy();
}
public Payload(final String[] values) {
this.stringArray = Arrays.copyOf(values, values.length);
}
public Payload(final Double[] values) {
this.doubleArray = Arrays.copyOf(values, values.length);
}
public Payload(final Long[] values) {
this.longArray = Arrays.copyOf(values, values.length);
}
public Payload() {
/* intentionally empty */
}
public Payload(@Nonnull final Payload other) {
this.doubleValue = other.doubleValue;
if (other.doubleArray != null) {
this.doubleArray = Arrays.copyOf(other.doubleArray, other.doubleArray.length);
}
this.longValue = other.longValue;
if (other.longArray != null) {
this.longArray = Arrays.copyOf(other.longArray, other.longArray.length);
}
this.stringValue = other.stringValue;
if (other.stringArray != null) {
this.stringArray = Arrays.copyOf(other.stringArray, other.stringArray.length);
}
if (other.map != null) {
this.map = new HashMap<>(other.map);
}
if (other.json != null) {
this.json = other.json.deepCopy();
}
}
@Nullable
public Double getDoubleValue() {
return doubleValue;
}
public void setDoubleValue(@Nullable final Double doubleValue) {
precheckStateAllNull();
this.doubleValue = doubleValue;
}
@Nullable
public Double[] getDoubleArray() {
return doubleArray;
}
public void setDoubleArray(@Nullable final Double[] doubleArray) {
precheckStateAllNull();
this.doubleArray = doubleArray;
}
@Nullable
public Long getLongValue() {
return longValue;
}
public void setLongValue(@Nullable final Long longValue) {
precheckStateAllNull();
this.longValue = longValue;
}
@Nullable
public Long[] getLongArray() {
return longArray;
}
public void setLongArray(@Nullable final Long[] longArray) {
precheckStateAllNull();
this.longArray = longArray;
}
@Nullable
public String getStringValue() {
return stringValue;
}
public void setStringValue(@Nullable final String stringValue) {
precheckStateAllNull();
this.stringValue = stringValue;
}
@Nullable
public String[] getStringArray() {
return stringArray;
}
public void setStringArray(@Nullable final String[] stringArray) {
precheckStateAllNull();
this.stringArray = stringArray;
}
@Nullable
public JsonNode getJson() {
return json;
}
public void setJson(@Nullable final JsonNode json) {
precheckStateAllNull();
this.json = json;
}
@Nullable
public Map getMap() {
return map;
}
public void setMap(@Nullable final Map map) {
precheckStateAllNull();
this.map = map;
}
// Sanity check precondition for above setters
private void precheckStateAllNull() throws IllegalStateException {
if (Stream.of(PayloadType.values()).anyMatch(pt -> resolvers.get(pt).apply(this) != null)) {
throw new IllegalStateException(PAYLOAD_OVERWRITE_EXCEPTION + this);
}
}
@Nonnull
@Override
public String toString() {
final StringBuilder s = new StringBuilder(50).append('{');
// careful of the autoboxing...
if (map != null) {
s.append(" map : [");
for (final Map.Entry entry : map.entrySet()) {
s.append('(')
.append(entry.getKey())
.append(',')
.append(entry.getValue())
.append(')');
}
s.append(']');
}
if (doubleValue != null) {
s.append(" doubleValue : ").append(doubleValue);
}
if (doubleArray != null) {
s.append(" doubleArray : [");
s.append(StringUtils.join(doubleArray, ", "));
s.append(']');
}
if (longValue != null) {
s.append(" longValue : ").append(longValue);
}
if (longArray != null) {
s.append(" longArray : [");
s.append(StringUtils.join(longArray, ", "));
s.append(']');
}
if (stringValue != null) {
s.append(" stringValue : \"").append(stringValue).append('"');
}
if (stringArray != null) {
s.append(" stringArray : [");
if (stringArray.length > 0) {
s.append('"');
s.append(String.join("\", \"", stringArray));
s.append('"');
}
s.append(']');
}
if (json != null) {
s.append(" json : ");
s.append(json);
}
s.append(" }");
return s.toString();
}
/**
* infers payloadtype based on the value that is set.
*
* @return payloadType unless emptyPayload
*/
@Nonnull
public Optional fetchPayloadType() {
return Optional.ofNullable(getPayloadType());
}
public static boolean hasType(final Payload payload, final PayloadType payloadType) {
if (payload == null) {
return false;
}
final Function resolver = resolvers.get(payloadType);
return resolver != null && resolver.apply(payload) != null;
}
@CheckForNull
private PayloadType getPayloadType() {
return Stream.of(PayloadType.values())
.filter(pt -> resolvers.get(pt).apply(this) != null)
.findFirst()
.orElse(null);
}
/**
* @return the payload type as a string. Used by Proctor Webapp.
* @deprecated use fetchPayloadType
*/
@Nonnull
@Deprecated
public String fetchType() {
return fetchPayloadType().map(t -> t.payloadTypeName).orElse("none");
}
public boolean sameType(@Nullable final Payload other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
// Both this and that must have either null
// or something filled in for each slot.
return this.getPayloadType() == other.getPayloadType();
}
public int numFieldsDefined() {
return (int)
Stream.of(PayloadType.values())
.filter(pt -> resolvers.get(pt).apply(this) != null)
.count();
}
/**
* @return "the" value of this Payload, stuffed into an Object. This is used for evaluating the
* "validator" portion of a PayloadSpecification against these Payloads.
* We don't want the JsonSerializer to know about this, so renamed to not begin with
* "get".
*/
@CheckForNull
public Object fetchAValue() {
return Stream.of(PayloadType.values())
.map(pt -> resolvers.get(pt).apply(this))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
@Override
public boolean equals(final Object o) {
/*
* WARNING: Do not implement equals using Objects.equals for the arrays,
* because new String[]{"a"}.equals(new String[]{"a"}) is false
*/
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Payload payload = (Payload) o;
return Objects.equals(doubleValue, payload.doubleValue)
&& Arrays.equals(doubleArray, payload.doubleArray)
&& Objects.equals(longValue, payload.longValue)
&& Arrays.equals(longArray, payload.longArray)
&& Objects.equals(stringValue, payload.stringValue)
&& Arrays.equals(stringArray, payload.stringArray)
&& Objects.equals(json, payload.json)
&& comparePayloadMap(map, payload.map);
}
private static boolean comparePayloadMap(
final Map a, final Map b) {
if (a != null && b != null && a.size() == b.size()) {
boolean isEqual = true;
for (final String keyString : a.keySet()) {
if (!isEqual) {
break;
}
final Object aValue = a.get(keyString);
final Object bValue = b.get(keyString);
if (aValue instanceof Double
|| aValue instanceof Long
|| aValue instanceof String) {
isEqual &= Objects.equals(aValue, bValue);
} else if (aValue instanceof ArrayList && bValue instanceof ArrayList) {
isEqual &= Objects.equals(aValue, bValue);
} else if (aValue instanceof ArrayList) {
isEqual &= Arrays.equals(((ArrayList>) aValue).toArray(), (Object[]) bValue);
} else if (bValue instanceof ArrayList) {
isEqual &= Arrays.equals(((ArrayList>) bValue).toArray(), (Object[]) aValue);
} else if (aValue instanceof Double[]) {
final Double[] aArr = (Double[]) aValue;
final Double[] bArr = (Double[]) bValue;
isEqual &= Arrays.equals(aArr, bArr);
} else if (aValue instanceof Long[]) {
final Long[] aArr = (Long[]) aValue;
final Long[] bArr = (Long[]) bValue;
isEqual &= Arrays.equals(aArr, bArr);
} else if (aValue instanceof String[]) {
final String[] aArr = (String[]) aValue;
final String[] bArr = (String[]) bValue;
isEqual &= Arrays.equals(aArr, bArr);
} else {
isEqual &= Objects.equals(aValue, bValue);
}
}
return isEqual;
}
return Objects.equals(a, b);
}
@Override
public int hashCode() {
int result = Objects.hash(doubleValue, longValue, stringValue, map);
result = 31 * result + Arrays.hashCode(doubleArray);
result = 31 * result + Arrays.hashCode(longArray);
result = 31 * result + Arrays.hashCode(stringArray);
return result;
}
private static final Map> resolvers =
ImmutableMap.>builder()
.put(PayloadType.DOUBLE_VALUE, Payload::getDoubleValue)
.put(PayloadType.DOUBLE_ARRAY, Payload::getDoubleArray)
.put(PayloadType.LONG_VALUE, Payload::getLongValue)
.put(PayloadType.LONG_ARRAY, Payload::getLongArray)
.put(PayloadType.STRING_VALUE, Payload::getStringValue)
.put(PayloadType.STRING_ARRAY, Payload::getStringArray)
.put(PayloadType.MAP, Payload::getMap)
.put(PayloadType.JSON, Payload::getJson)
.build();
}