summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Hugo Benichi <hugobenichi@google.com> 2017-06-23 10:07:08 +0900
committer Hugo Benichi <hugobenichi@google.com> 2017-07-14 04:26:42 +0900
commitfd31b9d46ea09dc7d74ddc03ab7e5ecfbe80b3dc (patch)
treef113067dca4697fea38ddfe75457ca59ebb4f6dc
parent5bb30496cf381c6546cbf02a2c5b55adf046aeb8 (diff)
IpManager: define InitialConfiguration
This patch adds a InitialConfiguration class to IpManager for specifying IP information in IpManager ProvisioningConfiguration at IpManager startup. At the moment this InitialConfiguration is not used, but is validated in startProvsiioning if ProvisioningConfiguration includes one. It will be integrated into IpManager IP provisioning logic in follow-up patches. This patch also includes an example of data driven unit tests using a table of test case. The highlights of this methodology are: 1) easy extensibility for new test case, 2) rich and informative error messages, Unfortunately Java support for inlined data structure literals is poor and some companion static methods for data generation are required for enabling this methodology. Bug: 62988545 Test: added new test in FrameworksNetTests, $ runtest frameworks-net $ runtest frameworks-wifi Merged-In: I060b02603af7d73a6407df89344bf0c000574af2 (cherry pick of commit 2757fcf3a13b0addc4a168a12c72ac2fc418b012) Change-Id: I48dbf89232d7758f1b07ed4d76ce93281e5c6b53
-rw-r--r--core/java/android/net/LinkAddress.java20
-rw-r--r--core/java/android/net/metrics/IpManagerEvent.java3
-rw-r--r--services/net/java/android/net/ip/IpManager.java141
-rw-r--r--services/net/java/android/net/util/NetworkConstants.java1
-rw-r--r--tests/net/java/android/net/ip/IpManagerTest.java178
5 files changed, 326 insertions, 17 deletions
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index 6e74f14bd138..62de9911bc48 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -16,6 +16,15 @@
package android.net;
+import static android.system.OsConstants.IFA_F_DADFAILED;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_OPTIMISTIC;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
@@ -26,15 +35,6 @@ import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.UnknownHostException;
-import static android.system.OsConstants.IFA_F_DADFAILED;
-import static android.system.OsConstants.IFA_F_DEPRECATED;
-import static android.system.OsConstants.IFA_F_OPTIMISTIC;
-import static android.system.OsConstants.IFA_F_TENTATIVE;
-import static android.system.OsConstants.RT_SCOPE_HOST;
-import static android.system.OsConstants.RT_SCOPE_LINK;
-import static android.system.OsConstants.RT_SCOPE_SITE;
-import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
-
/**
* Identifies an IP address on a network link.
*
@@ -101,7 +101,7 @@ public class LinkAddress implements Parcelable {
* Per RFC 4193 section 8, fc00::/7 identifies these addresses.
*/
private boolean isIPv6ULA() {
- if (address != null && address instanceof Inet6Address) {
+ if (address instanceof Inet6Address) {
byte[] bytes = address.getAddress();
return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
}
diff --git a/core/java/android/net/metrics/IpManagerEvent.java b/core/java/android/net/metrics/IpManagerEvent.java
index e0a026ed678d..007846e8c1ae 100644
--- a/core/java/android/net/metrics/IpManagerEvent.java
+++ b/core/java/android/net/metrics/IpManagerEvent.java
@@ -42,10 +42,13 @@ public final class IpManagerEvent implements Parcelable {
/** @hide */ public static final int ERROR_STARTING_IPV6 = 5;
/** @hide */ public static final int ERROR_STARTING_IPREACHABILITYMONITOR = 6;
+ /** @hide */ public static final int ERROR_INVALID_PROVISIONING = 7;
+
/** {@hide} */
@IntDef(value = {
PROVISIONING_OK, PROVISIONING_FAIL, COMPLETE_LIFECYCLE,
ERROR_STARTING_IPV4, ERROR_STARTING_IPV6, ERROR_STARTING_IPREACHABILITYMONITOR,
+ ERROR_INVALID_PROVISIONING,
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType {}
diff --git a/services/net/java/android/net/ip/IpManager.java b/services/net/java/android/net/ip/IpManager.java
index b2930a4d536d..adaf59974caf 100644
--- a/services/net/java/android/net/ip/IpManager.java
+++ b/services/net/java/android/net/ip/IpManager.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.net.DhcpResults;
import android.net.INetd;
import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties.ProvisioningChange;
import android.net.LinkProperties;
@@ -35,6 +36,7 @@ import android.net.dhcp.DhcpClient;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpManagerEvent;
import android.net.util.MultinetworkPolicyTracker;
+import android.net.util.NetworkConstants;
import android.net.util.SharedLog;
import android.os.INetworkManagementService;
import android.os.Message;
@@ -51,17 +53,25 @@ import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.IState;
+import com.android.internal.util.Preconditions;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.net.NetlinkTracker;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.Objects;
+import java.util.Set;
import java.util.StringJoiner;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
@@ -308,6 +318,11 @@ public class IpManager extends StateMachine {
return this;
}
+ public Builder withInitialConfiguration(InitialConfiguration initialConfig) {
+ mConfig.mInitialConfig = initialConfig;
+ return this;
+ }
+
public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) {
mConfig.mStaticIpConfig = staticConfig;
return this;
@@ -342,18 +357,20 @@ public class IpManager extends StateMachine {
/* package */ boolean mEnableIPv6 = true;
/* package */ boolean mUsingIpReachabilityMonitor = true;
/* package */ int mRequestedPreDhcpActionMs;
+ /* package */ InitialConfiguration mInitialConfig;
/* package */ StaticIpConfiguration mStaticIpConfig;
/* package */ ApfCapabilities mApfCapabilities;
/* package */ int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
/* package */ int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
- public ProvisioningConfiguration() {}
+ public ProvisioningConfiguration() {} // used by Builder
public ProvisioningConfiguration(ProvisioningConfiguration other) {
mEnableIPv4 = other.mEnableIPv4;
mEnableIPv6 = other.mEnableIPv6;
mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
+ mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
mStaticIpConfig = other.mStaticIpConfig;
mApfCapabilities = other.mApfCapabilities;
mProvisioningTimeoutMs = other.mProvisioningTimeoutMs;
@@ -366,12 +383,124 @@ public class IpManager extends StateMachine {
.add("mEnableIPv6: " + mEnableIPv6)
.add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
.add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
+ .add("mInitialConfig: " + mInitialConfig)
.add("mStaticIpConfig: " + mStaticIpConfig)
.add("mApfCapabilities: " + mApfCapabilities)
.add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs)
.add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
.toString();
}
+
+ public boolean isValid() {
+ return (mInitialConfig == null) || mInitialConfig.isValid();
+ }
+ }
+
+ public static class InitialConfiguration {
+ public final Set<LinkAddress> ipAddresses = new HashSet<>();
+ public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>();
+ public final Set<InetAddress> dnsServers = new HashSet<>();
+ public Inet4Address gateway; // WiFi legacy behavior with static ipv4 config
+
+ public static InitialConfiguration copy(InitialConfiguration config) {
+ if (config == null) {
+ return null;
+ }
+ InitialConfiguration configCopy = new InitialConfiguration();
+ configCopy.ipAddresses.addAll(config.ipAddresses);
+ configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes);
+ configCopy.dnsServers.addAll(config.dnsServers);
+ return configCopy;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s}, v4 gateway: %s)",
+ join(", ", ipAddresses), join(", ", directlyConnectedRoutes),
+ join(", ", dnsServers), gateway);
+ }
+
+ public boolean isValid() {
+ // For every IP address, there must be at least one prefix containing that address.
+ for (LinkAddress addr : ipAddresses) {
+ if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) {
+ return false;
+ }
+ }
+ // For every dns server, there must be at least one prefix containing that address.
+ for (InetAddress addr : dnsServers) {
+ if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) {
+ return false;
+ }
+ }
+ // All IPv6 LinkAddresses have an RFC7421-suitable prefix length
+ // (read: compliant with RFC4291#section2.5.4).
+ if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) {
+ return false;
+ }
+ // If directlyConnectedRoutes contains an IPv6 default route
+ // then ipAddresses MUST contain at least one non-ULA GUA.
+ if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute)
+ && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) {
+ return false;
+ }
+ // The prefix length of routes in directlyConnectedRoutes be within reasonable
+ // bounds for IPv6: /48-/64 just as we’d accept in RIOs.
+ if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) {
+ return false;
+ }
+ // There no more than one IPv4 address
+ if (ipAddresses.stream().filter(Inet4Address.class::isInstance).count() > 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isPrefixLengthCompliant(LinkAddress addr) {
+ return (addr.getAddress() instanceof Inet4Address)
+ || isCompliantIPv6PrefixLength(addr.getPrefixLength());
+ }
+
+ private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
+ return (prefix.getAddress() instanceof Inet4Address)
+ || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
+ }
+
+ private static boolean isCompliantIPv6PrefixLength(int prefixLength) {
+ return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength)
+ && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH);
+ }
+
+ private static boolean isIPv6DefaultRoute(IpPrefix prefix) {
+ return prefix.getAddress().equals(Inet6Address.ANY);
+ }
+
+ private static boolean isIPv6GUA(LinkAddress addr) {
+ return (addr.getAddress() instanceof Inet6Address) && addr.isGlobalPreferred();
+ }
+
+ private static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+ for (T t : coll) {
+ if (fn.test(t)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+ return !any(coll, not(fn));
+ }
+
+ private static <T> Predicate<T> not(Predicate<T> fn) {
+ return (t) -> !fn.test(t);
+ }
+
+ private static <T> String join(String delimiter, Collection<T> coll) {
+ return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
+ }
}
public static final String DUMP_ARG = "ipmanager";
@@ -436,8 +565,7 @@ public class IpManager extends StateMachine {
private boolean mMulticastFiltering;
private long mStartTimeMillis;
- public IpManager(Context context, String ifName, Callback callback)
- throws IllegalArgumentException {
+ public IpManager(Context context, String ifName, Callback callback) {
this(context, ifName, callback, INetworkManagementService.Stub.asInterface(
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)));
}
@@ -446,7 +574,7 @@ public class IpManager extends StateMachine {
* An expanded constructor, useful for dependency injection.
*/
public IpManager(Context context, String ifName, Callback callback,
- INetworkManagementService nwService) throws IllegalArgumentException {
+ INetworkManagementService nwService) {
super(IpManager.class.getSimpleName() + "." + ifName);
mTag = getName();
@@ -563,6 +691,11 @@ public class IpManager extends StateMachine {
}
public void startProvisioning(ProvisioningConfiguration req) {
+ if (!req.isValid()) {
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+ return;
+ }
+
getNetworkInterface();
mCallback.setNeighborDiscoveryOffload(true);
diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java
index a012e0cb0547..9b3bc3f0301a 100644
--- a/services/net/java/android/net/util/NetworkConstants.java
+++ b/services/net/java/android/net/util/NetworkConstants.java
@@ -102,6 +102,7 @@ public final class NetworkConstants {
public static final int IPV6_ADDR_LEN = 16;
public static final int IPV6_MIN_MTU = 1280;
public static final int RFC7421_PREFIX_LENGTH = 64;
+ public static final int RFC6177_MIN_PREFIX_LENGTH = 48;
/**
* ICMPv6 constants.
diff --git a/tests/net/java/android/net/ip/IpManagerTest.java b/tests/net/java/android/net/ip/IpManagerTest.java
index 025b01740aad..e7dbfe3f5044 100644
--- a/tests/net/java/android/net/ip/IpManagerTest.java
+++ b/tests/net/java/android/net/ip/IpManagerTest.java
@@ -16,11 +16,26 @@
package android.net.ip;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.AlarmManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.ip.IpManager.Callback;
+import android.net.ip.IpManager.InitialConfiguration;
+import android.net.ip.IpManager.ProvisioningConfiguration;
import android.os.INetworkManagementService;
import android.provider.Settings;
import android.support.test.filters.SmallTest;
@@ -31,11 +46,17 @@ import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.R;
import org.junit.Before;
-import org.junit.runner.RunWith;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Tests for IpManager.
*/
@@ -44,14 +65,20 @@ import org.mockito.MockitoAnnotations;
public class IpManagerTest {
private static final int DEFAULT_AVOIDBADWIFI_CONFIG_VALUE = 1;
+ private static final String VALID = "VALID";
+ private static final String INVALID = "INVALID";
+
@Mock private Context mContext;
@Mock private INetworkManagementService mNMService;
@Mock private Resources mResources;
+ @Mock private Callback mCb;
+ @Mock private AlarmManager mAlarm;
private MockContentResolver mContentResolver;
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
when(mContext.getResources()).thenReturn(mResources);
when(mResources.getInteger(R.integer.config_networkAvoidBadWifi))
.thenReturn(DEFAULT_AVOIDBADWIFI_CONFIG_VALUE);
@@ -68,7 +95,152 @@ public class IpManagerTest {
@Test
public void testInvalidInterfaceDoesNotThrow() throws Exception {
- final IpManager.Callback cb = new IpManager.Callback();
- final IpManager ipm = new IpManager(mContext, "test_wlan0", cb, mNMService);
+ final IpManager ipm = new IpManager(mContext, "test_wlan0", mCb, mNMService);
+ }
+
+ @Test
+ public void testDefaultProvisioningConfiguration() throws Exception {
+ final String iface = "test_wlan0";
+ final IpManager ipm = new IpManager(mContext, iface, mCb, mNMService);
+ ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+ .withoutIPv4()
+ // TODO: mock IpReachabilityMonitor's dependencies (NetworkInterface, PowerManager)
+ // and enable it in this test
+ .withoutIpReachabilityMonitor()
+ .build();
+
+ ipm.startProvisioning(config);
+ verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+ verify(mCb, timeout(100).times(1)).setFallbackMulticastFilter(false);
+ verify(mCb, never()).onProvisioningFailure(any());
+
+ ipm.stop();
+ verify(mNMService, timeout(100).times(1)).disableIpv6(iface);
+ verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface);
+ }
+
+ @Test
+ public void testInitialConfigurations() throws Exception {
+ InitialConfigurationTestCase[] testcases = {
+ validConf("valid IPv4 configuration",
+ links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("192.0.2.2")),
+ validConf("another valid IPv4 configuration",
+ links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns()),
+ validConf("valid IPv6 configurations",
+ links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+ prefixes("2001:db8:dead:beef::/64", "fe80::/64"),
+ dns("2001:db8:dead:beef:f00::02")),
+ validConf("valid IPv6 configurations",
+ links("fe80::1/64"), prefixes("fe80::/64"), dns()),
+ validConf("valid IPv6/v4 configuration",
+ links("2001:db8:dead:beef:f00::a0/48", "192.0.2.12/24"),
+ prefixes("2001:db8:dead:beef::/64", "192.0.2.0/24"),
+ dns("192.0.2.2", "2001:db8:dead:beef:f00::02")),
+ validConf("valid IPv6 configuration without any GUA.",
+ links("fd00:1234:5678::1/48"),
+ prefixes("fd00:1234:5678::/48"),
+ dns("fd00:1234:5678::1000")),
+
+ invalidConf("v4 addr and dns not in any prefix",
+ links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
+ invalidConf("v4 addr not in any prefix",
+ links("198.51.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
+ invalidConf("v4 dns addr not in any prefix",
+ links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("198.51.100.2")),
+ invalidConf("v6 addr not in any prefix",
+ links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+ prefixes("2001:db8:dead:beef::/64"),
+ dns("2001:db8:dead:beef:f00::02")),
+ invalidConf("v6 dns addr not in any prefix",
+ links("fe80::1/64"), prefixes("fe80::/64"), dns("2001:db8:dead:beef:f00::02")),
+ invalidConf("default ipv6 route and no GUA",
+ links("fd01:1111:2222:3333::a0/128"), prefixes("::/0"), dns()),
+ invalidConf("invalid v6 prefix length",
+ links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/32"),
+ dns()),
+ invalidConf("another invalid v6 prefix length",
+ links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/72"),
+ dns())
+ };
+
+ for (InitialConfigurationTestCase testcase : testcases) {
+ if (testcase.config.isValid() != testcase.isValid) {
+ fail(testcase.errorMessage());
+ }
+ }
+ }
+
+ static class InitialConfigurationTestCase {
+ String descr;
+ boolean isValid;
+ InitialConfiguration config;
+ public String errorMessage() {
+ return String.format("%s: expected configuration %s to be %s, but was %s",
+ descr, config, validString(isValid), validString(!isValid));
+ }
+ }
+
+ static String validString(boolean isValid) {
+ return isValid ? VALID : INVALID;
+ }
+
+ static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links,
+ Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+ return confTestCase(descr, true, conf(links, prefixes, dns));
+ }
+
+ static InitialConfigurationTestCase invalidConf(String descr, Set<LinkAddress> links,
+ Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+ return confTestCase(descr, false, conf(links, prefixes, dns));
+ }
+
+ static InitialConfigurationTestCase confTestCase(
+ String descr, boolean isValid, InitialConfiguration config) {
+ InitialConfigurationTestCase testcase = new InitialConfigurationTestCase();
+ testcase.descr = descr;
+ testcase.isValid = isValid;
+ testcase.config = config;
+ return testcase;
+ }
+
+ static InitialConfiguration conf(
+ Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+ InitialConfiguration conf = new InitialConfiguration();
+ conf.ipAddresses.addAll(links);
+ conf.directlyConnectedRoutes.addAll(prefixes);
+ conf.dnsServers.addAll(dns);
+ return conf;
+ }
+
+ static Set<IpPrefix> prefixes(String... prefixes) {
+ return mapIntoSet(prefixes, IpPrefix::new);
+ }
+
+ static Set<LinkAddress> links(String... addresses) {
+ return mapIntoSet(addresses, LinkAddress::new);
+ }
+
+ static Set<InetAddress> ips(String... addresses) {
+ return mapIntoSet(addresses, InetAddress::getByName);
+ }
+
+ static Set<InetAddress> dns(String... addresses) {
+ return ips(addresses);
+ }
+
+ static <A, B> Set<B> mapIntoSet(A[] in, Fn<A, B> fn) {
+ Set<B> out = new HashSet<>(in.length);
+ for (A item : in) {
+ try {
+ out.add(fn.call(item));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return out;
+ }
+
+ interface Fn<A,B> {
+ B call(A a) throws Exception;
}
}