diff --git a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java index ba9f54f2177..fbe5eaf63b5 100644 --- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java +++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java @@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.JsonMergeUtil; import java.util.ArrayList; import java.util.Arrays; @@ -39,6 +40,8 @@ public class OrtbTypesResolver { private static final String BIDREQUEST = "bidrequest"; private static final String TARGETING = "targeting"; private static final String UNKNOWN_REFERER = "unknown referer"; + private static final String DATA = "data"; + private static final String EXT = "ext"; private static final Map> FIRST_ARRAY_ELEMENT_STANDARD_FIELDS; private static final Map> FIRST_ARRAY_ELEMENT_REQUEST_FIELDS; @@ -66,9 +69,11 @@ public class OrtbTypesResolver { } private final JacksonMapper jacksonMapper; + private final JsonMergeUtil jsonMergeUtil; public OrtbTypesResolver(JacksonMapper jacksonMapper) { this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.jsonMergeUtil = new JsonMergeUtil(jacksonMapper); } /** @@ -172,6 +177,8 @@ private JsonNode normalizeNode(JsonNode containerNode, String nodeName, .forEach(fieldName -> updateWithNormalizedField(containerObjectNode, fieldName, () -> toCommaSeparatedTextNode(containerObjectNode, fieldName, nodeName, nodePrefix, warnings))); + + normalizeDataExtension(containerObjectNode, nodeName, nodePrefix, warnings); } else { warnings.add(String.format("%s%s field ignored. Expected type is object, but was `%s`.", nodePrefix, nodeName, containerNode.getNodeType().name())); @@ -238,6 +245,38 @@ private JsonNode toCommaSeparatedTextNode(ObjectNode containerNode, String field } } + public void normalizeDataExtension(ObjectNode containerNode, String containerName, String nodePrefix, + List warnings) { + final JsonNode data = containerNode.get(DATA); + if (data == null || data.isNull()) { + return; + } + final JsonNode extData = containerNode.path(EXT).path(DATA); + final JsonNode ext = containerNode.get(EXT); + if (!extData.isNull() && !extData.isMissingNode()) { + final JsonNode resolvedExtData = jsonMergeUtil.merge(extData, data); + ((ObjectNode) ext).set(DATA, resolvedExtData); + } else { + copyDataToExtData(containerNode, containerName, nodePrefix, warnings, data); + } + containerNode.remove(DATA); + } + + private void copyDataToExtData(ObjectNode containerNode, String containerName, String nodePrefix, + List warnings, JsonNode data) { + final JsonNode ext = containerNode.get(EXT); + if (ext != null && ext.isObject()) { + ((ObjectNode) ext).set(DATA, data); + } else if (ext != null && !ext.isObject()) { + warnings.add(String.format("Incorrect type for first party data field %s%s.%s, expected is " + + "object, but was %s. Replaced with object", + nodePrefix, containerName, EXT, ext.getNodeType())); + containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data)); + } else { + containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data)); + } + } + private void warnForExpectedStringArrayType(String fieldName, String containerName, List warnings, String nodePrefix, JsonNodeType nodeType) { warnings.add(String.format("Incorrect type for first party data field %s%s.%s, expected strings, but" diff --git a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java index fe3ecb97db9..3071553c82b 100644 --- a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java +++ b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java @@ -213,6 +213,104 @@ public void normalizeTargetingShouldTolerateIncorrectTypeUserFieldAndRemoveIt() assertThat(containerNode).isEqualTo(mapper.createObjectNode()); } + @Test + public void normalizeBidRequestShouldMergeUserDataToUserExtDataAndRemoveData() { + // given + final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue")) + .set("ext", obj("data", obj("extDataField", "extDataValue")))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + + assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue") + .put("dataField", "dataValue"))))); + } + + @Test + public void normalizeBidRequestShouldMergeSiteDataToSiteExtDataAndRemoveData() { + // given + final ObjectNode containerNode = obj("site", obj("data", obj("dataField", "dataValue")) + .set("ext", obj("data", obj("extDataField", "extDataValue")))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + + assertThat(containerNode).isEqualTo(obj("site", obj("ext", obj("data", obj("extDataField", "extDataValue") + .put("dataField", "dataValue"))))); + } + + @Test + public void normalizeBidRequestShouldMergeAppDataToAppExtDataAndRemoveData() { + // given + final ObjectNode containerNode = obj("app", obj("data", obj("dataField", "dataValue")) + .set("ext", obj("data", obj("extDataField", "extDataValue")))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + assertThat(containerNode).isEqualTo(obj("app", obj("ext", obj("data", obj("extDataField", "extDataValue") + .put("dataField", "dataValue"))))); + } + + @Test + public void normalizeBidRequestShouldNotChangeUserWhenUserDataNotDefined() { + // given + final ObjectNode containerNode = obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue")))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue"))))); + } + + @Test + public void normalizeBidRequestShouldSetDataToUserIfExtDataNotExist() { + // given + final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue")) + .set("ext", obj("extField", "extValue"))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("dataField", "dataValue")) + .put("extField", "extValue")))); + } + + @Test + public void normalizeBidRequestShouldSetExtDataToUserIfExtNotExist() { + // given + final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue"))); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer"); + + // then + assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("dataField", "dataValue"))))); + } + + @Test + public void normalizeBidRequestShouldSetExtDataToUserIfExtIncorrectType() { + // given + final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue")) + .set("ext", mapper.createArrayNode())); + final List warnings = new ArrayList<>(); + + // when + ortbTypesResolver.normalizeBidRequest(containerNode, warnings, "referer"); + + // then + assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("dataField", "dataValue"))))); + assertThat(warnings).hasSize(1).containsOnly("WARNING: Incorrect type for first party data field" + + " bidrequest.user.ext, expected is object, but was ARRAY. Replaced with object"); + } + @Test public void normalizeBidRequestShouldResolveORTBFieldsWithIdForRequestAndExcludedIdForBidderConfig() { // given @@ -290,4 +388,12 @@ public void normalizeBidRequestShouldResolveORTBFieldsWithIdForRequestAndExclude .path("user")) .isEqualTo(mapper.createObjectNode().put("gender", "gender1").put("keywords", "keyword1,keyword2")); } + + private static ObjectNode obj(String fieldName, JsonNode value) { + return (ObjectNode) mapper.createObjectNode().set(fieldName, value); + } + + private static ObjectNode obj(String fieldName, String value) { + return mapper.createObjectNode().put(fieldName, value); + } }