From ea94ea782b5efb81386bac81ecddb2421651324f Mon Sep 17 00:00:00 2001 From: Adith Date: Thu, 24 Oct 2024 16:59:14 +1100 Subject: [PATCH] Testing, Documentation and Cleanup. --- .../commons/mwapi/OkHttpJsonApiClient.java | 36 ++++-- .../free/nrw/commons/nearby/NearbyPlaces.java | 1 + .../commons/settings/SettingsFragment.java | 55 +++------ app/src/test/java/StringBuilderTest.java | 74 ++++++++++++ .../SettingsFragmentSecondaryLanguageTests.kt | 110 ++++++++++++++++++ 5 files changed, 230 insertions(+), 46 deletions(-) create mode 100644 app/src/test/java/StringBuilderTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentSecondaryLanguageTests.kt diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index dd041d8bc..132c5c3ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -400,35 +400,43 @@ public class OkHttpJsonApiClient { /** * Retrieves a list of places based on the provided list of places and language. * - * @param placeList A list of Place objects for which to fetch information. - * @param language The language code to use for the query. + * @param placeList A list of Place objects for which to fetch information. + * @param language The language code to use for the query. + * @param secondaryLanguages The serialized secondary language code(s) to use for fallback queries. * @return A list of Place objects with additional information retrieved from Wikidata, or null - * if an error occurs. + * if an error occurs. * @throws IOException If there is an issue with reading the resource file or executing the HTTP * request. */ @Nullable public List getPlaces( final List placeList, final String language, final String secondaryLanguages) throws IOException { - final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); - final String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); // could be used to generate backup SparQL Queries + // Read the SparQL query template from the resource file + final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); + + // Split the secondary languages string into an array to use in fallback queries + final String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); + + // Prepare the Wikidata entity IDs (QIDs) for each place in the list String qids = ""; for (final Place place : placeList) { qids += "\n" + ("wd:" + place.getWikiDataEntityId()); } + // Build fallback descriptions for secondary languages in case the primary language is unavailable StringBuilder fallBackDescription = new StringBuilder(); for (int i = 0; i < secondaryLanguageArray.length; i++) { fallBackDescription.append("OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_") - .append(i + 1) + .append(i + 1) // Unique identifier for each fallback .append(". FILTER (lang(?itemDescriptionPreferredLanguage_") .append(i + 1) .append(") = \"") - .append(secondaryLanguageArray[i]) + .append(secondaryLanguageArray[i]) // Use the secondary language code .append("\")}\n"); } + // Build fallback labels for secondary languages StringBuilder fallbackLabel = new StringBuilder(); for (int i = 0; i < secondaryLanguageArray.length; i++) { fallbackLabel.append("OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_") @@ -440,6 +448,7 @@ public class OkHttpJsonApiClient { .append("\")}\n"); } + // Build fallback class labels for secondary languages StringBuilder fallbackClassLabel = new StringBuilder(); for (int i = 0; i < secondaryLanguageArray.length; i++) { fallbackClassLabel.append("OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_") @@ -451,6 +460,7 @@ public class OkHttpJsonApiClient { .append("\")}\n"); } + // Replace placeholders in the query with actual data: QIDs, language codes, and fallback options final String query = wikidataQuery .replace("${ENTITY}", qids) .replace("${LANG}", language) @@ -458,33 +468,41 @@ public class OkHttpJsonApiClient { .replace("${SECONDARYLABEL}", fallbackLabel.toString()) .replace("${SECONDARYCLASSLABEL}", fallbackClassLabel.toString()); + // Build the URL for the SparQL query with the formatted query string final HttpUrl.Builder urlBuilder = HttpUrl .parse(sparqlQueryUrl) .newBuilder() .addQueryParameter("query", query) - .addQueryParameter("format", "json"); + .addQueryParameter("format", "json"); // Ensure JSON response + // Create and send the HTTP request final Request request = new Request.Builder() .url(urlBuilder.build()) .build(); + // Execute the request and handle the response try (Response response = okHttpClient.newCall(request).execute()) { if (response.isSuccessful()) { + // Parse the JSON response and convert it to a list of NearbyResultItems final String json = response.body().string(); final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); final List bindings = nearbyResponse.getResults().getBindings(); + + // Convert each NearbyResultItem into a Place object and return the list of places final List places = new ArrayList<>(); for (final NearbyResultItem item : bindings) { final Place placeFromNearbyItem = Place.from(item); places.add(placeFromNearbyItem); } - return places; + return places; // Return the list of places with additional information } else { + // Handle unsuccessful HTTP response codes throw new IOException("Unexpected response code: " + response.code()); } } } + /** * Make API Call to get Places * diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index cfc45945e..1cd3fdcc2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -127,6 +127,7 @@ public class NearbyPlaces { * * @param placeList A list of Place objects for which to fetch information. * @param lang The language code to use for the query. + * @param lang2 The serialised secondary language code to use for the query. * @return A list of Place objects obtained from the Wikidata query. * @throws Exception If an error occurs during the retrieval process. */ diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 01224b22c..7e95b14ef 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -290,6 +290,13 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } + /** + * Updates the ListView to display saved languages using the SavedLanguagesAdapter. + * + * @param savedLanguageListView The ListView that will display the saved languages. + * @param savedLanguages A list of saved Language objects to be displayed. + * @param selectedLanguages A HashMap containing the selected language IDs and their corresponding names. + */ private void updateSavedLanguages(ListView savedLanguageListView, List savedLanguages, HashMap selectedLanguages) { // Use SavedLanguagesAdapter to display saved languages SavedLanguagesAdapter savedLanguagesAdapter = new SavedLanguagesAdapter( @@ -302,6 +309,12 @@ public class SettingsFragment extends PreferenceFragmentCompat { savedLanguageListView.setAdapter(savedLanguagesAdapter); } + /** + * Deserializes a comma-separated string of language codes into an ArrayList of strings. + * + * @param languageCodes A string containing language codes separated by commas. + * @return An ArrayList of language codes, or an empty ArrayList if the input is null or empty. + */ private ArrayList deSerialise(String languageCodes) { // Check if the stored string is empty or null if (languageCodes == null || languageCodes.isEmpty()) { @@ -313,21 +326,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { return new ArrayList<>(Arrays.asList(languageArray)); // Convert array to ArrayList and return } - + /** + * Prepare and Show language selection dialog box + * Disable default/already selected language from dialog box + * Saves values chosen by user to shared preferences as a serialised string. + */ private void prepareSecondaryLanguageDialog() { final String languageCode = getCurrentLanguageCode("descriptionSecondaryLanguagePref"); HashMap selectedLanguages = new HashMap<>(); assert languageCode != null; selectedLanguages.put(0, Locale.getDefault().getLanguage()); - System.out.println(Locale.getDefault().getLanguage()); - System.out.println(languageCode); // Deserializing saved language codes to Language objects ArrayList savedLanguages = new ArrayList<>(); for (String code : deSerialise(languageCode)) { - System.out.println(code); if(code.equals(Locale.getDefault().getLanguage())){ - System.out.println("match"); continue; } Locale locale = new Locale(code); @@ -633,42 +646,10 @@ public class SettingsFragment extends PreferenceFragmentCompat { separator.setVisibility(View.GONE); } - private String reSerialise(ArrayList languageCodes) { - // Join the elements of the list into a single string, separated by a comma and a space - return String.join(", ", languageCodes); - } - /** * Changing the default app language with selected one and save it to SharedPreferences */ public void setLocale(final Activity activity, String userSelectedValue) { -// if (userSelectedValue.equals("")) { -// userSelectedValue = Locale.getDefault().getLanguage(); -// } -// -// String current = Locale.getDefault().getLanguage(); -// ArrayList languageCodes = deSerialise(current); -// if(appUI) { -// languageCodes.set(0, userSelectedValue); -// userSelectedValue = reSerialise(languageCodes); -// } -// else{ -// ArrayList newLanguageCodes = new ArrayList<>(); -// ArrayList userSelctedCode = deSerialise(userSelectedValue); -// -// newLanguageCodes.add(languageCodes.get(0)); -// for(String code : userSelctedCode){ -// newLanguageCodes.add(code); -// } -// userSelectedValue = reSerialise(newLanguageCodes); -// } -// -// System.out.println("Final locale"); -// System.out.println(userSelectedValue); -// -// System.out.println("vs"); -// System.out.println(getCurrentLanguageCode("appUiDefaultLanguagePref")); -// System.out.println(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); final Locale locale = createLocale(userSelectedValue); Locale.setDefault(locale); diff --git a/app/src/test/java/StringBuilderTest.java b/app/src/test/java/StringBuilderTest.java new file mode 100644 index 000000000..73ad3902b --- /dev/null +++ b/app/src/test/java/StringBuilderTest.java @@ -0,0 +1,74 @@ +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class StringBuilderTest { + @Test + void testFallbackDescriptionBuilder() { + String secondaryLanguages = "en,fr,es"; // Example secondary languages + String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); + + StringBuilder fallBackDescription = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallBackDescription.append("OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_") + .append(i + 1) // Unique identifier for each fallback + .append(". FILTER (lang(?itemDescriptionPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) // Use the secondary language code + .append("\")}\n"); + } + + String expected = "OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_1. FILTER (lang(?itemDescriptionPreferredLanguage_1) = \"en\")}\n" + + "OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_2. FILTER (lang(?itemDescriptionPreferredLanguage_2) = \"fr\")}\n" + + "OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_3. FILTER (lang(?itemDescriptionPreferredLanguage_3) = \"es\")}\n"; + + assertEquals(expected, fallBackDescription.toString()); + } + + @Test + void testFallbackLabelBuilder() { + String secondaryLanguages = "en,fr,es"; // Example secondary languages + String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); + + StringBuilder fallbackLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackLabel.append("OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?itemLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + String expected = "OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_1. FILTER (lang(?itemLabelPreferredLanguage_1) = \"en\")}\n" + + "OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_2. FILTER (lang(?itemLabelPreferredLanguage_2) = \"fr\")}\n" + + "OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_3. FILTER (lang(?itemLabelPreferredLanguage_3) = \"es\")}\n"; + + assertEquals(expected, fallbackLabel.toString()); + } + + @Test + void testFallbackClassLabelBuilder() { + String secondaryLanguages = "en,fr,es"; // Example secondary languages + String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); + + StringBuilder fallbackClassLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackClassLabel.append("OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?classLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + String expected = "OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_1. FILTER (lang(?classLabelPreferredLanguage_1) = \"en\")}\n" + + "OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_2. FILTER (lang(?classLabelPreferredLanguage_2) = \"fr\")}\n" + + "OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_3. FILTER (lang(?classLabelPreferredLanguage_3) = \"es\")}\n"; + + assertEquals(expected, fallbackClassLabel.toString()); + } +} + diff --git a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentSecondaryLanguageTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentSecondaryLanguageTests.kt new file mode 100644 index 000000000..9f95efe4d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentSecondaryLanguageTests.kt @@ -0,0 +1,110 @@ +package fr.free.nrw.commons.settings + +import android.app.Dialog +import android.os.Looper +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.recentlanguages.Language +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import java.lang.reflect.Method + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class SettingsFragmentSecondaryLanguageTests { + + private lateinit var fragment: SettingsFragment + + private lateinit var recentLanguagesDao: RecentLanguagesDao + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + val activity = Robolectric.buildActivity(SettingsActivity::class.java).create().get() + fragment = SettingsFragment() + val fragmentManager = activity.supportFragmentManager + val fragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commitNowAllowingStateLoss() + // Mock RecentLanguagesDao + recentLanguagesDao = Mockito.mock(RecentLanguagesDao::class.java) + fragment.recentLanguagesDao = recentLanguagesDao + } + + @Test + @Throws(Exception::class) + fun `Test prepareSecondaryLanguageDialog is invoked and dialog is created`() { + // Set up the main looper to idle, as necessary in Robolectric tests + Shadows.shadowOf(Looper.getMainLooper()).idle() + + val method: Method = SettingsFragment::class.java.getDeclaredMethod("prepareSecondaryLanguageDialog") + method.isAccessible = true + + method.invoke(fragment) + + // Verify if the dialog was created and is not null + val dialogField = SettingsFragment::class.java.getDeclaredField("dialog") + dialogField.isAccessible = true + val dialog: Dialog? = dialogField.get(fragment) as Dialog? + Assert.assertNotNull(dialog) + } + + @Test + @Throws(Exception::class) + fun `Test prepareSecondaryLanguageDialog adds a language and updates preferences`() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + + // Mock recent languages and saved languages + val savedLanguages = mutableListOf(Language("English", "en")) + + val method: Method = SettingsFragment::class.java.getDeclaredMethod("prepareSecondaryLanguageDialog") + method.isAccessible = true + + method.invoke(fragment) + + val dialogField = SettingsFragment::class.java.getDeclaredField("dialog") + dialogField.isAccessible = true + val dialog: Dialog? = dialogField.get(fragment) as Dialog? + + val newLanguage = Language("German", "de") + savedLanguages.add(newLanguage) + + // Verify if the saved languages now include the newly added language + Assert.assertTrue(savedLanguages.contains(newLanguage)) + } + + @Test + @Throws(Exception::class) + fun `Test prepareSecondaryLanguageDialog removes a language and updates preferences`() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + + // Mock recent languages and saved languages + val savedLanguages = mutableListOf(Language("English", "en"), Language("French", "fr")) + + val method: Method = SettingsFragment::class.java.getDeclaredMethod("prepareSecondaryLanguageDialog") + method.isAccessible = true + + method.invoke(fragment) + + val dialogField = SettingsFragment::class.java.getDeclaredField("dialog") + dialogField.isAccessible = true + val dialog: Dialog? = dialogField.get(fragment) as Dialog? + + // Simulate removing a language from the saved list (e.g., removing "French") + val positionToRemove = 1 + savedLanguages.removeAt(positionToRemove) + + Assert.assertFalse(savedLanguages.any { it.languageCode == "fr" }) + } +} +