Skip to content

Commit

Permalink
INIT: better error handling
Browse files Browse the repository at this point in the history
Signed-off-by: George Chen <qchea@amazon.com>
  • Loading branch information
chenqi0805 committed Feb 24, 2025
1 parent 94d1f15 commit db8a48c
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 13 deletions.
1 change: 1 addition & 0 deletions data-prepper-pipeline-parser/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.apache.commons:commons-text:1.13.0'
implementation 'org.projectlombok:lombok:1.18.22'
implementation 'com.jayway.jsonpath:json-path:2.6.0'
implementation 'javax.inject:javax.inject:1'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.pipeline.parser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.util.ClassUtil;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;

@Named
public class DataPrepperDeserializationProblemHandler extends DeserializationProblemHandler {

@Inject
public DataPrepperDeserializationProblemHandler() {
super();
}

@Override
public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException {
throw new IOException(
String.format("Cannot deserialize value of type %s from String \"%s\": %s",
ClassUtil.nameOf(targetType), valueToConvert, failureMsg));
}

@Override
public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, JsonToken t, JsonParser p, String failureMsg) throws IOException {
throw JsonMappingException.from(ctxt.getParser(),
String.format("Cannot deserialize value of type %s from %s.",
ClassUtil.getTypeDescription(targetType), JsonToken.valueDescFor(t)));
}

