|
@@ -0,0 +1,502 @@
|
|
|
+/*
|
|
|
+ * 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.ingest.geoip;
|
|
|
+
|
|
|
+import com.maxmind.db.Network;
|
|
|
+import com.maxmind.geoip2.DatabaseReader;
|
|
|
+import com.maxmind.geoip2.model.AbstractResponse;
|
|
|
+import com.maxmind.geoip2.model.AnonymousIpResponse;
|
|
|
+import com.maxmind.geoip2.model.AsnResponse;
|
|
|
+import com.maxmind.geoip2.model.CityResponse;
|
|
|
+import com.maxmind.geoip2.model.ConnectionTypeResponse;
|
|
|
+import com.maxmind.geoip2.model.CountryResponse;
|
|
|
+import com.maxmind.geoip2.model.DomainResponse;
|
|
|
+import com.maxmind.geoip2.model.EnterpriseResponse;
|
|
|
+import com.maxmind.geoip2.model.IpRiskResponse;
|
|
|
+import com.maxmind.geoip2.model.IspResponse;
|
|
|
+import com.maxmind.geoip2.record.MaxMind;
|
|
|
+
|
|
|
+import org.elasticsearch.common.util.set.Sets;
|
|
|
+import org.elasticsearch.test.ESTestCase;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.lang.reflect.ParameterizedType;
|
|
|
+import java.lang.reflect.Type;
|
|
|
+import java.net.InetAddress;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.SortedSet;
|
|
|
+import java.util.TreeSet;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.empty;
|
|
|
+import static org.hamcrest.Matchers.equalTo;
|
|
|
+
|
|
|
+/**
|
|
|
+ * This test is meant to help us understand if we are falling behind on support for mmdb types and fields that get added over time. It
|
|
|
+ * maintains a list for each mmdb file type we support of which fields in that type we support and which we do not.
|
|
|
+ *
|
|
|
+ * This test will:
|
|
|
+ *
|
|
|
+ * - Fail if MaxMind adds a new field to one of the mmdb file types we currently support (city, country, etc) and we don't update this test
|
|
|
+ * with whether or not we support that field.
|
|
|
+ * - Fail if we add support for a new mmdb file type (enterprise for example) but don't update the test with which fields we do and do not
|
|
|
+ * support.
|
|
|
+ * - Fail if MaxMind adds a new mmdb file type that we don't know about
|
|
|
+ * - Fail if we expose a MaxMind type through GeoIpDatabase, but don't update the test to know how to handle it
|
|
|
+ */
|
|
|
+public class MaxMindSupportTests extends ESTestCase {
|
|
|
+
|
|
|
+ private static final Set<String> ASN_SUPPORTED_FIELDS = Set.of("autonomousSystemNumber", "autonomousSystemOrganization", "network");
|
|
|
+ private static final Set<String> ASN_UNSUPPORTED_FIELDS = Set.of("ipAddress");
|
|
|
+
|
|
|
+ private static final Set<String> CITY_SUPPORTED_FIELDS = Set.of(
|
|
|
+ "city.name",
|
|
|
+ "continent.name",
|
|
|
+ "country.isoCode",
|
|
|
+ "country.name",
|
|
|
+ "location.latitude",
|
|
|
+ "location.longitude",
|
|
|
+ "location.timeZone",
|
|
|
+ "mostSpecificSubdivision.isoCode",
|
|
|
+ "mostSpecificSubdivision.name"
|
|
|
+ );
|
|
|
+ private static final Set<String> CITY_UNSUPPORTED_FIELDS = Set.of(
|
|
|
+ "city.confidence",
|
|
|
+ "city.geoNameId",
|
|
|
+ "city.names",
|
|
|
+ "continent.code",
|
|
|
+ "continent.geoNameId",
|
|
|
+ "continent.names",
|
|
|
+ "country.confidence",
|
|
|
+ "country.geoNameId",
|
|
|
+ "country.inEuropeanUnion",
|
|
|
+ "country.names",
|
|
|
+ "leastSpecificSubdivision.confidence",
|
|
|
+ "leastSpecificSubdivision.geoNameId",
|
|
|
+ "leastSpecificSubdivision.isoCode",
|
|
|
+ "leastSpecificSubdivision.name",
|
|
|
+ "leastSpecificSubdivision.names",
|
|
|
+ "location.accuracyRadius",
|
|
|
+ "location.averageIncome",
|
|
|
+ "location.metroCode",
|
|
|
+ "location.populationDensity",
|
|
|
+ "maxMind",
|
|
|
+ "mostSpecificSubdivision.confidence",
|
|
|
+ "mostSpecificSubdivision.geoNameId",
|
|
|
+ "mostSpecificSubdivision.names",
|
|
|
+ "postal.code",
|
|
|
+ "postal.confidence",
|
|
|
+ "registeredCountry.confidence",
|
|
|
+ "registeredCountry.geoNameId",
|
|
|
+ "registeredCountry.inEuropeanUnion",
|
|
|
+ "registeredCountry.isoCode",
|
|
|
+ "registeredCountry.name",
|
|
|
+ "registeredCountry.names",
|
|
|
+ "representedCountry.confidence",
|
|
|
+ "representedCountry.geoNameId",
|
|
|
+ "representedCountry.inEuropeanUnion",
|
|
|
+ "representedCountry.isoCode",
|
|
|
+ "representedCountry.name",
|
|
|
+ "representedCountry.names",
|
|
|
+ "representedCountry.type",
|
|
|
+ "subdivisions.confidence",
|
|
|
+ "subdivisions.geoNameId",
|
|
|
+ "subdivisions.isoCode",
|
|
|
+ "subdivisions.name",
|
|
|
+ "subdivisions.names",
|
|
|
+ "traits.anonymous",
|
|
|
+ "traits.anonymousProxy",
|
|
|
+ "traits.anonymousVpn",
|
|
|
+ "traits.anycast",
|
|
|
+ "traits.autonomousSystemNumber",
|
|
|
+ "traits.autonomousSystemOrganization",
|
|
|
+ "traits.connectionType",
|
|
|
+ "traits.domain",
|
|
|
+ "traits.hostingProvider",
|
|
|
+ "traits.ipAddress",
|
|
|
+ "traits.isp",
|
|
|
+ "traits.legitimateProxy",
|
|
|
+ "traits.mobileCountryCode",
|
|
|
+ "traits.mobileNetworkCode",
|
|
|
+ "traits.network",
|
|
|
+ "traits.organization",
|
|
|
+ "traits.publicProxy",
|
|
|
+ "traits.residentialProxy",
|
|
|
+ "traits.satelliteProvider",
|
|
|
+ "traits.staticIpScore",
|
|
|
+ "traits.torExitNode",
|
|
|
+ "traits.userCount",
|
|
|
+ "traits.userType"
|
|
|
+ );
|
|
|
+
|
|
|
+ private static final Set<String> COUNTRY_SUPPORTED_FIELDS = Set.of("continent.name", "country.isoCode", "country.name");
|
|
|
+ private static final Set<String> COUNTRY_UNSUPPORTED_FIELDS = Set.of(
|
|
|
+ "continent.code",
|
|
|
+ "continent.geoNameId",
|
|
|
+ "continent.names",
|
|
|
+ "country.confidence",
|
|
|
+ "country.geoNameId",
|
|
|
+ "country.inEuropeanUnion",
|
|
|
+ "country.names",
|
|
|
+ "maxMind",
|
|
|
+ "registeredCountry.confidence",
|
|
|
+ "registeredCountry.geoNameId",
|
|
|
+ "registeredCountry.inEuropeanUnion",
|
|
|
+ "registeredCountry.isoCode",
|
|
|
+ "registeredCountry.name",
|
|
|
+ "registeredCountry.names",
|
|
|
+ "representedCountry.confidence",
|
|
|
+ "representedCountry.geoNameId",
|
|
|
+ "representedCountry.inEuropeanUnion",
|
|
|
+ "representedCountry.isoCode",
|
|
|
+ "representedCountry.name",
|
|
|
+ "representedCountry.names",
|
|
|
+ "representedCountry.type",
|
|
|
+ "traits.anonymous",
|
|
|
+ "traits.anonymousProxy",
|
|
|
+ "traits.anonymousVpn",
|
|
|
+ "traits.anycast",
|
|
|
+ "traits.autonomousSystemNumber",
|
|
|
+ "traits.autonomousSystemOrganization",
|
|
|
+ "traits.connectionType",
|
|
|
+ "traits.domain",
|
|
|
+ "traits.hostingProvider",
|
|
|
+ "traits.ipAddress",
|
|
|
+ "traits.isp",
|
|
|
+ "traits.legitimateProxy",
|
|
|
+ "traits.mobileCountryCode",
|
|
|
+ "traits.mobileNetworkCode",
|
|
|
+ "traits.network",
|
|
|
+ "traits.organization",
|
|
|
+ "traits.publicProxy",
|
|
|
+ "traits.residentialProxy",
|
|
|
+ "traits.satelliteProvider",
|
|
|
+ "traits.staticIpScore",
|
|
|
+ "traits.torExitNode",
|
|
|
+ "traits.userCount",
|
|
|
+ "traits.userType"
|
|
|
+ );
|
|
|
+
|
|
|
+ private static final Map<Database, Set<String>> TYPE_TO_SUPPORTED_FIELDS_MAP = Map.of(
|
|
|
+ Database.Asn,
|
|
|
+ ASN_SUPPORTED_FIELDS,
|
|
|
+ Database.City,
|
|
|
+ CITY_SUPPORTED_FIELDS,
|
|
|
+ Database.Country,
|
|
|
+ COUNTRY_SUPPORTED_FIELDS
|
|
|
+ );
|
|
|
+ private static final Map<Database, Set<String>> TYPE_TO_UNSUPPORTED_FIELDS_MAP = Map.of(
|
|
|
+ Database.Asn,
|
|
|
+ ASN_UNSUPPORTED_FIELDS,
|
|
|
+ Database.City,
|
|
|
+ CITY_UNSUPPORTED_FIELDS,
|
|
|
+ Database.Country,
|
|
|
+ COUNTRY_UNSUPPORTED_FIELDS
|
|
|
+ );
|
|
|
+ private static final Map<Database, Class<? extends AbstractResponse>> TYPE_TO_MAX_MIND_CLASS = Map.of(
|
|
|
+ Database.Asn,
|
|
|
+ AsnResponse.class,
|
|
|
+ Database.City,
|
|
|
+ CityResponse.class,
|
|
|
+ Database.Country,
|
|
|
+ CountryResponse.class
|
|
|
+ );
|
|
|
+
|
|
|
+ private static final Set<Class<? extends AbstractResponse>> KNOWN_UNSUPPORTED_RESPONSE_CLASSES = Set.of(
|
|
|
+ AnonymousIpResponse.class,
|
|
|
+ ConnectionTypeResponse.class,
|
|
|
+ DomainResponse.class,
|
|
|
+ EnterpriseResponse.class,
|
|
|
+ IspResponse.class,
|
|
|
+ IpRiskResponse.class
|
|
|
+ );
|
|
|
+
|
|
|
+ public void testMaxMindSupport() {
|
|
|
+ for (Database databaseType : Database.values()) {
|
|
|
+ Class<? extends AbstractResponse> maxMindClass = TYPE_TO_MAX_MIND_CLASS.get(databaseType);
|
|
|
+ Set<String> supportedFields = TYPE_TO_SUPPORTED_FIELDS_MAP.get(databaseType);
|
|
|
+ Set<String> unsupportedFields = TYPE_TO_UNSUPPORTED_FIELDS_MAP.get(databaseType);
|
|
|
+ assertNotNull(
|
|
|
+ "A new Database type, "
|
|
|
+ + databaseType
|
|
|
+ + ", has been added, but this test has not been updated to know which MaxMind "
|
|
|
+ + "class to use to load it. Update TYPE_TO_MAX_MIND_CLASS",
|
|
|
+ maxMindClass
|
|
|
+ );
|
|
|
+ assertNotNull(
|
|
|
+ "A new Database type, "
|
|
|
+ + databaseType
|
|
|
+ + ", has been added, but this test has not been updated to know which fields we "
|
|
|
+ + "support for it. Update TYPE_TO_SUPPORTED_FIELDS_MAP",
|
|
|
+ supportedFields
|
|
|
+ );
|
|
|
+ assertNotNull(
|
|
|
+ "A new Database type, "
|
|
|
+ + databaseType
|
|
|
+ + ", has been added, but this test has not been updated to know which fields we "
|
|
|
+ + "do not support for it. Update TYPE_TO_UNSUPPORTED_FIELDS_MAP",
|
|
|
+ unsupportedFields
|
|
|
+ );
|
|
|
+ /*
|
|
|
+ * fieldNamesFromMaxMind: The fields that MaxMind's Response object contains
|
|
|
+ * unusedFields: Fields that MaxMind's Response object contains that we do not claim to support
|
|
|
+ * unknownUnsupportedFieldNames: Fields that we document as unsupported that MaxMind's Response object doesn't even contain
|
|
|
+ * unknownSupportedFieldNames: Fields that we document as supported that MaxMind's Response object doesn't even contain
|
|
|
+ */
|
|
|
+ final SortedSet<String> fieldNamesFromMaxMind = getFieldNamesUsedFromClass("", maxMindClass);
|
|
|
+ Set<String> unusedFields = Sets.difference(fieldNamesFromMaxMind, supportedFields);
|
|
|
+ Set<String> unknownUnsupportedFieldNames = Sets.difference(unsupportedFields, unusedFields);
|
|
|
+ assertThat(
|
|
|
+ "We have documented fields in TYPE_TO_UNSUPPORTED_FIELDS_MAP that MaxMind does not actually support for "
|
|
|
+ + databaseType
|
|
|
+ + ": "
|
|
|
+ + unknownUnsupportedFieldNames,
|
|
|
+ unknownUnsupportedFieldNames,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ assertThat(
|
|
|
+ "New MaxMind fields have been added for "
|
|
|
+ + databaseType
|
|
|
+ + " that we do not use or have documented in TYPE_TO_UNSUPPORTED_FIELDS_MAP. The actual list of fields is:\n"
|
|
|
+ + getFormattedList(unusedFields),
|
|
|
+ unusedFields,
|
|
|
+ equalTo(new TreeSet<>(unsupportedFields))
|
|
|
+ );
|
|
|
+ Set<String> unknownSupportedFieldNames = Sets.difference(supportedFields, fieldNamesFromMaxMind);
|
|
|
+ assertThat(
|
|
|
+ "We are attempting to use fields that MaxMind does not support for " + databaseType + ": " + unknownSupportedFieldNames,
|
|
|
+ unknownSupportedFieldNames,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testUnknownMaxMindResponseClassess() {
|
|
|
+ Set<Class<? extends AbstractResponse>> supportedMaxMindClasses = new HashSet<>(TYPE_TO_MAX_MIND_CLASS.values());
|
|
|
+ // First just a sanity check that there's no overlap between what's supported and what's not:
|
|
|
+ Set<Class<? extends AbstractResponse>> supportedAndUnsupportedMaxMindClasses = Sets.intersection(
|
|
|
+ supportedMaxMindClasses,
|
|
|
+ KNOWN_UNSUPPORTED_RESPONSE_CLASSES
|
|
|
+ );
|
|
|
+ assertThat(
|
|
|
+ "We claim both to support and not support some MaxMind response classes: " + supportedAndUnsupportedMaxMindClasses,
|
|
|
+ supportedAndUnsupportedMaxMindClasses,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ Set<Class<? extends AbstractResponse>> allActualMaxMindClasses = new HashSet<>();
|
|
|
+
|
|
|
+ Method[] methods = DatabaseReader.class.getMethods();
|
|
|
+ for (Method method : methods) {
|
|
|
+ if (method.getName().startsWith("try")) {
|
|
|
+ if (method.getReturnType().equals(Optional.class)) {
|
|
|
+ Type genericReturnType = method.getGenericReturnType();
|
|
|
+ if (genericReturnType instanceof ParameterizedType parameterizedGenericReturnType) {
|
|
|
+ Type[] actualTypes = parameterizedGenericReturnType.getActualTypeArguments();
|
|
|
+ if (actualTypes != null && actualTypes.length == 1 && actualTypes[0] instanceof Class<?> actualTypeClass) {
|
|
|
+ allActualMaxMindClasses.add(actualTypeClass.asSubclass(AbstractResponse.class));
|
|
|
+ if (KNOWN_UNSUPPORTED_RESPONSE_CLASSES.contains(actualTypeClass) == false) {
|
|
|
+ assertTrue(
|
|
|
+ "MaxMind has added support for " + actualTypeClass.getSimpleName(),
|
|
|
+ supportedMaxMindClasses.contains(actualTypeClass)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Now make sure that we're not claiming to support any maxmind classes that aren't ever read by DatabaseReader:
|
|
|
+ Set<Class<? extends AbstractResponse>> supportedMaxMindClassesThatDoNotExist = Sets.difference(
|
|
|
+ supportedMaxMindClasses,
|
|
|
+ allActualMaxMindClasses
|
|
|
+ );
|
|
|
+ assertThat(
|
|
|
+ "We claim to support a MaxMind response class that MaxMind does not expose through DatabaseReader: "
|
|
|
+ + supportedMaxMindClassesThatDoNotExist,
|
|
|
+ supportedMaxMindClassesThatDoNotExist,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * This tests that this test has a mapping in TYPE_TO_MAX_MIND_CLASS for all MaxMind classes exposed through GeoIpDatabase.
|
|
|
+ */
|
|
|
+ public void testUsedMaxMindResponseClassesAreAccountedFor() {
|
|
|
+ Set<Class<? extends AbstractResponse>> usedMaxMindResponseClasses = getUsedMaxMindResponseClasses();
|
|
|
+ Set<Class<? extends AbstractResponse>> supportedMaxMindClasses = new HashSet<>(TYPE_TO_MAX_MIND_CLASS.values());
|
|
|
+ Set<Class<? extends AbstractResponse>> usedButNotSupportedMaxMindResponseClasses = Sets.difference(
|
|
|
+ usedMaxMindResponseClasses,
|
|
|
+ supportedMaxMindClasses
|
|
|
+ );
|
|
|
+ assertThat(
|
|
|
+ "GeoIpDatabase exposes MaxMind response classes that this test does not know what to do with. Add mappings to "
|
|
|
+ + "TYPE_TO_MAX_MIND_CLASS for the following: "
|
|
|
+ + usedButNotSupportedMaxMindResponseClasses,
|
|
|
+ usedButNotSupportedMaxMindResponseClasses,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ Set<Class<? extends AbstractResponse>> supportedButNotUsedMaxMindClasses = Sets.difference(
|
|
|
+ supportedMaxMindClasses,
|
|
|
+ usedMaxMindResponseClasses
|
|
|
+ );
|
|
|
+ assertThat(
|
|
|
+ "This test claims to support MaxMind response classes that are not exposed in GeoIpDatabase. Remove the following from "
|
|
|
+ + "TYPE_TO_MAX_MIND_CLASS: "
|
|
|
+ + supportedButNotUsedMaxMindClasses,
|
|
|
+ supportedButNotUsedMaxMindClasses,
|
|
|
+ empty()
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * This is the list of field types that causes us to stop recursing. That is, fields of these types are the lowest-level fields that
|
|
|
+ * we care about.
|
|
|
+ */
|
|
|
+ private static final Set<Class<?>> TERMINAL_TYPES = Set.of(
|
|
|
+ boolean.class,
|
|
|
+ Boolean.class,
|
|
|
+ char.class,
|
|
|
+ Character.class,
|
|
|
+ Class.class,
|
|
|
+ ConnectionTypeResponse.ConnectionType.class,
|
|
|
+ double.class,
|
|
|
+ Double.class,
|
|
|
+ InetAddress.class,
|
|
|
+ int.class,
|
|
|
+ Integer.class,
|
|
|
+ long.class,
|
|
|
+ Long.class,
|
|
|
+ MaxMind.class,
|
|
|
+ Network.class,
|
|
|
+ Object.class,
|
|
|
+ String.class,
|
|
|
+ void.class,
|
|
|
+ Void.class
|
|
|
+ );
|
|
|
+ /*
|
|
|
+ * These are types that are containers for other types. We don't need to recurse into each method on these types. Instead, we need to
|
|
|
+ * look at their generic types.
|
|
|
+ */
|
|
|
+ private static final Set<Class<?>> CONTAINER_TYPES = Set.of(Collection.class, List.class, Map.class, Optional.class);
|
|
|
+ /*
|
|
|
+ * These are methods we don't want to traverse into.
|
|
|
+ */
|
|
|
+ private static final Set<Method> IGNORED_METHODS = Arrays.stream(Object.class.getMethods()).collect(Collectors.toUnmodifiableSet());
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the set of bean-property-like field names referenced from aClass, sorted alphabetically. This method calls itself
|
|
|
+ * recursively for all methods until it reaches one of the types in TERMINAL_TYPES. The name of the method returning one of those
|
|
|
+ * terminal types is converted to a bean-property-like name using the "beanify" method.
|
|
|
+ *
|
|
|
+ * @param context This is a String representing where in the list of methods we are
|
|
|
+ * @param aClass The class whose methods we want to traverse to generate field names
|
|
|
+ * @return A sorted set of bean-property-like field names that can recursively be found from aClass
|
|
|
+ */
|
|
|
+ private static SortedSet<String> getFieldNamesUsedFromClass(String context, Class<?> aClass) {
|
|
|
+ SortedSet<String> fieldNames = new TreeSet<>();
|
|
|
+ Method[] methods = aClass.getMethods();
|
|
|
+ if (TERMINAL_TYPES.contains(aClass)) {
|
|
|
+ // We got here because a container type had a terminal type
|
|
|
+ fieldNames.add(context);
|
|
|
+ return fieldNames;
|
|
|
+ }
|
|
|
+ for (Method method : methods) {
|
|
|
+ if (IGNORED_METHODS.contains(method)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (method.getName().startsWith("to")) {
|
|
|
+ // ignoring methods like toJson or toString
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String currentContext = context + (context.isEmpty() ? "" : ".") + beanify(method.getName());
|
|
|
+ if (TERMINAL_TYPES.contains(method.getReturnType())) {
|
|
|
+ fieldNames.add(currentContext);
|
|
|
+ } else {
|
|
|
+ Class<?> returnType = method.getReturnType();
|
|
|
+ if (CONTAINER_TYPES.contains(returnType)) {
|
|
|
+ ParameterizedType genericReturnType = (ParameterizedType) method.getGenericReturnType();
|
|
|
+ for (Type actualType : genericReturnType.getActualTypeArguments()) {
|
|
|
+ if (actualType instanceof Class<?> actualTypeClass) {
|
|
|
+ fieldNames.addAll(getFieldNamesUsedFromClass(currentContext, actualTypeClass));
|
|
|
+ } else {
|
|
|
+ assert false : "This test needs to be updated to deal with this situation";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ fieldNames.addAll(getFieldNamesUsedFromClass(currentContext, method.getReturnType()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return fieldNames;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * This method converts a method name into what would be its equivalent bean property. For example "getName" returns "name".
|
|
|
+ */
|
|
|
+ private static String beanify(String methodName) {
|
|
|
+ if (methodName.startsWith("get")) {
|
|
|
+ return beanify("get", methodName);
|
|
|
+ } else if (methodName.startsWith("is")) {
|
|
|
+ return beanify("is", methodName);
|
|
|
+ } else {
|
|
|
+ return methodName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String beanify(String prefix, String methodName) {
|
|
|
+ return methodName.substring(prefix.length(), prefix.length() + 1).toLowerCase(Locale.ROOT) + methodName.substring(
|
|
|
+ prefix.length() + 1
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * This is a convenience to format the list of field names in fields into a String that can be copied into a Set initializer above,
|
|
|
+ * like countryUnsupportedFields.
|
|
|
+ */
|
|
|
+ private static String getFormattedList(Set<String> fields) {
|
|
|
+ StringBuilder result = new StringBuilder();
|
|
|
+ SortedSet<String> sortedFields = new TreeSet<>(fields);
|
|
|
+ for (Iterator<String> it = sortedFields.iterator(); it.hasNext();) {
|
|
|
+ result.append("\"");
|
|
|
+ result.append(it.next());
|
|
|
+ result.append("\"");
|
|
|
+ if (it.hasNext()) {
|
|
|
+ result.append(",\n");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * This returns all AbstractResponse classes that are returned from getter methods on GeoIpDatabase.
|
|
|
+ */
|
|
|
+ private static Set<Class<? extends AbstractResponse>> getUsedMaxMindResponseClasses() {
|
|
|
+ Set<Class<? extends AbstractResponse>> result = new HashSet<>();
|
|
|
+ Method[] methods = GeoIpDatabase.class.getMethods();
|
|
|
+ for (Method method : methods) {
|
|
|
+ if (method.getName().startsWith("get")) {
|
|
|
+ Class<?> returnType = method.getReturnType();
|
|
|
+ try {
|
|
|
+ result.add(returnType.asSubclass(AbstractResponse.class));
|
|
|
+ } catch (ClassCastException ignore) {
|
|
|
+ // This is not what we were looking for, move on
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|