diff --git a/ip-intelligence.translation/pom.xml b/ip-intelligence.translation/pom.xml new file mode 100644 index 0000000..5160629 --- /dev/null +++ b/ip-intelligence.translation/pom.xml @@ -0,0 +1,99 @@ + + + + pipeline.ip-intelligence + com.51degrees + 4.4.21-SNAPSHOT + + 4.0.0 + + ip-intelligence.translation + 51Degrees :: IP Intelligence :: Translation + Translation engines that turn the weighted ISO country codes + from 51Degrees IP Intelligence into localized, ordered country name and + code lists. + https://51degrees.com?utm_source=maven&utm_medium=package&utm_campaign=ip-intelligence-java&utm_content=ip-intelligence.translation-pom.xml&utm_term=url + + + + ${project.groupId} + ip-intelligence.shared + ${project.version} + + + ${project.groupId} + pipeline.translation + ${pipeline.version} + + + + ${project.groupId} + ip-intelligence.shared + ${project.version} + test-jar + test + + + + ${project.groupId} + ip-intelligence.engine.on-premise + ${project.version} + test + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-translation-data + generate-resources + + copy-resources + + + ${project.build.outputDirectory}/fiftyone/ipintelligence/translation + + + ${project.basedir}/../ip-intelligence.engine.on-premise/src/main/cxx/ip-intelligence-cxx/ip-intelligence-data/Translations + + countrycodes.en_GB.yml + + + + ${project.basedir}/../ip-intelligence.engine.on-premise/src/main/cxx/ip-intelligence-cxx/ip-intelligence-data/Translations/OSM + + countries.*.yml + + + + + + + + + + diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Constants.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Constants.java new file mode 100644 index 0000000..8ad29b3 --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Constants.java @@ -0,0 +1,51 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation; + +/** + * Constants used by the country translation flow elements. + */ +public class Constants { + + private Constants() { + } + + /** + * Element data key used by + * {@link fiftyone.ipintelligence.translation.flowelements.CountryCodeTranslationEngine}. + */ + public static final String COUNTRY_NAMES_KEY = "countrynames"; + + /** + * Element data key used by + * {@link fiftyone.ipintelligence.translation.flowelements.CountriesTranslationEngine}. + */ + public static final String COUNTRY_NAMES_TRANSLATED_KEY = + "countrynamestranslated"; + + /** + * Element data key of the IP Intelligence engine, the source of the + * weighted country codes. + */ + public static final String IP_INTELLIGENCE_KEY = "ip"; +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Resources.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Resources.java new file mode 100644 index 0000000..487ed14 --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/Resources.java @@ -0,0 +1,119 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Reads the translation YAML files that are wired in from the IP Intelligence + * data submodule and copied onto the classpath under this package by the build + * (see the module pom). + *