@Override
public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, ValueInstantiator valueInsta, JsonParser p, String msg) throws IOException {
throw JsonMappingException.from(ctxt.getParser(),
String.format("Cannot deserialize '%s' into '%s'.", p.getText(), instClass.getName()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@
public class DataPrepperDurationParser {
private static final String SIMPLE_DURATION_REGEX = "^(0|[1-9]\\d*)(s|ms)$";
private static final Pattern SIMPLE_DURATION_PATTERN = Pattern.compile(SIMPLE_DURATION_REGEX);
private static final String INVALID_DURATION_ERROR_MESSAGE =
"Durations must use either ISO 8601 notation or simple notations for seconds (60s) or milliseconds (100ms). Whitespace is ignored.";

public static Duration parse(final String durationString) {
if (durationString == null) {
throw new IllegalArgumentException(INVALID_DURATION_ERROR_MESSAGE);
}
try {
return Duration.parse(durationString);
} catch (final DateTimeParseException e) {
final Duration duration = parseSimpleDuration(durationString);
if (duration == null) {
throw new IllegalArgumentException("Durations must use either ISO 8601 notation or simple notations for seconds (60s) or milliseconds (100ms). Whitespace is ignored.");
throw new IllegalArgumentException(INVALID_DURATION_ERROR_MESSAGE);
}
return duration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
public class PipelinesDataflowModelParser {
private static final Logger LOG = LoggerFactory.getLogger(PipelinesDataflowModelParser.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory())
.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY);
.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
.addHandler(new DataPrepperDeserializationProblemHandler());

private final PipelineConfigurationReader pipelineConfigurationReader;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.pipeline.parser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.util.ClassUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.IOException;
import java.util.UUID;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DataPrepperDeserializationProblemHandlerTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Class<?> TEST_TARGET_TYPE = Boolean.class;
private static final String TEST_VALUE = UUID.randomUUID().toString();
private static final String TEST_FAILURE_MESSAGE = UUID.randomUUID().toString();

@Mock
private DeserializationContext deserializationContext;

@Mock
private JsonParser jsonParser;

@Mock
private ValueInstantiator valueInstantiator;

private final DataPrepperDeserializationProblemHandler objectUnderTest = new DataPrepperDeserializationProblemHandler();

@Test
void testHandleWeirdStringValue() {
final IOException exception = assertThrows(IOException.class, () -> objectUnderTest.handleWeirdStringValue(
deserializationContext, TEST_TARGET_TYPE, TEST_VALUE, TEST_FAILURE_MESSAGE));
assertThat(exception.getMessage(), containsString(ClassUtil.nameOf(TEST_TARGET_TYPE)));
assertThat(exception.getMessage(), containsString(TEST_VALUE));
assertThat(exception.getMessage(), containsString(TEST_FAILURE_MESSAGE));
}

@Test
void testHandleUnexpectedToken() {
when(deserializationContext.getParser()).thenReturn(jsonParser);
final JavaType javaType = OBJECT_MAPPER.constructType(TEST_TARGET_TYPE);
final JsonToken jsonToken = JsonToken.END_OBJECT;
final JsonMappingException exception = assertThrows(JsonMappingException.class, () -> objectUnderTest.handleUnexpectedToken(
deserializationContext, javaType, jsonToken, jsonParser, TEST_FAILURE_MESSAGE));
assertThat(exception.getMessage(), containsString(ClassUtil.getTypeDescription(javaType)));
assertThat(exception.getMessage(), not(containsString(TEST_FAILURE_MESSAGE)));
}

@Test
void testHandleMissingInstantiator() throws IOException {
when(deserializationContext.getParser()).thenReturn(jsonParser);
when(jsonParser.getText()).thenReturn(UUID.randomUUID().toString());
final JsonMappingException exception = assertThrows(JsonMappingException.class, () -> objectUnderTest.handleMissingInstantiator(
deserializationContext, TEST_TARGET_TYPE, valueInstantiator, jsonParser, TEST_FAILURE_MESSAGE));
assertThat(exception.getMessage(), containsString(jsonParser.getText()));
assertThat(exception.getMessage(), containsString(TEST_TARGET_TYPE.getName()));
assertThat(exception.getMessage(), not(containsString(TEST_FAILURE_MESSAGE)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@

class DataPrepperDurationParserTest {

@Test
void nullValueThrowIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> DataPrepperDurationParser.parse(null));
}

@ParameterizedTest
@ValueSource(strings = {"6s1s", "60ms 100s", "20.345s", "-1s", "06s", "100m", "100sm", "100"})
void invalidDurationStringsThrowIllegalArgumentException(final String durationString) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.opensearch.dataprepper.model.plugin.PluginConfigVariable;
import org.opensearch.dataprepper.model.types.ByteCount;
import org.opensearch.dataprepper.pipeline.parser.ByteCountDeserializer;
import org.opensearch.dataprepper.pipeline.parser.DataPrepperDeserializationProblemHandler;
import org.opensearch.dataprepper.pipeline.parser.DataPrepperDurationDeserializer;
import org.opensearch.dataprepper.pipeline.parser.EnumDeserializer;
import org.opensearch.dataprepper.pipeline.parser.EventKeyDeserializer;
Expand All @@ -32,7 +33,8 @@ public class ObjectMapperConfiguration {
Boolean.class, Character.class, PluginConfigVariable.class);

@Bean(name = "extensionPluginConfigObjectMapper")
ObjectMapper extensionPluginConfigObjectMapper() {
ObjectMapper extensionPluginConfigObjectMapper(
final DataPrepperDeserializationProblemHandler dataPrepperDeserializationProblemHandler) {
final SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Duration.class, new DataPrepperDurationDeserializer());
simpleModule.addDeserializer(Enum.class, new EnumDeserializer());
Expand All @@ -41,13 +43,15 @@ ObjectMapper extensionPluginConfigObjectMapper() {

return new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.registerModule(simpleModule);
.registerModule(simpleModule)
.addHandler(dataPrepperDeserializationProblemHandler);
}

@Bean(name = "pluginConfigObjectMapper")
ObjectMapper pluginConfigObjectMapper(
final VariableExpander variableExpander,
final EventKeyFactory eventKeyFactory) {
final EventKeyFactory eventKeyFactory,
final DataPrepperDeserializationProblemHandler dataPrepperDeserializationProblemHandler) {
final SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Duration.class, new DataPrepperDurationDeserializer());
simpleModule.addDeserializer(ByteCount.class, new ByteCountDeserializer());
Expand All @@ -58,6 +62,7 @@ ObjectMapper pluginConfigObjectMapper(

return new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.registerModule(simpleModule);
.registerModule(simpleModule)
.addHandler(dataPrepperDeserializationProblemHandler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException;
import org.opensearch.dataprepper.pipeline.parser.DataPrepperDeserializationProblemHandler;
import org.opensearch.dataprepper.plugins.test.TestExtension;
import org.opensearch.dataprepper.plugins.test.TestExtensionConfig;

Expand Down Expand Up @@ -43,7 +44,8 @@ class ExtensionPluginConfigurationConverterTest {
@Mock
private ConstraintViolation<Object> constraintViolation;

private final ObjectMapper objectMapper = new ObjectMapperConfiguration().extensionPluginConfigObjectMapper();
private final ObjectMapper objectMapper = new ObjectMapperConfiguration()
.extensionPluginConfigObjectMapper(new DataPrepperDeserializationProblemHandler());
private ExtensionPluginConfigurationConverter objectUnderTest;

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.model.event.EventKey;
import org.opensearch.dataprepper.model.event.EventKeyFactory;
import org.opensearch.dataprepper.pipeline.parser.DataPrepperDeserializationProblemHandler;

import java.time.Duration;
import java.util.Arrays;
Expand All @@ -38,18 +39,23 @@ class ObjectMapperConfigurationTest {
@Mock
private EventKeyFactory eventKeyFactory;

@Mock
private DataPrepperDeserializationProblemHandler dataPrepperDeserializationProblemHandler;

@Test
void test_duration_with_pluginConfigObjectMapper() {
final String durationTestString = "10s";
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(variableExpander, eventKeyFactory);
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(
variableExpander, eventKeyFactory, dataPrepperDeserializationProblemHandler);
final Duration duration = objectMapper.convertValue(durationTestString, Duration.class);
assertThat(duration, equalTo(Duration.ofSeconds(10)));
}

@Test
void test_enum_with_pluginConfigObjectMapper() throws JsonProcessingException {
final String testModelAsString = "{ \"name\": \"my-name\", \"test_type\": \"test\" }";
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(variableExpander, eventKeyFactory);
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(
variableExpander, eventKeyFactory, dataPrepperDeserializationProblemHandler);
final TestModel testModel = objectMapper.readValue(testModelAsString, TestModel.class);
assertThat(testModel, notNullValue());
assertThat(testModel.getTestType(), equalTo(TestType.TEST));
Expand All @@ -58,15 +64,17 @@ void test_enum_with_pluginConfigObjectMapper() throws JsonProcessingException {
@Test
void test_duration_with_extensionPluginConfigObjectMapper() {
final String durationTestString = "10s";
final ObjectMapper objectMapper = objectMapperConfiguration.extensionPluginConfigObjectMapper();
final ObjectMapper objectMapper = objectMapperConfiguration.extensionPluginConfigObjectMapper(
dataPrepperDeserializationProblemHandler);
final Duration duration = objectMapper.convertValue(durationTestString, Duration.class);
assertThat(duration, equalTo(Duration.ofSeconds(10)));
}

@Test
void test_enum_with_extensionPluginConfigObjectMapper() throws JsonProcessingException {
final String testModelAsString = "{ \"name\": \"my-name\", \"test_type\": \"test\" }";
final ObjectMapper objectMapper = objectMapperConfiguration.extensionPluginConfigObjectMapper();
final ObjectMapper objectMapper = objectMapperConfiguration.extensionPluginConfigObjectMapper(
dataPrepperDeserializationProblemHandler);
final TestModel testModel = objectMapper.readValue(testModelAsString, TestModel.class);
assertThat(testModel, notNullValue());
assertThat(testModel.getTestType(), equalTo(TestType.TEST));
Expand All @@ -77,7 +85,8 @@ void test_eventKey_with_pluginConfigObjectMapper() {
final String testKey = "test";
final EventKey eventKey = mock(EventKey.class);
when(eventKeyFactory.createEventKey(testKey, EventKeyFactory.EventAction.ALL)).thenReturn(eventKey);
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(variableExpander, eventKeyFactory);
final ObjectMapper objectMapper = objectMapperConfiguration.pluginConfigObjectMapper(
variableExpander, eventKeyFactory, dataPrepperDeserializationProblemHandler);
final EventKey actualEventKey = objectMapper.convertValue(testKey, EventKey.class);
assertThat(actualEventKey, equalTo(eventKey));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.model.plugin.PluginConfigValueTranslator;
import org.opensearch.dataprepper.model.plugin.PluginConfigVariable;
import org.opensearch.dataprepper.pipeline.parser.DataPrepperDeserializationProblemHandler;

import java.io.IOException;
import java.math.BigDecimal;
Expand All @@ -38,7 +39,8 @@

@ExtendWith(MockitoExtension.class)
class VariableExpanderTest {
static final ObjectMapper OBJECT_MAPPER = new ObjectMapperConfiguration().extensionPluginConfigObjectMapper();
static final ObjectMapper OBJECT_MAPPER = new ObjectMapperConfiguration()
.extensionPluginConfigObjectMapper(new DataPrepperDeserializationProblemHandler());
static final JsonFactory JSON_FACTORY = new MappingJsonFactory();

@Mock
Expand Down

0 comments on commit db8a48c

Please sign in to comment.