Skip to content

Commit f9cd7ef

Browse files
committed
Fix: Formatting corrections
1 parent c3e8552 commit f9cd7ef

File tree

3 files changed

+251
-6
lines changed

3 files changed

+251
-6
lines changed

web/src/main/java/org/openmrs/web/filter/initialization/InitializationFilter.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -994,12 +994,9 @@ private void checkLocaleAttributes(HttpServletRequest httpRequest) {
994994
* @param httpRequest the http request object
995995
*/
996996
public void checkLocaleAttributesForFirstTime(HttpServletRequest httpRequest) {
997-
Locale locale = httpRequest.getLocale();
998-
if (CustomResourceLoader.getInstance(httpRequest).getAvailablelocales().contains(locale)) {
999-
httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, locale.toString());
1000-
} else {
1001-
httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, Locale.ENGLISH.toString());
1002-
}
997+
String acceptLanguageHeader = httpRequest.getHeader("Accept-Language");
998+
Locale locale = CustomResourceLoader.getInstance(httpRequest).findBestMatchLocale(acceptLanguageHeader);
999+
httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, locale.toString());
10031000
}
10041001

10051002
/**

web/src/main/java/org/openmrs/web/filter/util/CustomResourceLoader.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import java.io.IOException;
1313
import java.io.InputStreamReader;
1414
import java.nio.charset.StandardCharsets;
15+
import java.util.ArrayList;
1516
import java.util.HashMap;
1617
import java.util.HashSet;
18+
import java.util.List;
1719
import java.util.Locale;
20+
import java.util.Locale.LanguageRange;
1821
import java.util.Map;
1922
import java.util.PropertyResourceBundle;
2023
import java.util.ResourceBundle;
@@ -67,6 +70,19 @@ private CustomResourceLoader(HttpServletRequest httpRequest) {
6770
log.error(ex.getMessage(), ex);
6871
}
6972
}
73+
74+
/**
75+
* Protected constructor for testing purposes that allows injecting a specific
76+
* set of available locales.
77+
* This enables unit testing of locale matching logic without file system
78+
* dependencies.
79+
*
80+
* @param availableLocales the set of locales to be used as available locales
81+
*/
82+
protected CustomResourceLoader(Set<Locale> availableLocales) {
83+
this.resources = new HashMap<>();
84+
this.availablelocales = availableLocales != null ? availableLocales : new HashSet<>();
85+
}
7086

