From a3b5f91be1bb9cae6c3b08228a62730fa3ad9af3 Mon Sep 17 00:00:00 2001 From: Asif Sohail Mohammed Date: Tue, 15 Aug 2023 17:13:08 -0500 Subject: [PATCH 1/3] Fix: IllegalArgument Exception in String converter Signed-off-by: Asif Sohail Mohammed --- .../typeconverter/StringConverter.java | 18 +++++++++++-- .../typeconverter/StringConverterTests.java | 25 +++++++++++++++---- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java index dd024741b8..9767029d66 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java @@ -5,8 +5,17 @@ package org.opensearch.dataprepper.typeconverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +import static org.opensearch.dataprepper.logging.DataPrepperMarkers.EVENT; + public class StringConverter implements TypeConverter { - public String convert(Object source) throws IllegalArgumentException { + private static final Logger LOG = LoggerFactory.getLogger(StringConverter.class); + + public String convert(final Object source) { if (source instanceof Long) { return Long.toString(((Number)source).longValue()); } @@ -25,6 +34,11 @@ public String convert(Object source) throws IllegalArgumentException { if (source instanceof String) { return (String)source; } - throw new IllegalArgumentException("Unsupported type conversion"); + LOG.error(EVENT, "Unable to convert {} to String", source); + if (Objects.nonNull(source)) { + return source.toString(); + } else { + return ""; + } } } diff --git a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java index ae35283b62..63c22f98fd 100644 --- a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java +++ b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java @@ -7,12 +7,11 @@ import org.junit.jupiter.api.Test; -import java.util.Collections; +import java.util.List; import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; public class StringConverterTests { @Test @@ -64,9 +63,25 @@ void testStringToStringConversion() { assertThat(converter.convert(strConstant), equalTo(strConstant)); } @Test - void testInvalidStringConversion() { + void testNullValueStringConversion() { StringConverter converter = new StringConverter(); - final Map map = Collections.emptyMap(); - assertThrows(IllegalArgumentException.class, () -> converter.convert(map)); + final String expectedString = ""; + assertThat(converter.convert(null), equalTo(expectedString)); + } + + @Test + void testMapToStringConversion() { + StringConverter converter = new StringConverter(); + final Map map = Map.of("testKey", "testValue"); + final String expectedString = "{testKey=testValue}"; + assertThat(converter.convert(map), equalTo(expectedString)); + } + + @Test + void testListToStringConversion() { + StringConverter converter = new StringConverter(); + final List list = List.of("listItem1", "listItem2"); + final String expectedString = "[listItem1, listItem2]"; + assertThat(converter.convert(list), equalTo(expectedString)); } } From 460251e71a97a95677c5c30c5ce42a5195f742dd Mon Sep 17 00:00:00 2001 From: Asif Sohail Mohammed Date: Thu, 17 Aug 2023 23:51:21 -0500 Subject: [PATCH 2/3] Added tags_on_failure Signed-off-by: Asif Sohail Mohammed --- .../typeconverter/StringConverter.java | 17 +++--------- .../typeconverter/StringConverterTests.java | 20 +++++--------- .../ConvertEntryTypeProcessor.java | 16 ++++++++++- .../ConvertEntryTypeProcessorConfig.java | 7 +++++ .../ConvertEntryTypeProcessorTests.java | 27 ++++++++++++++----- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java index 9767029d66..cb3decdf08 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java @@ -5,17 +5,10 @@ package org.opensearch.dataprepper.typeconverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Objects; -import static org.opensearch.dataprepper.logging.DataPrepperMarkers.EVENT; - public class StringConverter implements TypeConverter { - private static final Logger LOG = LoggerFactory.getLogger(StringConverter.class); - - public String convert(final Object source) { + public String convert(final Object source) throws IllegalArgumentException { if (source instanceof Long) { return Long.toString(((Number)source).longValue()); } @@ -34,11 +27,9 @@ public String convert(final Object source) { if (source instanceof String) { return (String)source; } - LOG.error(EVENT, "Unable to convert {} to String", source); - if (Objects.nonNull(source)) { - return source.toString(); - } else { - return ""; + if (Objects.isNull(source)) { + return "null"; } + throw new IllegalArgumentException("Unsupported type conversion"); } } diff --git a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java index 63c22f98fd..bd0f14415f 100644 --- a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java +++ b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java @@ -7,11 +7,12 @@ import org.junit.jupiter.api.Test; -import java.util.List; +import java.util.Collections; import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; public class StringConverterTests { @Test @@ -65,23 +66,14 @@ void testStringToStringConversion() { @Test void testNullValueStringConversion() { StringConverter converter = new StringConverter(); - final String expectedString = ""; + final String expectedString = "null"; assertThat(converter.convert(null), equalTo(expectedString)); } @Test - void testMapToStringConversion() { + void testInvalidStringConversion() { StringConverter converter = new StringConverter(); - final Map map = Map.of("testKey", "testValue"); - final String expectedString = "{testKey=testValue}"; - assertThat(converter.convert(map), equalTo(expectedString)); - } - - @Test - void testListToStringConversion() { - StringConverter converter = new StringConverter(); - final List list = List.of("listItem1", "listItem2"); - final String expectedString = "[listItem1, listItem2]"; - assertThat(converter.convert(list), equalTo(expectedString)); + final Map map = Collections.emptyMap(); + assertThrows(IllegalArgumentException.class, () -> converter.convert(map)); } } diff --git a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java index d8806bde6c..f48c9d9092 100644 --- a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java +++ b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java @@ -14,18 +14,25 @@ import org.opensearch.dataprepper.model.processor.Processor; import org.opensearch.dataprepper.model.record.Record; import org.opensearch.dataprepper.typeconverter.TypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import static org.opensearch.dataprepper.logging.DataPrepperMarkers.EVENT; + @DataPrepperPlugin(name = "convert_entry_type", pluginType = Processor.class, pluginConfigurationType = ConvertEntryTypeProcessorConfig.class) public class ConvertEntryTypeProcessor extends AbstractProcessor, Record> { + private static final Logger LOG = LoggerFactory.getLogger(ConvertEntryTypeProcessor.class); private final List convertEntryKeys; private final TypeConverter converter; private final String convertWhen; private final List nullValues; + private final String type; + private final List tagsOnFailure; private final ExpressionEvaluator expressionEvaluator; @@ -35,11 +42,13 @@ public ConvertEntryTypeProcessor(final PluginMetrics pluginMetrics, final ExpressionEvaluator expressionEvaluator) { super(pluginMetrics); this.convertEntryKeys = getKeysToConvert(convertEntryTypeProcessorConfig); + this.type = convertEntryTypeProcessorConfig.getType().name(); this.converter = convertEntryTypeProcessorConfig.getType().getTargetConverter(); this.convertWhen = convertEntryTypeProcessorConfig.getConvertWhen(); this.nullValues = convertEntryTypeProcessorConfig.getNullValues() .orElse(List.of()); this.expressionEvaluator = expressionEvaluator; + this.tagsOnFailure = convertEntryTypeProcessorConfig.getTagsOnFailure(); } @Override @@ -56,7 +65,12 @@ public Collection> doExecute(final Collection> recor if (keyVal != null) { recordEvent.delete(key); if (!nullValues.contains(keyVal.toString())) { - recordEvent.put(key, this.converter.convert(keyVal)); + try { + recordEvent.put(key, converter.convert(keyVal)); + } catch (final RuntimeException e) { + LOG.error(EVENT, "Unable to convert key: {} with value: {} to {}", key, keyVal, type, e); + recordEvent.getMetadata().addTags(tagsOnFailure); + } } } } diff --git a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorConfig.java b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorConfig.java index 16f53b324d..07183f7bcf 100644 --- a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorConfig.java +++ b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorConfig.java @@ -26,6 +26,9 @@ public class ConvertEntryTypeProcessorConfig { @JsonProperty("null_values") private List nullValues; + @JsonProperty("tags_on_failure") + private List tagsOnFailure; + public String getKey() { return key; } @@ -41,4 +44,8 @@ public TargetType getType() { public Optional> getNullValues(){ return Optional.ofNullable(nullValues); } + + public List getTagsOnFailure() { + return tagsOnFailure; + } } diff --git a/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java b/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java index 1bddb03718..578495280a 100644 --- a/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java +++ b/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.UUID; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -103,11 +104,17 @@ void testBooleanToIntegerConvertEntryTypeProcessor() { } @Test - void testIntegerConvertEntryTypeProcessorWithInvalidType() { - Map testValue = Map.of("key", "value"); - when(mockConfig.getType()).thenReturn(TargetType.fromOptionValue("integer")); + void testMapToStringConvertEntryTypeProcessorWithInvalidTypeWillAddTags() { + final Map testValue = Map.of("key", "value"); + final List tags = List.of("convert_failed"); + when(mockConfig.getType()).thenReturn(TargetType.fromOptionValue("string")); + when(mockConfig.getTagsOnFailure()).thenReturn(tags); typeConversionProcessor = new ConvertEntryTypeProcessor(pluginMetrics, mockConfig, expressionEvaluator); - assertThrows(IllegalArgumentException.class, () -> executeAndGetProcessedEvent(testValue)); + Event event = executeAndGetProcessedEvent(testValue); + + assertThat(event.get(TEST_KEY, Object.class), equalTo(null)); + assertThat(event.getMetadata().getTags().size(), equalTo(1)); + assertThat(event.getMetadata().getTags(), containsInAnyOrder(tags.toArray())); } @Test @@ -178,11 +185,17 @@ void testBooleanToStringConvertEntryTypeProcessor() { } @Test - void testInvalidConvertEntryTypeProcessor() { - Double testDoubleValue = (double)123.789; + void testDoubleToIntegerConvertEntryTypeProcessorWillAddTags() { + final Double testDoubleValue = 123.789; + final List tags = List.of("convert_failed"); when(mockConfig.getType()).thenReturn(TargetType.fromOptionValue("integer")); + when(mockConfig.getTagsOnFailure()).thenReturn(tags); typeConversionProcessor = new ConvertEntryTypeProcessor(pluginMetrics, mockConfig, expressionEvaluator); - assertThrows(IllegalArgumentException.class, () -> executeAndGetProcessedEvent(testDoubleValue)); + Event event = executeAndGetProcessedEvent(testDoubleValue); + + assertThat(event.get(TEST_KEY, Object.class), equalTo(null)); + assertThat(event.getMetadata().getTags().size(), equalTo(1)); + assertThat(event.getMetadata().getTags(), containsInAnyOrder(tags.toArray())); } @Test From 72d4584ed71d8f83f4b366c39a6e8ffad1c169a3 Mon Sep 17 00:00:00 2001 From: Asif Sohail Mohammed Date: Mon, 21 Aug 2023 12:53:25 -0500 Subject: [PATCH 3/3] Addressed feedback Signed-off-by: Asif Sohail Mohammed --- .../dataprepper/typeconverter/BooleanConverter.java | 2 +- .../dataprepper/typeconverter/DoubleConverter.java | 2 +- .../dataprepper/typeconverter/IntegerConverter.java | 2 +- .../dataprepper/typeconverter/StringConverter.java | 9 ++------- .../dataprepper/typeconverter/StringConverterTests.java | 6 ------ data-prepper-plugins/mutate-event-processors/README.md | 1 + .../processor/mutateevent/ConvertEntryTypeProcessor.java | 3 ++- .../mutateevent/ConvertEntryTypeProcessorTests.java | 4 ++-- 8 files changed, 10 insertions(+), 19 deletions(-) diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/BooleanConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/BooleanConverter.java index 289d6e1856..a24c0bdc64 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/BooleanConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/BooleanConverter.java @@ -22,6 +22,6 @@ public Boolean convert(Object source) throws IllegalArgumentException { if (source instanceof Boolean) { return (Boolean)source; } - throw new IllegalArgumentException("Unsupported type conversion"); + throw new IllegalArgumentException("Unsupported type conversion. Source class: " + source.getClass()); } } diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/DoubleConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/DoubleConverter.java index 4239b8e882..f769f8cd99 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/DoubleConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/DoubleConverter.java @@ -19,6 +19,6 @@ public Double convert(Object source) throws IllegalArgumentException { if (source instanceof Boolean) { return (double)(((Boolean)source) ? 1.0 : 0.0); } - throw new IllegalArgumentException("Unsupported type conversion"); + throw new IllegalArgumentException("Unsupported type conversion. Source class: " + source.getClass()); } } diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/IntegerConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/IntegerConverter.java index d373c4768e..c4202f53a1 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/IntegerConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/IntegerConverter.java @@ -19,6 +19,6 @@ public Integer convert(Object source) throws IllegalArgumentException { if (source instanceof Integer) { return (Integer)source; } - throw new IllegalArgumentException("Unsupported type conversion"); + throw new IllegalArgumentException("Unsupported type conversion. Source class: " + source.getClass()); } } diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java index cb3decdf08..b218503993 100644 --- a/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/typeconverter/StringConverter.java @@ -5,10 +5,8 @@ package org.opensearch.dataprepper.typeconverter; -import java.util.Objects; - public class StringConverter implements TypeConverter { - public String convert(final Object source) throws IllegalArgumentException { + public String convert(Object source) throws IllegalArgumentException { if (source instanceof Long) { return Long.toString(((Number)source).longValue()); } @@ -27,9 +25,6 @@ public String convert(final Object source) throws IllegalArgumentException { if (source instanceof String) { return (String)source; } - if (Objects.isNull(source)) { - return "null"; - } - throw new IllegalArgumentException("Unsupported type conversion"); + throw new IllegalArgumentException("Unsupported type conversion. Source class: " + source.getClass()); } } diff --git a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java index bd0f14415f..209f42cf64 100644 --- a/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java +++ b/data-prepper-api/src/test/java/org/opensearch/dataprepper/typeconverter/StringConverterTests.java @@ -63,12 +63,6 @@ void testStringToStringConversion() { final String strConstant = "testString"; assertThat(converter.convert(strConstant), equalTo(strConstant)); } - @Test - void testNullValueStringConversion() { - StringConverter converter = new StringConverter(); - final String expectedString = "null"; - assertThat(converter.convert(null), equalTo(expectedString)); - } @Test void testInvalidStringConversion() { diff --git a/data-prepper-plugins/mutate-event-processors/README.md b/data-prepper-plugins/mutate-event-processors/README.md index bcc869ba08..eb1398e154 100644 --- a/data-prepper-plugins/mutate-event-processors/README.md +++ b/data-prepper-plugins/mutate-event-processors/README.md @@ -266,6 +266,7 @@ and the type conversion processor will change it to the following output, where * `type` - target type for the value of the key. Possible values are `integer`, `double`, `string`, and `boolean`. Default is `integer`. * `null_values` - treat any value in the null_values list as null. * Example: `null_values` is `["-"]` and `key` is `key1`. `{"key1": "-", "key2": "value2"}` will parse into `{"key2": "value2"}` +* `tags_on_failure`(Optional)- A `List` of `String`s that specifies the tags to be set in the event the processor fails to convert `key` or `keys` to configured `type`. These tags may be used in conditional expressions in other parts of the configuration ## List-to-map Processor A processor that converts a list of objects from an event, where each object has a key field, to a map of keys to objects. diff --git a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java index f48c9d9092..8ba0400734 100644 --- a/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java +++ b/data-prepper-plugins/mutate-event-processors/src/main/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessor.java @@ -63,7 +63,6 @@ public Collection> doExecute(final Collection> recor for(final String key : convertEntryKeys) { Object keyVal = recordEvent.get(key, Object.class); if (keyVal != null) { - recordEvent.delete(key); if (!nullValues.contains(keyVal.toString())) { try { recordEvent.put(key, converter.convert(keyVal)); @@ -71,6 +70,8 @@ public Collection> doExecute(final Collection> recor LOG.error(EVENT, "Unable to convert key: {} with value: {} to {}", key, keyVal, type, e); recordEvent.getMetadata().addTags(tagsOnFailure); } + } else { + recordEvent.delete(key); } } } diff --git a/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java b/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java index 578495280a..6985c776a0 100644 --- a/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java +++ b/data-prepper-plugins/mutate-event-processors/src/test/java/org/opensearch/dataprepper/plugins/processor/mutateevent/ConvertEntryTypeProcessorTests.java @@ -112,7 +112,7 @@ void testMapToStringConvertEntryTypeProcessorWithInvalidTypeWillAddTags() { typeConversionProcessor = new ConvertEntryTypeProcessor(pluginMetrics, mockConfig, expressionEvaluator); Event event = executeAndGetProcessedEvent(testValue); - assertThat(event.get(TEST_KEY, Object.class), equalTo(null)); + assertThat(event.get(TEST_KEY, Object.class), equalTo(testValue)); assertThat(event.getMetadata().getTags().size(), equalTo(1)); assertThat(event.getMetadata().getTags(), containsInAnyOrder(tags.toArray())); } @@ -193,7 +193,7 @@ void testDoubleToIntegerConvertEntryTypeProcessorWillAddTags() { typeConversionProcessor = new ConvertEntryTypeProcessor(pluginMetrics, mockConfig, expressionEvaluator); Event event = executeAndGetProcessedEvent(testDoubleValue); - assertThat(event.get(TEST_KEY, Object.class), equalTo(null)); + assertThat(event.get(TEST_KEY, Object.class), equalTo(123.789)); assertThat(event.getMetadata().getTags().size(), equalTo(1)); assertThat(event.getMetadata().getTags(), containsInAnyOrder(tags.toArray())); }