+ * The set of country name locale files is known and stable, so the files are + * loaded by name rather than by scanning the classpath, which keeps loading + * reliable whether running from a directory or a packaged jar. A file that is + * not present is skipped. + */ +public class Resources { + + /** + * The single country-code to English-name file. This is also the list of + * all known countries. + */ + private static final String COUNTRY_CODE_FILE = "countrycodes.en_GB.yml"; + + /** + * The country English-name to localized-name files, one per shipped + * locale. + */ + private static final String[] COUNTRY_FILES = new String[]{ + "countries.de_DE.yml", + "countries.es_ES.yml", + "countries.fr_FR.yml", + "countries.it_IT.yml", + "countries.nl_NL.yml", + "countries.pl_PL.yml", + "countries.pt_PT.yml", + "countries.sv_SE.yml", + "countries.tr_TR.yml", + "countries.uk_UA.yml" + }; + + private Resources() { + } + + /** + * Get the country name translation YAML files (English name to localized + * name). + * @return map of file contents keyed on file name + */ + public static Map getCountryResources() { + Map result = new LinkedHashMap<>(); + for (String name : COUNTRY_FILES) { + String content = read(name); + if (content != null) { + result.put(name, content); + } + } + return result; + } + + /** + * Get the country code translation YAML file (ISO code to English name). + * @return map of file contents keyed on file name + */ + public static Map getCountryCodeResources() { + Map result = new LinkedHashMap<>(); + String content = read(COUNTRY_CODE_FILE); + if (content != null) { + result.put(COUNTRY_CODE_FILE, content); + } + return result; + } + + /** + * Read a resource from this package as a UTF-8 string, or null if it is + * not present. + */ + private static String read(String name) { + try (InputStream stream = Resources.class.getResourceAsStream(name)) { + if (stream == null) { + return null; + } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int read; + while ((read = stream.read(chunk)) != -1) { + buffer.write(chunk, 0, read); + } + return new String(buffer.toByteArray(), StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountriesTranslationData.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountriesTranslationData.java new file mode 100644 index 0000000..c3e27ce --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountriesTranslationData.java @@ -0,0 +1,101 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.data; + +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.translation.data.TranslationData; +import org.slf4j.Logger; + +import java.util.List; + +/** + * Concrete implementation of {@link ICountriesTranslationData}. + */ +public class CountriesTranslationData + extends TranslationData + implements ICountriesTranslationData { + + /** + * Construct a new instance. + * @param logger used for logging + * @param flowData the {@link FlowData} the element data is added to + */ + public CountriesTranslationData(Logger logger, FlowData flowData) { + super(logger, flowData); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue>> + getCountryNamesGeographicalTranslated() { + return getAs("CountryNamesGeographicalTranslated", + AspectPropertyValue.class, List.class, IWeightedValue.class, + String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue>> + getCountryNamesPopulationTranslated() { + return getAs("CountryNamesPopulationTranslated", + AspectPropertyValue.class, List.class, IWeightedValue.class, + String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue> + getCountryNamesGeographicalAllTranslated() { + return getAs("CountryNamesGeographicalAllTranslated", + AspectPropertyValue.class, List.class, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue> + getCountryNamesPopulationAllTranslated() { + return getAs("CountryNamesPopulationAllTranslated", + AspectPropertyValue.class, List.class, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue> getCountryCodesGeographicalAll() { + return getAs("CountryCodesGeographicalAll", + AspectPropertyValue.class, List.class, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue> getCountryCodesPopulationAll() { + return getAs("CountryCodesPopulationAll", + AspectPropertyValue.class, List.class, String.class); + } + + @Override + public String getSortingCultureUsed() { + return getAs("SortingCultureUsed", String.class); + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountryCodeTranslationData.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountryCodeTranslationData.java new file mode 100644 index 0000000..c803f0f --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/CountryCodeTranslationData.java @@ -0,0 +1,64 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.data; + +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.translation.data.TranslationData; +import org.slf4j.Logger; + +import java.util.List; + +/** + * Concrete implementation of {@link ICountryCodeTranslationData}. + */ +public class CountryCodeTranslationData + extends TranslationData + implements ICountryCodeTranslationData { + + /** + * Construct a new instance. + * @param logger used for logging + * @param flowData the {@link FlowData} the element data is added to + */ + public CountryCodeTranslationData(Logger logger, FlowData flowData) { + super(logger, flowData); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue>> + getCountryNamesGeographical() { + return getAs("CountryNamesGeographical", AspectPropertyValue.class, + List.class, IWeightedValue.class, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public AspectPropertyValue>> + getCountryNamesPopulation() { + return getAs("CountryNamesPopulation", AspectPropertyValue.class, + List.class, IWeightedValue.class, String.class); + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountriesTranslationData.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountriesTranslationData.java new file mode 100644 index 0000000..2ded4bf --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountriesTranslationData.java @@ -0,0 +1,97 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.data; + +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.translation.data.ITranslationData; + +import java.util.List; + +/** + * Contains translated country names and the complete ordered lists of country + * names and codes, combining the weighted results from IP Intelligence with + * all known countries. + */ +public interface ICountriesTranslationData extends ITranslationData { + + /** + * Translated list of country names based on the geographical weighted list + * from IP Intelligence, translated to the browser language. + * @return weighted list of localized country names + */ + AspectPropertyValue>> + getCountryNamesGeographicalTranslated(); + + /** + * Translated list of country names based on the population weighted list + * from IP Intelligence, translated to the browser language. + * @return weighted list of localized country names + */ + AspectPropertyValue>> + getCountryNamesPopulationTranslated(); + + /** + * Translated list of all country names ordered by geographical weighting + * (descending), followed by the remaining countries sorted alphabetically + * by translated name. + * @return ordered list of localized country names + */ + AspectPropertyValue> + getCountryNamesGeographicalAllTranslated(); + + /** + * Translated list of all country names ordered by population weighting + * (descending), followed by the remaining countries sorted alphabetically + * by translated name. + * @return ordered list of localized country names + */ + AspectPropertyValue> + getCountryNamesPopulationAllTranslated(); + + /** + * List of all country codes ordered by geographical weighting + * (descending), followed by the remaining country codes in alphabetical + * order of their translated country names. Index-aligned with + * {@link #getCountryNamesGeographicalAllTranslated()}. + * @return ordered list of country codes + */ + AspectPropertyValue> getCountryCodesGeographicalAll(); + + /** + * List of all country codes ordered by population weighting (descending), + * followed by the remaining country codes in alphabetical order of their + * translated country names. Index-aligned with + * {@link #getCountryNamesPopulationAllTranslated()}. + * @return ordered list of country codes + */ + AspectPropertyValue> getCountryCodesPopulationAll(); + + /** + * The culture used by the engine to sort the data. Exposed for test + * purposes; not intended for customer use. May be an empty string when the + * invariant (case-insensitive) ordering was used. + * @return the sorting culture, or an empty string + */ + String getSortingCultureUsed(); +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountryCodeTranslationData.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountryCodeTranslationData.java new file mode 100644 index 0000000..b9a3c6a --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/data/ICountryCodeTranslationData.java @@ -0,0 +1,50 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.data; + +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.translation.data.ITranslationData; + +import java.util.List; + +/** + * Contains English country names translated from the country codes for both + * the geographical and population weighted lists from IP Intelligence. + */ +public interface ICountryCodeTranslationData extends ITranslationData { + + /** + * List of country names based on the geographical weighted list of country + * codes from IP Intelligence. + * @return weighted list of English country names + */ + AspectPropertyValue>> getCountryNamesGeographical(); + + /** + * List of country names based on the population weighted list of country + * codes from IP Intelligence. + * @return weighted list of English country names + */ + AspectPropertyValue>> getCountryNamesPopulation(); +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngine.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngine.java new file mode 100644 index 0000000..d49b841 --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngine.java @@ -0,0 +1,381 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.flowelements; + +import fiftyone.ipintelligence.shared.IPIntelligenceData; +import fiftyone.ipintelligence.translation.Constants; +import fiftyone.ipintelligence.translation.data.ICountriesTranslationData; +import fiftyone.pipeline.core.data.ElementPropertyMetaData; +import fiftyone.pipeline.core.data.ElementPropertyMetaDataDefault; +import fiftyone.pipeline.core.data.Evidence; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.core.data.factories.ElementDataFactory; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.engines.data.AspectPropertyValueDefault; +import fiftyone.pipeline.translation.data.Languages; +import fiftyone.pipeline.translation.data.MissingTranslationBehavior; +import fiftyone.pipeline.translation.data.TranslationProperty; +import fiftyone.pipeline.translation.data.Translator; +import fiftyone.pipeline.translation.flowelements.TranslationEngineBase; +import org.slf4j.Logger; + +import java.text.Collator; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Engine which takes the English country name properties from + * {@link CountryCodeTranslationEngine} and the weighted country code + * properties from the IP Intelligence engine, translates the country names to + * the browser language, and produces complete ordered lists of all countries + * combining the weighted results with all known countries. + *

+ * The base class handles the weighted translations + * (CountryNamesGeographical/Population to their Translated variants). This + * subclass adds the ordered "All" lists on top. Results are stored using the + * {@link Constants#COUNTRY_NAMES_TRANSLATED_KEY} key. + */ +public class CountriesTranslationEngine + extends TranslationEngineBase { + + private static final List EVIDENCE_KEYS = Arrays.asList( + "query.translation", + "query.accept-language", + "header.accept-language"); + + private static List translations() { + List list = new ArrayList<>(); + list.add(new TranslationProperty( + "CountryNamesGeographical", "CountryNamesGeographicalTranslated")); + list.add(new TranslationProperty( + "CountryNamesPopulation", "CountryNamesPopulationTranslated")); + return list; + } + + /** + * All known country codes and their English names, ordered as they appear + * in countrycodes.en_GB.yml. + */ + private final List> allCountries; + + /** + * The set of valid country codes (keys of {@link #allCountries}), used to + * drop the IP engine's no-match sentinel from the "All" lists. + */ + private final Set validCodes; + + private List allProperties; + + /** + * Construct a new instance. + * @param logger logger instance + * @param sources the country name translation sources, keyed on file name + * @param allCountries all known country codes and their English names, + * ordered as in countrycodes.en_GB.yml + * @param elementDataFactory factory used to create the element data + */ + public CountriesTranslationEngine( + Logger logger, + Map sources, + List> allCountries, + ElementDataFactory elementDataFactory) { + super( + Constants.COUNTRY_NAMES_KEY, + translations(), + sources, + null, + MissingTranslationBehavior.ORIGINAL, + ICountriesTranslationData.class, + logger, + elementDataFactory); + if (allCountries == null) { + throw new IllegalArgumentException("allCountries"); + } + this.allCountries = Collections.unmodifiableList( + new ArrayList<>(allCountries)); + this.validCodes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry country : this.allCountries) { + this.validCodes.add(country.getKey()); + } + } + + @Override + public String getElementDataKey() { + return Constants.COUNTRY_NAMES_TRANSLATED_KEY; + } + + @Override + public List getProperties() { + if (allProperties == null) { + List list = + new ArrayList<>(super.getProperties()); + String[] names = new String[]{ + "CountryNamesGeographicalAllTranslated", + "CountryNamesPopulationAllTranslated", + "CountryCodesGeographicalAll", + "CountryCodesPopulationAll"}; + for (String name : names) { + list.add(new ElementPropertyMetaDataDefault( + name, this, "", List.class, true)); + } + allProperties = list; + } + return allProperties; + } + + @Override + protected void processInternal(FlowData data) { + // The base class produces the weighted translated names and creates + // the element data, using the empty (pass-through) translator when the + // resolved language is English or unknown. + super.processInternal(data); + + ICountriesTranslationData elementData = + data.getOrAdd(getTypedDataKey(), getDataFactory()); + + String[] cultureUsed = new String[]{""}; + Translator translator; + Comparator comparator; + Languages.Match match = resolveMatch(data); + if (match != null) { + translator = match.getTranslator(); + comparator = createComparator(match.getLocale(), cultureUsed); + } else { + translator = null; + comparator = String.CASE_INSENSITIVE_ORDER; + } + elementData.put("SortingCultureUsed", cultureUsed[0]); + + List> geoCodes = readCodes(data, true); + List> popCodes = readCodes(data, false); + List> geoNames = readWeightedNames( + elementData.getCountryNamesGeographicalTranslated()); + List> popNames = readWeightedNames( + elementData.getCountryNamesPopulationTranslated()); + + buildAndStore(elementData, geoNames, geoCodes, translator, comparator, + "CountryNamesGeographicalAllTranslated", + "CountryCodesGeographicalAll"); + buildAndStore(elementData, popNames, popCodes, translator, comparator, + "CountryNamesPopulationAllTranslated", + "CountryCodesPopulationAll"); + } + + /** + * Build the complete "All" lists for one dimension and store them in the + * element data. + */ + private void buildAndStore( + ICountriesTranslationData elementData, + List> translatedWeightedNames, + List> weightedCodes, + Translator translator, + Comparator comparator, + String namesProperty, + String codesProperty) { + // The weighted countries, most probable first. Drop any code that is + // not a real country (the IP engine's no-match sentinel) so it never + // leads the list nor is re-added by the remaining step below. + List> weighted = new ArrayList<>(); + Set usedCodes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry pair : + buildWeightedTuples( + translatedWeightedNames, weightedCodes, comparator)) { + if (validCodes.contains(pair.getKey())) { + weighted.add(pair); + usedCodes.add(pair.getKey()); + } + } + + // Every remaining known country, alphabetical by translated name. + List errors = new ArrayList<>(); + List> remaining = new ArrayList<>(); + for (Map.Entry known : allCountries) { + if (usedCodes.contains(known.getKey()) == false) { + String name = known.getValue(); + if (translator != null) { + Object translated = translator.translate(name, errors); + if (translated instanceof String) { + name = (String) translated; + } + } + remaining.add(new AbstractMap.SimpleImmutableEntry<>( + known.getKey(), name)); + } + } + remaining.sort(new Comparator>() { + @Override + public int compare( + Map.Entry a, Map.Entry b) { + return comparator.compare(a.getValue(), b.getValue()); + } + }); + + List outCodes = new ArrayList<>(); + List outNames = new ArrayList<>(); + for (Map.Entry pair : weighted) { + outCodes.add(pair.getKey()); + outNames.add(pair.getValue()); + } + for (Map.Entry pair : remaining) { + outCodes.add(pair.getKey()); + outNames.add(pair.getValue()); + } + + elementData.put(codesProperty, + new AspectPropertyValueDefault>(outCodes)); + elementData.put(namesProperty, + new AspectPropertyValueDefault>(outNames)); + } + + /** + * Zip the translated weighted names with the weighted codes by index, then + * order by weighting (descending) and translated name (ascending). + */ + private static List> buildWeightedTuples( + List> translatedNames, + List> codes, + final Comparator comparator) { + List tuples = new ArrayList<>(); + if (translatedNames != null && codes != null) { + int count = Math.min(translatedNames.size(), codes.size()); + for (int i = 0; i < count; i++) { + IWeightedValue name = translatedNames.get(i); + IWeightedValue code = codes.get(i); + tuples.add(new WeightedTuple( + code.getValue(), name.getValue(), name.getRawWeighting())); + } + } + tuples.sort(new Comparator() { + @Override + public int compare(WeightedTuple a, WeightedTuple b) { + int byWeight = Integer.compare(b.weight, a.weight); + if (byWeight != 0) { + return byWeight; + } + return comparator.compare(a.name, b.name); + } + }); + List> result = new ArrayList<>(); + for (WeightedTuple tuple : tuples) { + result.add(new AbstractMap.SimpleImmutableEntry<>( + tuple.code, tuple.name)); + } + return result; + } + + /** + * Resolve the target language from the evidence using the base class's + * available languages. English is treated as the base language (no + * translation needed), so it yields no match. + */ + private Languages.Match resolveMatch(FlowData data) { + Evidence evidence = data.getEvidence(); + if (evidence == null) { + return null; + } + for (String key : EVIDENCE_KEYS) { + Object value = evidence.get(key); + if (value instanceof String && + ((String) value).trim().isEmpty() == false) { + Languages.Match match = getLanguages().match((String) value); + if (match != null) { + return match; + } + } + } + return null; + } + + private static Comparator createComparator( + String locale, String[] cultureUsed) { + cultureUsed[0] = ""; + if (locale == null) { + return String.CASE_INSENSITIVE_ORDER; + } + String tag = locale.replace('_', '-'); + final Collator collator = + Collator.getInstance(Locale.forLanguageTag(tag)); + cultureUsed[0] = tag; + return new Comparator() { + @Override + public int compare(String a, String b) { + return collator.compare(a, b); + } + }; + } + + private static List> readCodes( + FlowData data, boolean geographical) { + try { + IPIntelligenceData ipData = data.get(IPIntelligenceData.class); + if (ipData != null) { + AspectPropertyValue>> codes = + geographical + ? ipData.getCountryCodesGeographical() + : ipData.getCountryCodesPopulation(); + if (codes != null && codes.hasValue()) { + return codes.getValue(); + } + } + } catch (RuntimeException e) { + // The IP Intelligence data, or the specific property, may be + // absent. Treat that as no weighted codes. + } + return Collections.emptyList(); + } + + private static List> readWeightedNames( + AspectPropertyValue>> value) { + if (value != null && value.hasValue()) { + return value.getValue(); + } + return Collections.emptyList(); + } + + /** + * A code, its translated name and the weighting used to order the weighted + * countries. + */ + private static final class WeightedTuple { + + private final String code; + private final String name; + private final int weight; + + WeightedTuple(String code, String name, int weight) { + this.code = code; + this.name = name; + this.weight = weight; + } + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngineBuilder.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngineBuilder.java new file mode 100644 index 0000000..2c43db1 --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountriesTranslationEngineBuilder.java @@ -0,0 +1,91 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.flowelements; + +import fiftyone.ipintelligence.translation.Resources; +import fiftyone.ipintelligence.translation.data.CountriesTranslationData; +import fiftyone.pipeline.annotations.ElementBuilder; +import fiftyone.pipeline.translation.util.YamlTranslations; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Builder for {@link CountriesTranslationEngine}. Loads the country code + * resource (countrycodes.en_GB.yml), which lists all known country codes and + * their English names, and passes it to the engine as the set of all known + * countries. The country name translation resources (countries.*.yml) are + * handled by the base translation engine. + */ +@ElementBuilder +public class CountriesTranslationEngineBuilder { + + private final ILoggerFactory loggerFactory; + + /** + * Construct a new builder using the default logger factory. + */ + public CountriesTranslationEngineBuilder() { + this(LoggerFactory.getILoggerFactory()); + } + + /** + * Construct a new builder. + * @param loggerFactory the logger factory used by the engine and the + * element data it creates + */ + public CountriesTranslationEngineBuilder(ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; + } + + /** + * Build a new {@link CountriesTranslationEngine}. + * @return a new engine instance + */ + public CountriesTranslationEngine build() { + Map codeResources = + Resources.getCountryCodeResources(); + List> allCountries = new ArrayList<>(); + if (codeResources.isEmpty() == false) { + String content = codeResources.values().iterator().next(); + for (Map.Entry entry : + YamlTranslations.parse(content).entrySet()) { + allCountries.add(new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), entry.getValue())); + } + } + + return new CountriesTranslationEngine( + loggerFactory.getLogger(CountriesTranslationEngine.class.getName()), + Resources.getCountryResources(), + allCountries, + (flowData, flowElement) -> new CountriesTranslationData( + loggerFactory.getLogger( + CountriesTranslationData.class.getName()), + flowData)); + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngine.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngine.java new file mode 100644 index 0000000..364c03b --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngine.java @@ -0,0 +1,98 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.flowelements; + +import fiftyone.ipintelligence.translation.Constants; +import fiftyone.ipintelligence.translation.Resources; +import fiftyone.ipintelligence.translation.data.ICountryCodeTranslationData; +import fiftyone.pipeline.core.data.factories.ElementDataFactory; +import fiftyone.pipeline.translation.data.MissingTranslationBehavior; +import fiftyone.pipeline.translation.data.TranslationProperty; +import fiftyone.pipeline.translation.flowelements.TranslationEngineBase; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * Engine which takes the weighted country code properties from IP Intelligence + * and translates them to country names. The country names are always in + * English. Results are stored using the {@link Constants#COUNTRY_NAMES_KEY} + * key. + *

+ * All translation files are loaded as resources, so no configuration is + * required. + */ +public class CountryCodeTranslationEngine + extends TranslationEngineBase { + + private static List translations() { + List list = new ArrayList<>(); + list.add(new TranslationProperty( + "CountryCodesGeographical", "CountryNamesGeographical")); + list.add(new TranslationProperty( + "CountryCodesPopulation", "CountryNamesPopulation")); + return list; + } + + /** + * Construct a new instance reading from the default IP Intelligence + * element data key ({@link Constants#IP_INTELLIGENCE_KEY}). + * @param logger logger instance + * @param elementDataFactory factory used to create the element data + */ + public CountryCodeTranslationEngine( + Logger logger, + ElementDataFactory elementDataFactory) { + this(Constants.IP_INTELLIGENCE_KEY, logger, elementDataFactory); + } + + /** + * Construct a new instance reading from the supplied IP Intelligence + * element data key. The key is configurable because it has differed + * between IP Intelligence engine versions. + * @param sourceElementDataKey element data key of the IP Intelligence + * engine + * @param logger logger instance + * @param elementDataFactory factory used to create the element data + */ + public CountryCodeTranslationEngine( + String sourceElementDataKey, + Logger logger, + ElementDataFactory elementDataFactory) { + super( + sourceElementDataKey, + translations(), + Resources.getCountryCodeResources(), + "en_GB", + MissingTranslationBehavior.ORIGINAL, + ICountryCodeTranslationData.class, + logger, + elementDataFactory); + } + + @Override + public String getElementDataKey() { + return Constants.COUNTRY_NAMES_KEY; + } +} diff --git a/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngineBuilder.java b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngineBuilder.java new file mode 100644 index 0000000..0a23fe9 --- /dev/null +++ b/ip-intelligence.translation/src/main/java/fiftyone/ipintelligence/translation/flowelements/CountryCodeTranslationEngineBuilder.java @@ -0,0 +1,84 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.flowelements; + +import fiftyone.ipintelligence.translation.Constants; +import fiftyone.ipintelligence.translation.data.CountryCodeTranslationData; +import fiftyone.pipeline.annotations.ElementBuilder; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +/** + * Builder for {@link CountryCodeTranslationEngine}. This requires no + * configuration as all the translation files are loaded as resources. + */ +@ElementBuilder +public class CountryCodeTranslationEngineBuilder { + + private final ILoggerFactory loggerFactory; + private String sourceElementDataKey = Constants.IP_INTELLIGENCE_KEY; + + /** + * Construct a new builder using the default logger factory. + */ + public CountryCodeTranslationEngineBuilder() { + this(LoggerFactory.getILoggerFactory()); + } + + /** + * Construct a new builder. + * @param loggerFactory the logger factory used by the engine and the + * element data it creates + */ + public CountryCodeTranslationEngineBuilder(ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; + } + + /** + * Set the element data key of the IP Intelligence engine to read the + * weighted country codes from. Defaults to + * {@link Constants#IP_INTELLIGENCE_KEY}. + * @param sourceElementDataKey the IP Intelligence element data key + * @return this builder + */ + public CountryCodeTranslationEngineBuilder setSourceElementDataKey( + String sourceElementDataKey) { + this.sourceElementDataKey = sourceElementDataKey; + return this; + } + + /** + * Build a new {@link CountryCodeTranslationEngine}. + * @return a new engine instance + */ + public CountryCodeTranslationEngine build() { + return new CountryCodeTranslationEngine( + sourceElementDataKey, + loggerFactory.getLogger( + CountryCodeTranslationEngine.class.getName()), + (flowData, flowElement) -> new CountryCodeTranslationData( + loggerFactory.getLogger( + CountryCodeTranslationData.class.getName()), + flowData)); + } +} diff --git a/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/CountryTranslationIntegrationTest.java b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/CountryTranslationIntegrationTest.java new file mode 100644 index 0000000..a5eb58d --- /dev/null +++ b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/CountryTranslationIntegrationTest.java @@ -0,0 +1,156 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation; + +import fiftyone.ipintelligence.engine.onpremise.flowelements.IPIntelligenceOnPremiseEngine; +import fiftyone.ipintelligence.engine.onpremise.flowelements.IPIntelligenceOnPremiseEngineBuilder; +import fiftyone.ipintelligence.translation.data.ICountriesTranslationData; +import fiftyone.ipintelligence.translation.flowelements.CountriesTranslationEngineBuilder; +import fiftyone.ipintelligence.translation.flowelements.CountryCodeTranslationEngineBuilder; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.core.flowelements.Pipeline; +import fiftyone.pipeline.core.flowelements.PipelineBuilder; +import fiftyone.pipeline.engines.Constants; +import org.junit.Test; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.PrintStream; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +/** + * End-to-end test that drives the real on-premise IP Intelligence engine with + * a data file, then the two country translation engines, and checks that a + * complete, localized, ordered country dropdown is produced. + *

+ * The data file is resolved from the {@code TestDataFile} system property, the + * {@code 51DEGREES_IPI_PATH} environment variable, or a few well known + * locations, and the test is skipped if none is found. + *

+ * The IP Intelligence element data key has differed between engine versions + * (it is {@code "ip"} in current builds), so the test reads the actual key + * from the engine and configures the code translation engine with it. The + * country names engine reads the codes by {@code IPIntelligenceData} type, so + * it is unaffected. + */ +public class CountryTranslationIntegrationTest { + + private static final String SAMPLE_IP = "8.8.8.8"; + + private static String resolveDataFile() { + String[] candidates = { + System.getProperty("TestDataFile"), + System.getenv("51DEGREES_IPI_PATH"), + "51Degrees-IPIV4EnterpriseIpiV41.ipi", + "../51Degrees-IPIV4EnterpriseIpiV41.ipi", + "../../51Degrees-IPIV4EnterpriseIpiV41.ipi", + "D:/WorkSpace/51Degrees-IPIV4EnterpriseIpiV41.ipi" + }; + for (String candidate : candidates) { + if (candidate != null && new File(candidate).exists()) { + return candidate; + } + } + return null; + } + + @Test + public void translatesRealIpThroughOnPremiseEngine() throws Exception { + String dataFile = resolveDataFile(); + assumeTrue( + "Skipping integration test: no IP Intelligence data file found", + dataFile != null); + + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + PrintStream out = System.out; + + IPIntelligenceOnPremiseEngine ipEngine = + new IPIntelligenceOnPremiseEngineBuilder(loggerFactory, null) + .setPerformanceProfile(Constants.PerformanceProfiles.LowMemory) + .setAutoUpdate(false) + .build(dataFile, false); + String ipKey = ipEngine.getElementDataKey(); + + try (Pipeline pipeline = new PipelineBuilder(loggerFactory) + .addFlowElement(ipEngine) + .addFlowElement( + new CountryCodeTranslationEngineBuilder(loggerFactory) + .setSourceElementDataKey(ipKey) + .build()) + .addFlowElement( + new CountriesTranslationEngineBuilder(loggerFactory) + .build()) + .build(); + FlowData data = pipeline.createFlowData()) { + + data.addEvidence("query.client-ip", SAMPLE_IP); + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List codes = + ct.getCountryCodesGeographicalAll().getValue(); + List names = + ct.getCountryNamesGeographicalAllTranslated().getValue(); + List> weighted = + ct.getCountryNamesGeographicalTranslated().getValue(); + + // Print the real result so the run is self-documenting. + out.println("Integration test data file: " + dataFile); + out.println("IP engine element data key: " + ipKey); + out.println("Client IP: " + SAMPLE_IP + ", language: fr_FR"); + out.println("Weighted geographical countries (most probable first):"); + for (IWeightedValue item : weighted) { + out.println(" " + item.getValue() + + " (weight " + item.getRawWeighting() + ")"); + } + out.println("Dropdown (first 5 of " + names.size() + "):"); + for (int i = 0; i < Math.min(5, names.size()); i++) { + out.println(" option value=" + codes.get(i) + + " text=" + names.get(i)); + } + + // A complete, index-aligned, localized dropdown. + assertEquals(250, codes.size()); + assertEquals(codes.size(), names.size()); + assertEquals("fr-FR", ct.getSortingCultureUsed()); + // Localization works through the real engine: a tail country's + // code is aligned with its French name. + assertEquals("Allemagne", names.get(codes.indexOf("DE"))); + + // The sample IP resolves to weighted countries, which lead the + // dropdown ordered by weight descending. + assertTrue("Expected the sample IP to resolve to a country", + weighted.isEmpty() == false); + assertEquals(weighted.get(0).getValue(), names.get(0)); + assertTrue(weighted.get(0).getRawWeighting() > 0); + } + } +} diff --git a/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/TranslationTests.java b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/TranslationTests.java new file mode 100644 index 0000000..bdb26cc --- /dev/null +++ b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/TranslationTests.java @@ -0,0 +1,478 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation; + +import fiftyone.ipintelligence.translation.data.ICountriesTranslationData; +import fiftyone.ipintelligence.translation.data.ICountryCodeTranslationData; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.core.flowelements.Pipeline; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import org.junit.Test; + +import java.text.Collator; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.apv; +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.build; +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.buildCustom; +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.noValue; +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.weighted; +import static fiftyone.ipintelligence.translation.testhelpers.Fixtures.weightify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class TranslationTests { + + private static List values( + AspectPropertyValue>> value) { + List result = new ArrayList<>(); + for (IWeightedValue item : value.getValue()) { + result.add(item.getValue()); + } + return result; + } + + // =================================================================== + // 10a. Ported from the .NET suite. + // =================================================================== + + @Test + public void countryNamesFromCodes() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), apv(weightify("GB", "FR"))); + FlowData data = pipeline.createFlowData()) { + data.process(); + ICountryCodeTranslationData names = + data.get(ICountryCodeTranslationData.class); + assertEquals(Arrays.asList("United Kingdom", "France"), + values(names.getCountryNamesGeographical())); + assertEquals(Arrays.asList("United Kingdom", "France"), + values(names.getCountryNamesPopulation())); + } + } + + @Test + public void translatedCountry() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), apv(weightify("GB", "FR"))); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals(Arrays.asList("Royaume-Uni", "France"), + values(ct.getCountryNamesGeographicalTranslated())); + } + } + + @Test + public void allListsProducedCorrectlySorted() throws Exception { + // FR has the lower weight, GB the higher: GB should lead. + List> codes = new ArrayList<>(); + codes.add(weighted(30000, "FR")); + codes.add(weighted(35535, "GB")); + try (Pipeline pipeline = build(apv(codes), apv(codes)); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + List allNames = + ct.getCountryNamesGeographicalAllTranslated().getValue(); + + assertEquals("GB", allCodes.get(0)); + assertEquals("FR", allCodes.get(1)); + assertEquals("Royaume-Uni", allNames.get(0)); + assertEquals("France", allNames.get(1)); + assertEquals(250, allCodes.size()); + assertEquals(250, allNames.size()); + // GB and FR are not repeated in the alphabetical tail. + assertEquals(1, frequency(allCodes, "GB")); + assertEquals(1, frequency(allCodes, "FR")); + // Tail is sorted by the locale comparer. + assertTailSorted(allNames, 2, "fr-FR"); + } + } + + @Test + public void allListsProducedCorrectly() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), apv(weightify("GB", "FR"))); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + assertEquals("GB", allCodes.get(0)); + assertEquals("FR", allCodes.get(1)); + assertEquals(250, allCodes.size()); + } + } + + @Test + public void allListsWithoutLanguage() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), apv(weightify("GB", "FR"))); + FlowData data = pipeline.createFlowData()) { + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + List allNames = + ct.getCountryNamesGeographicalAllTranslated().getValue(); + assertEquals("GB", allCodes.get(0)); + assertEquals("United Kingdom", allNames.get(0)); + // Tail is the English names, alphabetically. + assertEquals("Afghanistan", allNames.get(2)); + assertEquals("", ct.getSortingCultureUsed()); + } + } + + @Test + public void allListsWithNoIpData() throws Exception { + try (Pipeline pipeline = build(noValue(), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "de_DE"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + List allNames = + ct.getCountryNamesGeographicalAllTranslated().getValue(); + // Every country is present and fully alphabetical (no weighted + // leaders). + assertEquals(250, allCodes.size()); + assertEquals(250, allNames.size()); + assertEquals("de-DE", ct.getSortingCultureUsed()); + assertTailSorted(allNames, 0, "de-DE"); + // The German name is present and aligned with its code. + assertEquals("Deutschland", allNames.get(allCodes.indexOf("DE"))); + } + } + + @Test + public void populationAndGeographicalAreIndependent() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("DE")), apv(weightify("US", "CN"))); + FlowData data = pipeline.createFlowData()) { + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals("DE", + ct.getCountryCodesGeographicalAll().getValue().get(0)); + assertEquals("US", + ct.getCountryCodesPopulationAll().getValue().get(0)); + assertEquals("CN", + ct.getCountryCodesPopulationAll().getValue().get(1)); + } + } + + @Test + public void germanTranslation() throws Exception { + try (Pipeline pipeline = build(apv(weightify("DE")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "de_DE"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals("Deutschland", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + assertEquals("Deutschland", + ct.getCountryNamesGeographicalAllTranslated().getValue().get(0)); + assertEquals("DE", + ct.getCountryCodesGeographicalAll().getValue().get(0)); + } + } + + @Test + public void acceptLanguageWithDash() throws Exception { + try (Pipeline pipeline = build(apv(weightify("GB")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr-FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals("Royaume-Uni", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + } + } + + @Test + public void englishLanguageNoTranslation() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "en-US,en;q=0.9"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals(Arrays.asList("United Kingdom", "France"), + values(ct.getCountryNamesGeographicalTranslated())); + assertEquals("", ct.getSortingCultureUsed()); + } + } + + @Test + public void englishPreferredOverOtherLanguages() throws Exception { + try (Pipeline pipeline = build(apv(weightify("DE")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence( + "header.accept-language", "en-US,en;q=0.9,de-DE;q=0.5"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + // English wins, so the name stays English (not "Deutschland"). + assertEquals("Germany", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + } + } + + @Test + public void preferredLanguageMatchedBeforeLowerPriority() throws Exception { + try (Pipeline pipeline = build(apv(weightify("DE")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence( + "header.accept-language", "es,de-DE;q=0.8,fr;q=0.5"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + // Spanish (es -> es_ES) wins. + assertEquals("Alemania", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + } + } + + // =================================================================== + // 10b. Gaps not covered by the .NET suite. + // =================================================================== + + @Test + public void queryTranslationEvidenceTranslates() throws Exception { + try (Pipeline pipeline = build(apv(weightify("GB")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("query.translation", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals("Royaume-Uni", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + } + } + + @Test + public void queryTranslationOverridesHeader() throws Exception { + try (Pipeline pipeline = build(apv(weightify("GB")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("query.translation", "fr_FR"); + data.addEvidence("header.accept-language", "de_DE"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + // query.translation has higher precedence, so French wins. + assertEquals("Royaume-Uni", + values(ct.getCountryNamesGeographicalTranslated()).get(0)); + assertEquals("fr-FR", ct.getSortingCultureUsed()); + } + } + + @Test + public void weightsPreservedThroughTranslation() throws Exception { + List> codes = new ArrayList<>(); + codes.add(weighted(30000, "FR")); + codes.add(weighted(35535, "GB")); + try (Pipeline pipeline = build(apv(codes), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List> translated = + ct.getCountryNamesGeographicalTranslated().getValue(); + // Order is preserved (input FR, GB); only the names changed. + assertEquals("France", translated.get(0).getValue()); + assertEquals(30000, translated.get(0).getRawWeighting()); + assertEquals("Royaume-Uni", translated.get(1).getValue()); + assertEquals(35535, translated.get(1).getRawWeighting()); + } + } + + @Test + public void equalWeightTieBreakByName() throws Exception { + List> codes = new ArrayList<>(); + codes.add(weighted(20000, "GB")); + codes.add(weighted(20000, "FR")); + try (Pipeline pipeline = build(apv(codes), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + // Equal weights, so the secondary sort by translated name applies: + // "France" < "Royaume-Uni", so FR leads despite GB being first in. + assertEquals("FR", allCodes.get(0)); + assertEquals("GB", allCodes.get(1)); + } + } + + @Test + public void unknownLocaleFallsBackToEnglish() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "zz-ZZ"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + assertEquals(Arrays.asList("United Kingdom", "France"), + values(ct.getCountryNamesGeographicalTranslated())); + // Every property is still populated. + assertEquals(250, + ct.getCountryCodesGeographicalAll().getValue().size()); + assertEquals(250, + ct.getCountryNamesGeographicalAllTranslated().getValue().size()); + assertEquals("", ct.getSortingCultureUsed()); + } + } + + @Test + public void missingSingleNameStaysEnglish() throws Exception { + // A custom locale map missing "France" but containing "Germany". + Map sources = new LinkedHashMap<>(); + sources.put("countries.xx_XX.yml", "Germany: Schland\n"); + List> allCountries = new ArrayList<>(); + allCountries.add(new AbstractMap.SimpleImmutableEntry<>("DE", "Germany")); + allCountries.add(new AbstractMap.SimpleImmutableEntry<>("FR", "France")); + + try (Pipeline pipeline = buildCustom( + apv(weightify("DE", "FR")), noValue(), sources, allCountries); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "xx_XX"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + // Germany is translated; France is missing so stays English. + assertEquals(Arrays.asList("Schland", "France"), + values(ct.getCountryNamesGeographicalTranslated())); + } + } + + @Test + public void fullIndexAlignmentIncludingTail() throws Exception { + try (Pipeline pipeline = + build(apv(weightify("GB", "FR")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + List allCodes = + ct.getCountryCodesGeographicalAll().getValue(); + List allNames = + ct.getCountryNamesGeographicalAllTranslated().getValue(); + assertEquals(allCodes.size(), allNames.size()); + // Spot-check alignment across the alphabetical tail. + assertEquals("Allemagne", allNames.get(allCodes.indexOf("DE"))); + assertEquals("Espagne", allNames.get(allCodes.indexOf("ES"))); + assertEquals("Italie", allNames.get(allCodes.indexOf("IT"))); + assertEquals("Chine", allNames.get(allCodes.indexOf("CN"))); + assertEquals("Japon", allNames.get(allCodes.indexOf("JP"))); + } + } + + @Test + public void sortingCultureUsedReflectsResolvedCulture() throws Exception { + try (Pipeline pipeline = build(apv(weightify("GB")), noValue()); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + assertEquals("fr-FR", + data.get(ICountriesTranslationData.class) + .getSortingCultureUsed()); + } + } + + @Test + public void populationTranslatedNamesDifferFromGeographical() + throws Exception { + try (Pipeline pipeline = + build(apv(weightify("DE")), apv(weightify("FR"))); + FlowData data = pipeline.createFlowData()) { + data.addEvidence("header.accept-language", "fr_FR"); + data.process(); + ICountriesTranslationData ct = + data.get(ICountriesTranslationData.class); + String geo = + values(ct.getCountryNamesGeographicalTranslated()).get(0); + String pop = + values(ct.getCountryNamesPopulationTranslated()).get(0); + assertEquals("Allemagne", geo); + assertEquals("France", pop); + assertNotEquals(geo, pop); + // The population All list carries the translated name, not a code. + assertEquals("France", + ct.getCountryNamesPopulationAllTranslated().getValue().get(0)); + } + } + + // =================================================================== + // Helpers + // =================================================================== + + private static int frequency(List list, String value) { + int count = 0; + for (String item : list) { + if (item.equals(value)) { + count++; + } + } + return count; + } + + private static void assertTailSorted( + List names, int from, String cultureTag) { + Collator collator = + Collator.getInstance(Locale.forLanguageTag(cultureTag)); + for (int i = from + 1; i < names.size(); i++) { + assertTrue( + "Tail not sorted at index " + i + ": '" + + names.get(i - 1) + "' then '" + names.get(i) + "'", + collator.compare(names.get(i - 1), names.get(i)) <= 0); + } + } +} diff --git a/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/Fixtures.java b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/Fixtures.java new file mode 100644 index 0000000..71cdb7a --- /dev/null +++ b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/Fixtures.java @@ -0,0 +1,187 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.testhelpers; + +import fiftyone.ipintelligence.shared.IPIntelligenceData; +import fiftyone.ipintelligence.translation.flowelements.CountriesTranslationEngine; +import fiftyone.ipintelligence.translation.flowelements.CountriesTranslationEngineBuilder; +import fiftyone.ipintelligence.translation.flowelements.CountryCodeTranslationEngine; +import fiftyone.ipintelligence.translation.flowelements.CountryCodeTranslationEngineBuilder; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.core.data.WeightedValue; +import fiftyone.pipeline.core.data.factories.ElementDataFactory; +import fiftyone.pipeline.core.flowelements.Pipeline; +import fiftyone.pipeline.core.flowelements.PipelineBuilder; +import fiftyone.pipeline.engines.data.AspectData; +import fiftyone.pipeline.engines.data.AspectPropertyMetaData; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import fiftyone.pipeline.engines.data.AspectPropertyValueDefault; +import fiftyone.pipeline.engines.flowelements.AspectEngine; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.mock; + +/** + * Helpers for building country translation pipelines in tests, with a stub IP + * Intelligence engine producing the weighted country codes. + */ +public final class Fixtures { + + private static final ILoggerFactory LOGGER_FACTORY = + LoggerFactory.getILoggerFactory(); + + private Fixtures() { + } + + /** + * Build a weighted list of codes with descending weights assigned by input + * order, so the first code is the most probable. + * @param codes the codes, most probable first + * @return the weighted list + */ + public static List> weightify(String... codes) { + List> list = new ArrayList<>(); + for (int i = 0; i < codes.length; i++) { + list.add(new WeightedValue( + (codes.length - i) * 1000, codes[i])); + } + return list; + } + + /** + * Build a single weighted code with an explicit weight. + * @param rawWeight the raw weight (0..65535) + * @param code the code + * @return the weighted value + */ + public static IWeightedValue weighted(int rawWeight, String code) { + return new WeightedValue(rawWeight, code); + } + + /** + * Wrap a weighted list in an {@link AspectPropertyValue} with a value. + * @param list the weighted list + * @return the wrapped value + */ + public static AspectPropertyValue>> apv( + List> list) { + return new AspectPropertyValueDefault>>( + list); + } + + /** + * A no-value {@link AspectPropertyValue}, used when the IP engine produced + * no weighted codes for a dimension. + * @return a no-value wrapper + */ + public static AspectPropertyValue>> noValue() { + return new AspectPropertyValueDefault>>(); + } + + /** + * Build a pipeline of: stub IP engine -> country code translation engine + * -> countries translation engine. + * @param geographical the geographical weighted codes + * @param population the population weighted codes + * @return the pipeline + * @throws Exception if the pipeline cannot be built + */ + @SuppressWarnings("unchecked") + public static Pipeline build( + AspectPropertyValue>> geographical, + AspectPropertyValue>> population) + throws Exception { + final Logger logger = LOGGER_FACTORY.getLogger("StubIpEngine"); + final AspectEngine + mockEngine = mock(AspectEngine.class); + ElementDataFactory ipFactory = + (flowData, flowElement) -> + new TestIpiData(logger, flowData, mockEngine, null); + + StubIpEngine stub = + new StubIpEngine(logger, ipFactory, geographical, population); + CountryCodeTranslationEngine codeEngine = + new CountryCodeTranslationEngineBuilder(LOGGER_FACTORY).build(); + CountriesTranslationEngine countriesEngine = + new CountriesTranslationEngineBuilder(LOGGER_FACTORY).build(); + + return new PipelineBuilder(LOGGER_FACTORY) + .addFlowElement(stub) + .addFlowElement(codeEngine) + .addFlowElement(countriesEngine) + .build(); + } + + /** + * Build a pipeline as {@link #build} but with a countries translation + * engine using custom country name sources and a custom set of all known + * countries. Used to exercise the missing-single-name behaviour with a + * deliberately incomplete locale map. + * @param geographical the geographical weighted codes + * @param population the population weighted codes + * @param countriesSources custom country name sources, keyed on file name + * @param allCountries custom ordered (code, English name) list + * @return the pipeline + * @throws Exception if the pipeline cannot be built + */ + @SuppressWarnings("unchecked") + public static Pipeline buildCustom( + AspectPropertyValue>> geographical, + AspectPropertyValue>> population, + Map countriesSources, + List> allCountries) + throws Exception { + final Logger logger = LOGGER_FACTORY.getLogger("StubIpEngine"); + final AspectEngine + mockEngine = mock(AspectEngine.class); + ElementDataFactory ipFactory = + (flowData, flowElement) -> + new TestIpiData(logger, flowData, mockEngine, null); + + StubIpEngine stub = + new StubIpEngine(logger, ipFactory, geographical, population); + CountryCodeTranslationEngine codeEngine = + new CountryCodeTranslationEngineBuilder(LOGGER_FACTORY).build(); + CountriesTranslationEngine countriesEngine = + new CountriesTranslationEngine( + LOGGER_FACTORY.getLogger("CountriesTranslationEngine"), + countriesSources, + allCountries, + (flowData, flowElement) -> new fiftyone.ipintelligence + .translation.data.CountriesTranslationData( + LOGGER_FACTORY.getLogger("CountriesTranslationData"), + flowData)); + + return new PipelineBuilder(LOGGER_FACTORY) + .addFlowElement(stub) + .addFlowElement(codeEngine) + .addFlowElement(countriesEngine) + .build(); + } +} diff --git a/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/StubIpEngine.java b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/StubIpEngine.java new file mode 100644 index 0000000..d9a58c0 --- /dev/null +++ b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/StubIpEngine.java @@ -0,0 +1,91 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.testhelpers; + +import fiftyone.ipintelligence.shared.IPIntelligenceData; +import fiftyone.pipeline.core.data.ElementPropertyMetaData; +import fiftyone.pipeline.core.data.EvidenceKeyFilter; +import fiftyone.pipeline.core.data.EvidenceKeyFilterWhitelist; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.data.IWeightedValue; +import fiftyone.pipeline.core.data.factories.ElementDataFactory; +import fiftyone.pipeline.core.flowelements.FlowElementBase; +import fiftyone.pipeline.engines.data.AspectPropertyValue; +import org.slf4j.Logger; + +import java.util.Collections; +import java.util.List; + +/** + * Stub flow element with the IP Intelligence engine's element data key + * ({@code "ip"}) which exposes the weighted country code lists, standing in for + * the on-premise engine in tests. + */ +public class StubIpEngine + extends FlowElementBase { + + private final AspectPropertyValue>> + geographical; + private final AspectPropertyValue>> population; + + public StubIpEngine( + Logger logger, + ElementDataFactory elementDataFactory, + AspectPropertyValue>> geographical, + AspectPropertyValue>> population) { + super(logger, elementDataFactory); + this.geographical = geographical; + this.population = population; + } + + @Override + public String getElementDataKey() { + return "ip"; + } + + @Override + public EvidenceKeyFilter getEvidenceKeyFilter() { + return new EvidenceKeyFilterWhitelist(Collections.emptyList()); + } + + @Override + public List getProperties() { + return Collections.emptyList(); + } + + @Override + protected void processInternal(FlowData data) { + IPIntelligenceData elementData = + data.getOrAdd(getTypedDataKey(), getDataFactory()); + elementData.put("countrycodesgeographical", geographical); + elementData.put("countrycodespopulation", population); + } + + @Override + protected void managedResourcesCleanup() { + } + + @Override + protected void unmanagedResourcesCleanup() { + } +} diff --git a/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/TestIpiData.java b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/TestIpiData.java new file mode 100644 index 0000000..f287e6d --- /dev/null +++ b/ip-intelligence.translation/src/test/java/fiftyone/ipintelligence/translation/testhelpers/TestIpiData.java @@ -0,0 +1,48 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.ipintelligence.translation.testhelpers; + +import fiftyone.ipintelligence.shared.IPIntelligenceDataBase; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.engines.data.AspectData; +import fiftyone.pipeline.engines.data.AspectPropertyMetaData; +import fiftyone.pipeline.engines.flowelements.AspectEngine; +import fiftyone.pipeline.engines.services.MissingPropertyService; +import org.slf4j.Logger; + +/** + * Minimal concrete {@link IPIntelligenceDataBase} used by the tests to expose + * weighted country codes as if they came from the on-premise IP Intelligence + * engine. + */ +public class TestIpiData extends IPIntelligenceDataBase { + + public TestIpiData( + Logger logger, + FlowData flowData, + AspectEngine + engine, + MissingPropertyService missingPropertyService) { + super(logger, flowData, engine, missingPropertyService); + } +} diff --git a/pom.xml b/pom.xml index 62ea847..0f90529 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ ip-intelligence.shared + ip-intelligence.translation ip-intelligence.engine.on-premise ip-intelligence.cloud ip-intelligence