7187
/**
7288
* Returns singleton instance of custom resource loader
@@ -130,4 +146,41 @@ public Map<Locale, ResourceBundle> getResource() {
130146
public Set<Locale> getAvailablelocales() {
131147
return availablelocales;
132148
}
149+
150+
/**
151+
* Finds the best matching locale from the available locales based on the
152+
* Accept-Language header.
153+
* Uses RFC 4647 language range matching to find the best match.
154+
*
155+
* @param acceptLanguageHeader the Accept-Language header value from the HTTP
156+
* request
157+
* (e.g., "fr-BE,fr;q=0.9,en;q=0.8")
158+
* @return the best matching locale from available locales, or Locale.ENGLISH if
159+
* no match is found
160+
* or if the header is null/empty
161+
*/
162+
public Locale findBestMatchLocale(String acceptLanguageHeader) {
163+
// Return default locale if header is null or empty
164+
if (acceptLanguageHeader == null || acceptLanguageHeader.trim().isEmpty()) {
165+
return Locale.ENGLISH;
166+
}
167+
168+
try {
169+
// Parse the Accept-Language header into language ranges
170+
List<LanguageRange> languageRanges = LanguageRange.parse(acceptLanguageHeader);
171+
172+
// Convert available locales set to list for Locale.lookup()
173+
List<Locale> availableLocalesList = new ArrayList<>(getAvailablelocales());
174+
175+
// Use RFC 4647 lookup to find the best match
176+
Locale matchedLocale = Locale.lookup(languageRanges, availableLocalesList);
177+
178+
// Return matched locale or default to English if no match found
179+
return matchedLocale != null ? matchedLocale : Locale.ENGLISH;
180+
} catch (IllegalArgumentException ex) {
181+
// If parsing fails (malformed header), log and return default
182+
log.warn("Failed to parse Accept-Language header: {}", acceptLanguageHeader, ex);
183+
return Locale.ENGLISH;
184+
}
185+
}
133186
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.web.filter.util;
11+
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
15+
import java.util.HashSet;
16+
import java.util.Locale;
17+
import java.util.Set;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
22+
/**
23+
* Unit tests for {@link CustomResourceLoader} locale matching functionality.
24+
* These tests verify RFC 4647 language range matching behavior.
25+
*/
26+
public class CustomResourceLoaderTest {
27+
28+
private CustomResourceLoader resourceLoader;
29+
private Set<Locale> availableLocales;
30+
31+
@BeforeEach
32+
public void setup() {
33+
// Create a set of available locales for testing
34+
availableLocales = new HashSet<>();
35+
availableLocales.add(Locale.ENGLISH);
36+
availableLocales.add(Locale.FRENCH);
37+
availableLocales.add(new Locale("es")); // Spanish
38+
availableLocales.add(new Locale("pt", "BR")); // Portuguese (Brazil)
39+
40+
// Use the protected constructor to inject test locales
41+
resourceLoader = new CustomResourceLoader(availableLocales);
42+
}
43+
44+
@Test
45+
public void testFindBestMatchLocale_ExactMatch() {
46+
// Test exact match for French
47+
Locale result = resourceLoader.findBestMatchLocale("fr");
48+
assertEquals(Locale.FRENCH, result);
49+
}
50+
51+
@Test
52+
public void testFindBestMatchLocale_ExactMatchWithRegion() {
53+
// Test exact match for Portuguese (Brazil)
54+
Locale result = resourceLoader.findBestMatchLocale("pt-BR");
55+
assertEquals(new Locale("pt", "BR"), result);
56+
}
57+
58+
@Test
59+
public void testFindBestMatchLocale_RegionalFallback() {
60+
// Test that fr-BE (Belgian French) falls back to fr (French)
61+
// since Belgian French is not available but French is
62+
Locale result = resourceLoader.findBestMatchLocale("fr-BE");
63+
assertEquals(Locale.FRENCH, result);
64+
}
65+
66+
@Test
67+
public void testFindBestMatchLocale_WithQualityWeights() {
68+
// Test with quality weights - should match the highest priority available
69+
// locale
70+
Locale result = resourceLoader.findBestMatchLocale("fr-BE,fr;q=0.9,en;q=0.8");
71+
// Should match French (fr) since fr-BE is not available but fr is
72+
assertEquals(Locale.FRENCH, result);
73+
}
74+
75+
@Test
76+
public void testFindBestMatchLocale_QualityWeightsPreferEnglish() {
77+
// Test with English having higher priority than unavailable locales
78+
Locale result = resourceLoader.findBestMatchLocale("de;q=0.9,en;q=0.8");
79+
// German is not available, so should fall back to English
80+
assertEquals(Locale.ENGLISH, result);
81+
}
82+
83+
@Test
84+
public void testFindBestMatchLocale_NullHeader() {
85+
// Test that null header returns default English locale
86+
Locale result = resourceLoader.findBestMatchLocale(null);
87+
assertEquals(Locale.ENGLISH, result);
88+
}
89+
90+
@Test
91+
public void testFindBestMatchLocale_EmptyHeader() {
92+
// Test that empty header returns default English locale
93+
Locale result = resourceLoader.findBestMatchLocale("");
94+
assertEquals(Locale.ENGLISH, result);
95+
}
96+
97+
@Test
98+
public void testFindBestMatchLocale_WhitespaceHeader() {
99+
// Test that whitespace-only header returns default English locale
100+
Locale result = resourceLoader.findBestMatchLocale(" ");
101+
assertEquals(Locale.ENGLISH, result);
102+
}
103+
104+
@Test
105+
public void testFindBestMatchLocale_NoMatch() {
106+
// Test that when no locale matches, it returns English as default
107+
Locale result = resourceLoader.findBestMatchLocale("de");
108+
assertEquals(Locale.ENGLISH, result);
109+
}
110+
111+
@Test
112+
public void testFindBestMatchLocale_NoMatchMultiple() {
113+
// Test with multiple non-matching locales
114+
Locale result = resourceLoader.findBestMatchLocale("de,it,ja");
115+
assertEquals(Locale.ENGLISH, result);
116+
}
117+
118+
@Test
119+
public void testFindBestMatchLocale_ComplexHeader() {
120+
// Test with a complex Accept-Language header
121+
Locale result = resourceLoader.findBestMatchLocale("fr-CH,fr;q=0.9,en;q=0.8,de;q=0.7,*;q=0.5");
122+
// Should match French (fr) since fr-CH is not available but fr is
123+
assertEquals(Locale.FRENCH, result);
124+
}
125+
126+
@Test
127+
public void testFindBestMatchLocale_CaseInsensitive() {
128+
// Test that locale matching is case-insensitive
129+
Locale result = resourceLoader.findBestMatchLocale("FR");
130+
assertEquals(Locale.FRENCH, result);
131+
}
132+
133+
@Test
134+
public void testFindBestMatchLocale_MixedCase() {
135+
// Test with mixed case locale codes
136+
Locale result = resourceLoader.findBestMatchLocale("Es");
137+
assertEquals(new Locale("es"), result);
138+
}
139+
140+
@Test
141+
public void testFindBestMatchLocale_MultipleRangesFirstMatch() {
142+
// Test that the first matching locale is returned when multiple match
143+
Locale result = resourceLoader.findBestMatchLocale("en,fr,es");
144+
// Should match English as it's first in the list
145+
assertEquals(Locale.ENGLISH, result);
146+
}
147+
148+
@Test
149+
public void testFindBestMatchLocale_PortugueseBrazilFallback() {
150+
// Test that pt (Portuguese) does NOT match pt-BR
151+
// RFC 4647 lookup only matches more specific to less specific, not vice versa
152+
Locale result = resourceLoader.findBestMatchLocale("pt");
153+
// Since only pt-BR is available (not generic pt), it should return English as
154+
// default
155+
assertEquals(Locale.ENGLISH, result);
156+
}
157+
158+
@Test
159+
public void testFindBestMatchLocale_MalformedHeader() {
160+
// Test with malformed header - should return English as default
161+
Locale result = resourceLoader.findBestMatchLocale("this-is-not-valid;;;");
162+
// Should handle gracefully and return default
163+
assertNotNull(result);
164+
assertEquals(Locale.ENGLISH, result);
165+
}
166+
167+
@Test
168+
public void testFindBestMatchLocale_WithWildcard() {
169+
// Test with wildcard in Accept-Language header
170+
Locale result = resourceLoader.findBestMatchLocale("de,*;q=0.5");
171+
// German is not available, wildcard should match any available locale
172+
// In this case, it should still return English as the default
173+
assertNotNull(result);
174+
}
175+
176+
@Test
177+
public void testConstructorWithNullLocales() {
178+
// Test that constructor handles null locale set gracefully
179+
CustomResourceLoader loader = new CustomResourceLoader((Set<Locale>) null);
180+
assertNotNull(loader.getAvailablelocales());
181+
assertEquals(0, loader.getAvailablelocales().size());
182+
}
183+
184+
@Test
185+
public void testConstructorWithEmptyLocales() {
186+
// Test constructor with empty locale set
187+
CustomResourceLoader loader = new CustomResourceLoader(new HashSet<>());
188+
assertNotNull(loader.getAvailablelocales());
189+
assertEquals(0, loader.getAvailablelocales().size());
190+
191+
// Should still return English as default when no locales are available
192+
Locale result = loader.findBestMatchLocale("fr");
193+
assertEquals(Locale.ENGLISH, result);
194+
}
195+
}

0 commit comments

Comments
 (0)