mirror of https://github.com/apache/druid.git
Merge pull request #2715 from navis/stringformat-null-handling
stringFormat extractionFn should be able to return null on null values (Fix for #2706)
This commit is contained in:
commit
4eb5a2c4f1
|
@ -346,10 +346,10 @@ For example, `'/druid/prod/historical'` is transformed to `'the dru'` as regular
|
||||||
Returns the dimension value formatted according to the given format string.
|
Returns the dimension value formatted according to the given format string.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type" : "stringFormat", "format" : <sprintf_expression> }
|
{ "type" : "stringFormat", "format" : <sprintf_expression>, "nullHandling" : <optional attribute for handling null value> }
|
||||||
```
|
```
|
||||||
|
|
||||||
For example if you want to concat "[" and "]" before and after the actual dimension value, you need to specify "[%s]" as format string.
|
For example if you want to concat "[" and "]" before and after the actual dimension value, you need to specify "[%s]" as format string. "nullHandling" can be one of `nullString`, `emptyString` or `returnNull`. With "[%s]" format, each configuration will result `[null]`, `[]`, `null`. Default is `nullString`.
|
||||||
|
|
||||||
### Filtered DimensionSpecs
|
### Filtered DimensionSpecs
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ package io.druid.query.extraction;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.metamx.common.StringUtils;
|
import com.metamx.common.StringUtils;
|
||||||
|
@ -32,15 +33,42 @@ import java.nio.ByteBuffer;
|
||||||
*/
|
*/
|
||||||
public class StringFormatExtractionFn extends DimExtractionFn
|
public class StringFormatExtractionFn extends DimExtractionFn
|
||||||
{
|
{
|
||||||
|
public static enum NullHandling
|
||||||
|
{
|
||||||
|
NULLSTRING,
|
||||||
|
EMPTYSTRING,
|
||||||
|
RETURNNULL;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public static NullHandling forValue(String value)
|
||||||
|
{
|
||||||
|
return value == null ? NULLSTRING : NullHandling.valueOf(value.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String toValue()
|
||||||
|
{
|
||||||
|
return name().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final String format;
|
private final String format;
|
||||||
|
private final NullHandling nullHandling;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public StringFormatExtractionFn(
|
public StringFormatExtractionFn(
|
||||||
@JsonProperty("format") String format
|
@JsonProperty("format") String format,
|
||||||
|
@JsonProperty("nullHandling") NullHandling nullHandling
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(format), "format string should not be empty");
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(format), "format string should not be empty");
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
this.nullHandling = nullHandling == null ? NullHandling.NULLSTRING : nullHandling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringFormatExtractionFn(String format)
|
||||||
|
{
|
||||||
|
this(format, NullHandling.NULLSTRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -49,12 +77,19 @@ public class StringFormatExtractionFn extends DimExtractionFn
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
public NullHandling getNullHandling()
|
||||||
|
{
|
||||||
|
return nullHandling;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] getCacheKey()
|
public byte[] getCacheKey()
|
||||||
{
|
{
|
||||||
byte[] bytes = StringUtils.toUtf8(format);
|
byte[] bytes = StringUtils.toUtf8(format);
|
||||||
return ByteBuffer.allocate(1 + bytes.length)
|
return ByteBuffer.allocate(2 + bytes.length)
|
||||||
.put(ExtractionCacheHelper.CACHE_TYPE_ID_STRING_FORMAT)
|
.put(ExtractionCacheHelper.CACHE_TYPE_ID_STRING_FORMAT)
|
||||||
|
.put((byte) nullHandling.ordinal())
|
||||||
.put(bytes)
|
.put(bytes)
|
||||||
.array();
|
.array();
|
||||||
}
|
}
|
||||||
|
@ -62,7 +97,15 @@ public class StringFormatExtractionFn extends DimExtractionFn
|
||||||
@Override
|
@Override
|
||||||
public String apply(String value)
|
public String apply(String value)
|
||||||
{
|
{
|
||||||
return String.format(format, value);
|
if (value == null) {
|
||||||
|
if (nullHandling == NullHandling.RETURNNULL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (nullHandling == NullHandling.EMPTYSTRING) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Strings.emptyToNull(String.format(format, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -89,13 +132,26 @@ public class StringFormatExtractionFn extends DimExtractionFn
|
||||||
|
|
||||||
StringFormatExtractionFn that = (StringFormatExtractionFn) o;
|
StringFormatExtractionFn that = (StringFormatExtractionFn) o;
|
||||||
|
|
||||||
|
if (nullHandling != that.nullHandling) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return format.equals(that.format);
|
return format.equals(that.format);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode()
|
public int hashCode()
|
||||||
{
|
{
|
||||||
return format.hashCode();
|
int result = format.hashCode();
|
||||||
|
result = 31 * result + nullHandling.ordinal();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "StringFormatExtractionFn{" +
|
||||||
|
"format='" + format + '\'' +
|
||||||
|
", nullHandling=" + nullHandling +
|
||||||
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
package io.druid.query.extraction;
|
package io.druid.query.extraction;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.druid.jackson.DefaultObjectMapper;
|
import io.druid.jackson.DefaultObjectMapper;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -39,18 +40,54 @@ public class StringFormatExtractionFnTest
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testApplyNull() throws Exception
|
public void testApplyNull1() throws Exception
|
||||||
{
|
{
|
||||||
StringFormatExtractionFn fn = new StringFormatExtractionFn("[%s]");
|
|
||||||
String test = null;
|
String test = null;
|
||||||
Assert.assertEquals("[null]", fn.apply(test));
|
Assert.assertEquals("[null]", format("[%s]", "nullString").apply(test));
|
||||||
|
Assert.assertEquals("[]", format("[%s]", "emptyString").apply(test));
|
||||||
|
Assert.assertNull(format("[%s]", "returnNull").apply(test));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testApplyNull2() throws Exception
|
||||||
|
{
|
||||||
|
String test = null;
|
||||||
|
Assert.assertEquals("null", format("%s", "nullString").apply(test));
|
||||||
|
Assert.assertNull(format("%s", "emptyString").apply(test));
|
||||||
|
Assert.assertNull(format("%s", "returnNull").apply(test));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testInvalidOption1() throws Exception
|
||||||
|
{
|
||||||
|
new StringFormatExtractionFn("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerde() throws Exception
|
public void testSerde() throws Exception
|
||||||
|
{
|
||||||
|
validateSerde("{ \"type\" : \"stringFormat\", \"format\" : \"[%s]\" }");
|
||||||
|
validateSerde(
|
||||||
|
"{ \"type\" : \"stringFormat\", \"format\" : \"[%s]\", \"nullHandling\" : \"returnNull\" }"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = JsonMappingException.class)
|
||||||
|
public void testInvalidOption2() throws Exception
|
||||||
|
{
|
||||||
|
validateSerde(
|
||||||
|
"{ \"type\" : \"stringFormat\", \"format\" : \"[%s]\", \"nullHandling\" : \"invalid\" }"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringFormatExtractionFn format(String format, String nullHandling)
|
||||||
|
{
|
||||||
|
return new StringFormatExtractionFn(format, StringFormatExtractionFn.NullHandling.forValue(nullHandling));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSerde(String json) throws java.io.IOException
|
||||||
{
|
{
|
||||||
final ObjectMapper objectMapper = new DefaultObjectMapper();
|
final ObjectMapper objectMapper = new DefaultObjectMapper();
|
||||||
final String json = "{ \"type\" : \"stringFormat\", \"format\" : \"[%s]\" }";
|
|
||||||
StringFormatExtractionFn extractionFn = (StringFormatExtractionFn) objectMapper.readValue(json, ExtractionFn.class);
|
StringFormatExtractionFn extractionFn = (StringFormatExtractionFn) objectMapper.readValue(json, ExtractionFn.class);
|
||||||
|
|
||||||
Assert.assertEquals("[%s]", extractionFn.getFormat());
|
Assert.assertEquals("[%s]", extractionFn.getFormat());
|
||||||
|
|
Loading…
Reference in New Issue