diff --git a/docs/changelog/81396.yaml b/docs/changelog/81396.yaml new file mode 100644 index 0000000000000..5cfc65f80ebba --- /dev/null +++ b/docs/changelog/81396.yaml @@ -0,0 +1,5 @@ +pr: 81396 +summary: "Script: fields API for IP mapped type" +area: Infra/Scripting +type: enhancement +issues: [] diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.net.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.net.txt index 6219d5ab99725..1d16b7951c834 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.net.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.net.txt @@ -6,6 +6,12 @@ # Side Public License, v 1. # +class org.elasticsearch.script.field.IPAddress { + (String) + boolean isV4() + boolean isV6() +} + class org.elasticsearch.painless.api.CIDR { (String) boolean contains(String) diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt index c71a87732a5b5..8d7e4ecf720bc 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt @@ -82,3 +82,11 @@ class org.elasticsearch.script.field.KeywordDocValuesField @dynamic_type { String get(String) String get(int, String) } + +class org.elasticsearch.script.field.IpDocValuesField @dynamic_type { + IPAddress get(IPAddress) + IPAddress get(int, IPAddress) + List asStrings() + String asString(String) + String asString(int, String) +} diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt index 907a98be5b973..7de0bb4a19dc7 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt @@ -122,11 +122,6 @@ class org.apache.lucene.util.BytesRef { String utf8ToString() } -class org.elasticsearch.index.mapper.IpFieldMapper$IpFieldType$IpScriptDocValues { - String get(int) - String getValue() -} - class org.elasticsearch.search.lookup.FieldLookup { def getValue() List getValues() diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml index 8a1f9702f3214..fa541be1d3e5c 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml @@ -53,7 +53,7 @@ setup: date: 2017-01-01T12:11:12 nanos: 2015-01-01T12:10:30.123456789Z geo_point: 41.12,-71.34 - ip: 192.168.0.1 + ip: 192.168.0.19 keyword: not split at all long: 12348732141234 integer: 134134566 @@ -79,6 +79,7 @@ setup: body: rank: 3 boolean: [true, false, true] + ip: ["10.1.2.3", "2001:db8::2:1"] date: [2017-01-01T12:11:12, 2018-01-01T12:11:12] nanos: [2015-01-01T12:10:30.123456789Z, 2015-01-01T12:10:30.987654321Z] keyword: ["one string", "another string"] @@ -526,7 +527,7 @@ setup: field: script: source: "doc['ip'].get(0)" - - match: { hits.hits.0.fields.field.0: "192.168.0.1" } + - match: { hits.hits.0.fields.field.0: "192.168.0.19" } - do: search: @@ -537,7 +538,76 @@ setup: field: script: source: "doc['ip'].value" - - match: { hits.hits.0.fields.field.0: "192.168.0.1" } + - match: { hits.hits.0.fields.field.0: "192.168.0.19" } + + - do: + search: + rest_total_hits_as_int: true + body: + sort: [ { rank: asc } ] + script_fields: + field: + script: + source: "field('ip').get(new IPAddress('127.0.0.1'))" + field_string: + script: + source: "field('ip').asString('127.0.0.1')" + - match: { hits.hits.0.fields.field.0: "192.168.0.19" } + - match: { hits.hits.0.fields.field_string.0: "192.168.0.19" } + - match: { hits.hits.1.fields.field.0: "127.0.0.1" } + - match: { hits.hits.1.fields.field_string.0: "127.0.0.1" } + - match: { hits.hits.2.fields.field.0: "10.1.2.3" } + - match: { hits.hits.2.fields.field_string.0: "10.1.2.3" } + + - do: + search: + rest_total_hits_as_int: true + body: + sort: [ { rank: asc } ] + script_fields: + field: + script: + source: "field('ip').get(1, new IPAddress('127.0.0.1'))" + field_string: + script: + source: "field('ip').asString(1, '127.0.0.1')" + - match: { hits.hits.0.fields.field.0: "127.0.0.1" } + - match: { hits.hits.0.fields.field_string.0: "127.0.0.1" } + - match: { hits.hits.1.fields.field.0: "127.0.0.1" } + - match: { hits.hits.1.fields.field_string.0: "127.0.0.1" } + - match: { hits.hits.2.fields.field.0: "2001:db8::2:1" } + - match: { hits.hits.2.fields.field_string.0: "2001:db8::2:1" } + + - do: + search: + rest_total_hits_as_int: true + body: + sort: [ { rank: asc } ] + script_fields: + field: + script: + source: "String.join(',', field('ip').asStrings())" + - match: { hits.hits.0.fields.field.0: "192.168.0.19" } + - match: { hits.hits.1.fields.field.0: "" } + - match: { hits.hits.2.fields.field.0: "10.1.2.3,2001:db8::2:1" } + + - do: + search: + rest_total_hits_as_int: true + body: + sort: [ { rank: asc } ] + runtime_mappings: + ip_script_field: + type: ip + script: + source: "for (IPAddress addr : field('ip')) { String ip = addr.toString(); emit(ip.substring(0, ip.length() - 1) + field('ip').size()) }" + script_fields: + field: + script: + source: "field('ip_script_field').get(new IPAddress('1.2.3.4'))" + - match: { hits.hits.0.fields.field.0: "192.168.0.11" } + - match: { hits.hits.1.fields.field.0: "1.2.3.4" } + - match: { hits.hits.2.fields.field.0: "10.1.2.2" } --- "keyword": diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/IpScriptFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/IpScriptFieldData.java index a0a34ed610288..dc3c4e7f9f121 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/IpScriptFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/IpScriptFieldData.java @@ -8,45 +8,39 @@ package org.elasticsearch.index.fielddata; -import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.network.InetAddresses; -import org.elasticsearch.index.fielddata.ScriptDocValues.Strings; -import org.elasticsearch.index.fielddata.ScriptDocValues.StringsSupplier; -import org.elasticsearch.index.mapper.IpFieldMapper; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.script.IpFieldScript; -import org.elasticsearch.script.field.DelegateDocValuesField; import org.elasticsearch.script.field.DocValuesField; +import org.elasticsearch.script.field.ToScriptField; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.aggregations.support.ValuesSourceType; -import java.net.InetAddress; - public class IpScriptFieldData extends BinaryScriptFieldData { public static class Builder implements IndexFieldData.Builder { private final String name; private final IpFieldScript.LeafFactory leafFactory; + private final ToScriptField toScriptField; - public Builder(String name, IpFieldScript.LeafFactory leafFactory) { + public Builder(String name, IpFieldScript.LeafFactory leafFactory, ToScriptField toScriptField) { this.name = name; this.leafFactory = leafFactory; + this.toScriptField = toScriptField; } @Override public IpScriptFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) { - return new IpScriptFieldData(name, leafFactory); + return new IpScriptFieldData(name, leafFactory, toScriptField); } } private final IpFieldScript.LeafFactory leafFactory; + private final ToScriptField toScriptField; - private IpScriptFieldData(String fieldName, IpFieldScript.LeafFactory leafFactory) { + private IpScriptFieldData(String fieldName, IpFieldScript.LeafFactory leafFactory, ToScriptField toScriptField) { super(fieldName); this.leafFactory = leafFactory; + this.toScriptField = toScriptField; } @Override @@ -55,7 +49,7 @@ public BinaryScriptLeafFieldData loadDirect(LeafReaderContext context) throws Ex return new BinaryScriptLeafFieldData() { @Override public DocValuesField getScriptField(String name) { - return new DelegateDocValuesField(new Strings(new IpSupplier(getBytesValues())), name); + return toScriptField.getScriptField(getBytesValues(), name); } @Override @@ -69,22 +63,4 @@ public SortedBinaryDocValues getBytesValues() { public ValuesSourceType getValuesSourceType() { return CoreValuesSourceType.IP; } - - /** - * Doc values supplier implementation for ips. We can't share - * {@link IpFieldMapper.IpFieldType.IpScriptDocValues} because it is based - * on global ordinals and we don't have those. - */ - public static class IpSupplier extends StringsSupplier { - - public IpSupplier(SortedBinaryDocValues in) { - super(in); - } - - @Override - protected String bytesToString(BytesRef bytesRef) { - InetAddress addr = InetAddressPoint.decode(BytesReference.toBytes(new BytesArray(bytesRef))); - return InetAddresses.toAddrString(addr); - } - } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index d978b1a130b42..ee39f775eeb20 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -13,10 +13,8 @@ import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.SortedSetDocValues; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesReference; @@ -27,14 +25,12 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.fielddata.IndexFieldData; -import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData; -import org.elasticsearch.index.mapper.IpFieldMapper.IpFieldType.IpScriptDocValues.IpSupplier; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.script.IpFieldScript; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptCompiler; -import org.elasticsearch.script.field.DelegateDocValuesField; +import org.elasticsearch.script.field.IpDocValuesField; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.lookup.FieldValues; @@ -44,7 +40,6 @@ import java.io.IOException; import java.net.InetAddress; import java.time.ZoneId; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -351,79 +346,10 @@ public static Query rangeQuery( return builder.apply(lower, upper); } - public static final class IpScriptDocValues extends ScriptDocValues { - - public static final class IpSupplier implements ScriptDocValues.Supplier { - - private final SortedSetDocValues in; - private long[] ords = new long[0]; - private int count; - - public IpSupplier(SortedSetDocValues in) { - this.in = in; - } - - @Override - public void setNextDocId(int docId) throws IOException { - count = 0; - if (in.advanceExact(docId)) { - for (long ord = in.nextOrd(); ord != SortedSetDocValues.NO_MORE_ORDS; ord = in.nextOrd()) { - ords = ArrayUtil.grow(ords, count + 1); - ords[count++] = ord; - } - } - } - - @Override - public String getInternal(int index) { - try { - BytesRef encoded = in.lookupOrd(ords[index]); - InetAddress address = InetAddressPoint.decode( - Arrays.copyOfRange(encoded.bytes, encoded.offset, encoded.offset + encoded.length) - ); - return InetAddresses.toAddrString(address); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public int size() { - return count; - } - } - - public IpScriptDocValues(IpSupplier supplier) { - super(supplier); - } - - public String getValue() { - if (supplier.size() == 0) { - return null; - } else { - return get(0); - } - } - - @Override - public String get(int index) { - return supplier.getInternal(index); - } - - @Override - public int size() { - return supplier.size(); - } - } - @Override public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier searchLookup) { failIfNoDocValues(); - return new SortedSetOrdinalsIndexFieldData.Builder( - name(), - CoreValuesSourceType.IP, - (dv, n) -> new DelegateDocValuesField(new IpScriptDocValues(new IpSupplier(dv)), n) - ); + return new SortedSetOrdinalsIndexFieldData.Builder(name(), CoreValuesSourceType.IP, IpDocValuesField::new); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpScriptFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/IpScriptFieldType.java index 5eb613f5c3b66..b8acd5b0cc953 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpScriptFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpScriptFieldType.java @@ -24,6 +24,7 @@ import org.elasticsearch.script.CompositeFieldScript; import org.elasticsearch.script.IpFieldScript; import org.elasticsearch.script.Script; +import org.elasticsearch.script.field.IpDocValuesField; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.runtime.IpScriptFieldExistsQuery; @@ -91,7 +92,7 @@ public DocValueFormat docValueFormat(String format, ZoneId timeZone) { @Override public IpScriptFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier searchLookup) { - return new IpScriptFieldData.Builder(name(), leafFactory(searchLookup.get())); + return new IpScriptFieldData.Builder(name(), leafFactory(searchLookup.get()), IpDocValuesField::new); } @Override diff --git a/server/src/main/java/org/elasticsearch/script/field/IPAddress.java b/server/src/main/java/org/elasticsearch/script/field/IPAddress.java new file mode 100644 index 0000000000000..dac236ccec275 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/field/IPAddress.java @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.script.field; + +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; + +/** + * IP address for use in scripting. + */ +public class IPAddress implements ToXContent { + protected final InetAddress address; + + IPAddress(InetAddress address) { + this.address = address; + } + + public IPAddress(String address) { + this.address = InetAddresses.forString(address); + } + + public boolean isV4() { + return address instanceof Inet4Address; + } + + public boolean isV6() { + return address instanceof Inet6Address; + } + + @Override + public String toString() { + return InetAddresses.toAddrString(address); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(this.toString()); + } + + @Override + public boolean isFragment() { + return false; + } +} diff --git a/server/src/main/java/org/elasticsearch/script/field/IpDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/IpDocValuesField.java new file mode 100644 index 0000000000000..4019d657cf3b3 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/field/IpDocValuesField.java @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.script.field; + +import org.apache.lucene.document.InetAddressPoint; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.index.fielddata.IpScriptFieldData; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class IpDocValuesField implements DocValuesField, ScriptDocValues.Supplier { + protected final String name; + protected final ScriptDocValues.Supplier raw; + + // used for backwards compatibility for old-style "doc" access + // as a delegate to this field class + protected ScriptDocValues.Strings strings = null; + + public IpDocValuesField(SortedSetDocValues input, String name) { + this.name = name; + this.raw = new SortedSetIpSupplier(input); + } + + public IpDocValuesField(SortedBinaryDocValues input, String name) { + this.name = name; + this.raw = new SortedBinaryIpSupplier(input); + } + + @Override + public void setNextDocId(int docId) throws IOException { + raw.setNextDocId(docId); + } + + @Override + public String getInternal(int index) { + return InetAddresses.toAddrString(raw.getInternal(index)); + } + + @Override + public ScriptDocValues getScriptDocValues() { + if (strings == null) { + strings = new ScriptDocValues.Strings(this); + } + + return strings; + } + + public String asString(String defaultValue) { + return asString(0, defaultValue); + } + + public String asString(int index, String defaultValue) { + if (isEmpty() || index < 0 || index >= size()) { + return defaultValue; + } + + return getInternal(index); + } + + public List asStrings() { + if (isEmpty()) { + return Collections.emptyList(); + } + + List values = new ArrayList<>(size()); + for (int i = 0; i < size(); i++) { + values.add(getInternal(i)); + } + + return values; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public int size() { + return raw.size(); + } + + public IPAddress get(IPAddress defaultValue) { + return get(0, defaultValue); + } + + public IPAddress get(int index, IPAddress defaultValue) { + if (isEmpty() || index < 0 || index >= size()) { + return defaultValue; + } + + return new IPAddress(raw.getInternal(index)); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private int index = 0; + + @Override + public boolean hasNext() { + return index < size(); + } + + @Override + public IPAddress next() { + if (hasNext() == false) { + throw new NoSuchElementException(); + } + return new IPAddress(raw.getInternal(index++)); + } + }; + } + + /** Used if we have access to global ordinals */ + protected static class SortedSetIpSupplier implements ScriptDocValues.Supplier { + private final SortedSetDocValues in; + private long[] ords = new long[0]; + private int count; + + public SortedSetIpSupplier(SortedSetDocValues in) { + this.in = in; + } + + @Override + public void setNextDocId(int docId) throws IOException { + count = 0; + if (in.advanceExact(docId)) { + for (long ord = in.nextOrd(); ord != SortedSetDocValues.NO_MORE_ORDS; ord = in.nextOrd()) { + ords = ArrayUtil.grow(ords, count + 1); + ords[count++] = ord; + } + } + } + + @Override + public InetAddress getInternal(int index) { + try { + BytesRef encoded = in.lookupOrd(ords[index]); + return InetAddressPoint.decode(Arrays.copyOfRange(encoded.bytes, encoded.offset, encoded.offset + encoded.length)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public int size() { + return count; + } + } + + /** Used if we do not have global ordinals, such as in the IP runtime field see: {@link IpScriptFieldData} */ + protected static class SortedBinaryIpSupplier implements ScriptDocValues.Supplier { + private final SortedBinaryDocValues in; + private BytesRefBuilder[] values = new BytesRefBuilder[0]; + private int count; + + public SortedBinaryIpSupplier(SortedBinaryDocValues in) { + this.in = in; + } + + @Override + public void setNextDocId(int docId) throws IOException { + if (in.advanceExact(docId)) { + resize(in.docValueCount()); + for (int i = 0; i < count; i++) { + // We need to make a copy here, because BytesBinaryDVLeafFieldData's SortedBinaryDocValues + // implementation reuses the returned BytesRef. Otherwise we would end up with the same BytesRef + // instance for all slots in the values array. + values[i].copyBytes(in.nextValue()); + } + } else { + resize(0); + } + } + + /** + * Set the {@link #size()} and ensure that the {@link #values} array can + * store at least that many entries. + */ + private void resize(int newSize) { + count = newSize; + if (newSize > values.length) { + final int oldLength = values.length; + values = ArrayUtil.grow(values, count); + for (int i = oldLength; i < values.length; ++i) { + values[i] = new BytesRefBuilder(); + } + } + } + + @Override + public InetAddress getInternal(int index) { + return InetAddressPoint.decode(BytesReference.toBytes(new BytesArray(values[index].toBytesRef()))); + } + + @Override + public int size() { + return count; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/script/field/IPAddressTests.java b/server/src/test/java/org/elasticsearch/script/field/IPAddressTests.java new file mode 100644 index 0000000000000..f6e6660151311 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/script/field/IPAddressTests.java @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.script.field; + +import org.elasticsearch.test.ESTestCase; + +public class IPAddressTests extends ESTestCase { + + public void testToString() { + String v4 = "192.168.7.255"; + assertEquals(v4, new IPAddress(v4).toString()); + String v6 = "b181:3a88:339c:97f5:2b40:5175:bf3d:f77e"; + assertEquals(v6, new IPAddress(v6).toString()); + } + + public void testV4() { + IPAddress addr4 = new IPAddress("169.254.0.0"); + assertTrue(addr4.isV4()); + assertFalse(addr4.isV6()); + } + + public void testV6() { + IPAddress addr4 = new IPAddress("b181:3a88:339c:97f5:2b40:5175:bf3d:f77e"); + assertFalse(addr4.isV4()); + assertTrue(addr4.isV6()); + } +}