| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to you under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.jmeter.resources; |
| |
| import static org.junit.jupiter.api.Assertions.assertEquals; |
| import static org.junit.jupiter.api.Assertions.assertNotNull; |
| import static org.junit.jupiter.api.Assertions.fail; |
| import static org.junit.jupiter.params.provider.Arguments.arguments; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.MissingResourceException; |
| import java.util.Properties; |
| import java.util.PropertyResourceBundle; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| |
| import org.apache.jmeter.gui.util.JMeterMenuBar; |
| import org.junit.jupiter.params.ParameterizedTest; |
| import org.junit.jupiter.params.provider.Arguments; |
| import org.junit.jupiter.params.provider.MethodSource; |
| import org.junit.jupiter.params.provider.ValueSource; |
| |
| /* |
| * Created on Nov 29, 2003 |
| * |
| * Test the composition of the messages*.properties files |
| * - properties files exist |
| * - properties files don't have duplicate keys |
| * - non-default properties files don't have any extra keys. |
| * |
| * N.B. If there is a default resource, ResourceBundle does not detect missing |
| * resources, i.e. the presence of messages.properties means that the |
| * ResourceBundle for Locale "XYZ" would still be found, and have the same keys |
| * as the default. This makes it not very useful for checking properties files. |
| * |
| * This is why the tests use Class.getResourceAsStream() etc |
| * |
| * The tests don't quite follow the normal JUnit test strategy of one test per |
| * possible failure. This was done in order to make it easier to report exactly |
| * why the tests failed. |
| */ |
| |
| public class PackageTest { |
| // We assume the test starts in "src/core" directory (which is true for Gradle and IDEs) |
| private static final File resourceFiledir = new File("src/main/resources"); |
| |
| private static final String MESSAGES = "messages"; |
| |
| private static PropertyResourceBundle defaultPRB; // current default language properties file |
| |
| // Read resource into ResourceBundle and store in List |
| private PropertyResourceBundle getRAS(String res) throws Exception { |
| InputStream ras = this.getClass().getResourceAsStream(res); |
| if (ras == null) { |
| return null; |
| } |
| return new PropertyResourceBundle(ras); |
| } |
| |
| private static final Object[] DUMMY_PARAMS = |
| new Object[] { "1", "2", "3", "4", "5", "6", "7", "8", "9" }; |
| |
| // Read resource file saving the keys |
| private void readRF(String res, String resourcePrefix, String lang, List<String> l) throws Exception { |
| InputStream ras = this.getClass().getResourceAsStream(res); |
| if (ras == null){ |
| if (MESSAGES.equals(resourcePrefix)|| lang.isEmpty()) { |
| throw new IOException("Cannot open resource file "+res); |
| } else { |
| return; |
| } |
| } |
| try (BufferedReader fileReader = new BufferedReader(new InputStreamReader(ras, StandardCharsets.UTF_8))) { |
| String s; |
| while ((s = fileReader.readLine()) != null) { |
| if (!s.isEmpty() && !s.startsWith("#") && !s.startsWith("!")) { |
| int equ = s.indexOf('='); |
| String key = s.substring(0, equ); |
| if (resourcePrefix.equals(MESSAGES)){// Only relevant for messages |
| /* |
| * JMeterUtils.getResString() converts space to _ and lowercases |
| * the key, so make sure all keys pass the test |
| */ |
| if (key.contains(" ") || !key.toLowerCase(java.util.Locale.ENGLISH).equals(key)) { |
| failures.add("Invalid key for JMeterUtils " + key); |
| } |
| } |
| String val = s.substring(equ + 1); |
| l.add(key); // Store the key |
| /* |
| * Now check for invalid message format: if string contains {0} |
| * and ' there may be a problem, so do a format with dummy |
| * parameters and check if there is a { in the output. A bit |
| * crude, but should be enough for now. |
| */ |
| if (val.contains("{0}") && val.contains("'")) { |
| String m = java.text.MessageFormat.format(val, DUMMY_PARAMS); |
| if (m.contains("{")) { |
| failures.add("Incorrect message format ? (input/output) for: " + key + |
| ". Output contains {, it seems not all paratemeters were replaced." + |
| "Format: " + val + ", message with dummy parameters: " + m); |
| } |
| } |
| |
| // We don't need to verify ASCII as build system ensures the final properties will be in ASCII |
| // The proper test would be to get value of a well-known resource and validate it |
| //if (!isPureAscii(val)) { |
| // failures.add("Message format should be pure ASCII. Actual format is " + val); |
| //} |
| } |
| } |
| } |
| } |
| |
| // Helper method to construct resource name |
| private String getResName(String lang, String resourcePrefix) { |
| if (lang.isEmpty()) { |
| return resourcePrefix+".properties"; |
| } else { |
| return resourcePrefix+"_" + lang + ".properties"; |
| } |
| } |
| |
| private void check(String resname, String resourcePrefix) throws Exception { |
| check(resname, resourcePrefix, true);// check that there aren't any extra entries |
| } |
| |
| /* |
| * perform the checks on the resources |
| * |
| */ |
| private void check(String resname, String resourcePrefix, boolean checkUnexpected) throws Exception { |
| ArrayList<String> alf = new ArrayList<>(500);// holds keys from file |
| String res = getResName(resname, resourcePrefix); |
| readRF(res, resourcePrefix, resname, alf); |
| Collections.sort(alf); |
| |
| // Look for duplicate keys in the file |
| String last = ""; |
| for (String curr : alf) { |
| if (curr.equals(last)) { |
| failures.add("Duplicate key=" + curr + " in " + res); |
| } |
| last = curr; |
| } |
| |
| if (resname.isEmpty()) // Must be the default resource file |
| { |
| defaultPRB = getRAS(res); |
| if (defaultPRB == null){ |
| throw new IOException("Could not find required file: "+res); |
| } |
| } else if (checkUnexpected) { |
| // Check all the keys are in the default props file |
| PropertyResourceBundle prb = getRAS(res); |
| if (prb == null){ |
| return; |
| } |
| final ArrayList<String> list = Collections.list(prb.getKeys()); |
| Collections.sort(list); |
| final boolean mainResourceFile = resname.startsWith("messages"); |
| for (String key : list) { |
| try { |
| String val = defaultPRB.getString(key); // Also Check key is in default |
| if (mainResourceFile && val.equals(prb.getString(key))){ |
| failures.add("Duplicate value? "+key+"="+val+" in "+res); |
| } |
| } catch (MissingResourceException e) { |
| failures.add(resourcePrefix + "_" + resname + " has unexpected key: " + key); |
| } |
| } |
| } |
| |
| if (failures.isEmpty()) { |
| return; |
| } |
| fail(String.join("\n", failures)); |
| } |
| |
| private static final String[] prefixList = getResources(resourceFiledir); |
| |
| /** |
| * Find I18N resources in classpath |
| * @param srcFileDir directory in which the files reside |
| * @return list of properties files subject to I18N |
| */ |
| public static String[] getResources(File srcFileDir) { |
| if (!srcFileDir.exists() && "resources".equals(srcFileDir.getName())) { |
| // Allow non-existing resources directory |
| return new String[0]; |
| } |
| Set<String> set = new TreeSet<>(); |
| findFile(srcFileDir, set, new FilenameFilter() { |
| @Override |
| public boolean accept(File dir, String name) { |
| return name.equals("messages.properties") || |
| name.endsWith("Resources.properties") |
| && !name.matches("Example\\d+Resources\\.properties") |
| || new File(dir, name).isDirectory(); |
| } |
| }); |
| return set.toArray(new String[set.size()]); |
| } |
| |
| /** |
| * Find resources matching filenameFiler and adds them to set removing |
| * everything before "/org" |
| * |
| * @param file |
| * directory in which the files reside |
| * @param set |
| * container into which the names of the files should be added |
| * @param filenameFilter |
| * filter that the files must satisfy to be included into |
| * <code>set</code> |
| */ |
| static void findFile(File file, Set<String> set, |
| FilenameFilter filenameFilter) { |
| File[] foundFiles = file.listFiles(filenameFilter); |
| assertNotNull(foundFiles, "Not a directory: "+file); |
| for (File file2 : foundFiles) { |
| if (file2.isDirectory()) { |
| findFile(file2, set, filenameFilter); |
| } else { |
| String absPath2 = file2.getAbsolutePath().replace('\\', '/'); // Fix up Windows paths |
| int indexOfOrg = absPath2.indexOf("/org"); |
| int lastIndex = absPath2.lastIndexOf('.'); |
| set.add(absPath2.substring(indexOfOrg, lastIndex)); |
| } |
| } |
| } |
| |
| /* |
| * Use a suite to ensure that the default is done first |
| */ |
| public static Collection<Arguments> languagesAndPrefixes() { |
| Collection<Arguments> res = new ArrayList<>(); |
| String[] languages = JMeterMenuBar.getLanguages(); |
| for(String prefix : prefixList){ |
| res.add(arguments("", prefix)); // load the default resource |
| for(String language : languages){ |
| if (!"en".equals(language)){ // Don't try to check the default language |
| res.add(arguments(language, prefix)); |
| } |
| } |
| } |
| return res; |
| } |
| |
| private List<String> failures = new ArrayList<>(); |
| |
| @ParameterizedTest |
| @MethodSource("languagesAndPrefixes") |
| public void testLang(String lang, String resourcePrefix) throws Exception{ |
| check(lang, resourcePrefix); |
| } |
| |
| /** |
| * Check all messages are available in one language |
| * @throws Exception if something fails |
| */ |
| @ParameterizedTest |
| @ValueSource(strings = {"fr"}) |
| public void checkI18n(String lang) throws Exception { |
| Map<String, Map<String,String>> missingLabelsPerBundle = new HashMap<>(); |
| for (String prefix : prefixList) { |
| Properties messages = new Properties(); |
| messages.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(prefix.substring(1)+".properties")); |
| checkMessagesForLanguage( missingLabelsPerBundle , messages,prefix.substring(1), lang); |
| } |
| |
| assertEquals(0, missingLabelsPerBundle.size(), missingLabelsPerBundle.size()+" missing labels, labels missing:"+printLabels(missingLabelsPerBundle)); |
| } |
| |
| private void checkMessagesForLanguage(Map<String, Map<String, String>> missingLabelsPerBundle, |
| Properties messages, String bundlePath, String language) |
| throws IOException { |
| Properties messagesFr = new Properties(); |
| String languageBundle = bundlePath+"_"+language+ ".properties"; |
| InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(languageBundle); |
| if(inputStream == null) { |
| Map<String, String> messagesAsProperties = new HashMap<>(); |
| for (Map.Entry<Object, Object> entry : messages.entrySet()) { |
| messagesAsProperties.put((String) entry.getKey(), (String) entry.getValue()); |
| } |
| missingLabelsPerBundle.put(languageBundle, messagesAsProperties); |
| return; |
| } |
| messagesFr.load(inputStream); |
| |
| Map<String, String> missingLabels = new TreeMap<>(); |
| for (Map.Entry<Object, Object> entry : messages.entrySet()) { |
| String key = (String) entry.getKey(); |
| final String I18NString = "[\\d% ]+";// numeric, space and % don't need translation |
| if (!messagesFr.containsKey(key)) { |
| String value = (String) entry.getValue(); |
| // TODO improve check of values that don't need translation |
| if (value.matches(I18NString)) { |
| System.out.println("Ignoring missing " + key + "=" + value + " in " + languageBundle); // TODO convert to list and display at end |
| } else { |
| missingLabels.put(key, (String) entry.getValue()); |
| } |
| } else { |
| String value = (String) entry.getValue(); |
| if (value.matches(I18NString)) { |
| System.out.println("Unnecessary entry " + key + "=" + value + " in " + languageBundle); |
| } |
| } |
| } |
| if (!missingLabels.isEmpty()) { |
| missingLabelsPerBundle.put(languageBundle, missingLabels); |
| } |
| } |
| |
| private String printLabels(Map<String, Map<String, String>> missingLabelsPerBundle) { |
| StringBuilder builder = new StringBuilder(); |
| for (Map.Entry<String, Map<String, String>> entry : missingLabelsPerBundle.entrySet()) { |
| builder.append("Missing labels in bundle:") |
| .append(entry.getKey()) |
| .append("\r\n"); |
| for (Map.Entry<String, String> entry2 : entry.getValue().entrySet()) { |
| builder.append(entry2.getKey()) |
| .append("=") |
| .append(entry2.getValue()) |
| .append("\r\n"); |
| } |
| builder.append("======================================================\r\n"); |
| } |
| return builder.toString(); |
| } |
| } |