/* * 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 java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; 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 java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.apache.jmeter.gui.util.JMeterMenuBar; import org.apache.jorphan.util.JOrphanUtils; /* * 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 extends TestCase { private static final String basedir = new File(System.getProperty("user.dir")).getParent(); // assumes the test starts in the bin directory private static final File srcFiledir = new File(basedir,"src"); private static final String MESSAGES = "messages"; private static PropertyResourceBundle defaultPRB; // current default language properties file private static PropertyResourceBundle messagePRB; // messages.properties private static final CharsetEncoder ASCII_ENCODER = Charset.forName("US-ASCII").newEncoder(); // Ensure properties files don't use special characters private static boolean isPureAscii(String v) { return ASCII_ENCODER.canEncode(v); } // 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 int readRF(String res, List<String> l) throws Exception { int fails = 0; InputStream ras = this.getClass().getResourceAsStream(res); if (ras == null){ if (MESSAGES.equals(resourcePrefix)|| lang.length() == 0 ) { throw new IOException("Cannot open resource file "+res); } else { return 0; } } BufferedReader fileReader = null; try { fileReader = new BufferedReader(new InputStreamReader(ras)); String s; while ((s = fileReader.readLine()) != null) { if (s.length() > 0 && !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)) { System.out.println("Invalid key for JMeterUtils " + key); fails++; } } 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("{")) { fails++; System.out.println("Incorrect message format ? (input/output) for: "+key); System.out.println(val); System.out.println(m); } } if (!isPureAscii(val)) { fails++; System.out.println("Incorrect char value in: "+s); } } } return fails; } finally { JOrphanUtils.closeQuietly(fileReader); } } // Helper method to construct resource name private String getResName(String lang) { if (lang.length() == 0) { return resourcePrefix+".properties"; } else { return resourcePrefix+"_" + lang + ".properties"; } } private void check(String resname) throws Exception { check(resname, true);// check that there aren't any extra entries } /* * perform the checks on the resources * */ private void check(String resname, boolean checkUnexpected) throws Exception { ArrayList<String> alf = new ArrayList<>(500);// holds keys from file String res = getResName(resname); subTestFailures += readRF(res, alf); Collections.sort(alf); // Look for duplicate keys in the file String last = ""; for (String curr : alf) { if (curr.equals(last)) { subTestFailures++; System.out.println("\nDuplicate key =" + curr + " in " + res); } last = curr; } if (resname.length() == 0) // Must be the default resource file { defaultPRB = getRAS(res); if (defaultPRB == null){ throw new IOException("Could not find required file: "+res); } if (resourcePrefix.endsWith(MESSAGES)) { messagePRB = defaultPRB; } } 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))){ System.out.println("Duplicate value? "+key+"="+val+" in "+res); subTestFailures++; } } catch (MissingResourceException e) { subTestFailures++; System.out.println(resourcePrefix + "_" + resname + " has unexpected key: " + key); } } } if (subTestFailures > 0) { fail("One or more subtests failed"); } } private static final String[] prefixList = getResources(srcFiledir); /** * 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) { 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> */ private static void findFile(File file, Set<String> set, FilenameFilter filenameFilter) { File[] foundFiles = file.listFiles(filenameFilter); assertNotNull("Not a directory: "+file, foundFiles); 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 Test suite() { TestSuite ts = new TestSuite("Resources PackageTest"); String languages[] = JMeterMenuBar.getLanguages(); for(String prefix : prefixList){ TestSuite pfx = new TestSuite(prefix) ; pfx.addTest(new PackageTest("testLang","", prefix)); // load the default resource for(String language : languages){ if (!"en".equals(language)){ // Don't try to check the default language pfx.addTest(new PackageTest("testLang", language, prefix)); } } ts.addTest(pfx); } ts.addTest(new PackageTest("checkI18n", "fr")); // TODO Add these some day // ts.addTest(new PackageTest("checkI18n", "es")); // ts.addTest(new PackageTest("checkI18n", "pl")); // ts.addTest(new PackageTest("checkI18n", "pt_BR")); // ts.addTest(new PackageTest("checkI18n", "tr")); // ts.addTest(new PackageTest("checkI18n", Locale.JAPANESE.toString())); // ts.addTest(new PackageTest("checkI18n", Locale.SIMPLIFIED_CHINESE.toString())); // ts.addTest(new PackageTest("checkI18n", Locale.TRADITIONAL_CHINESE.toString())); ts.addTest(new PackageTest("checkResourceReferences", "")); return ts; } private int subTestFailures; private final String lang; private final String resourcePrefix; // e.g. "/org/apache/jmeter/resources/messages" public PackageTest(String testName, String _lang) { this(testName, _lang, MESSAGES); } public PackageTest(String testName, String _lang, String propName) { super(testName); lang=_lang; subTestFailures = 0; resourcePrefix = propName; } public void testLang() throws Exception{ check(lang); } /** * Check all messages are available in one language * @throws Exception if something fails */ public void checkI18n() 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 , missingLabelsPerBundle, messages,prefix.substring(1), lang); } assertEquals(missingLabelsPerBundle.size()+" missing labels, labels missing:"+printLabels(missingLabelsPerBundle), 0, missingLabelsPerBundle.size()); } /** * Check messages are available in language * @param missingLabelsPerBundle2 * @param missingLabelsPerBundle * @param messages Properties messages in english * @param language Language * @throws IOException */ private void checkMessagesForLanguage(Map<String, Map<String, String>> missingLabelsPerBundle, Map<String, Map<String, String>> missingLabelsPerBundle2, 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); } } /** * Build message with missing labels per bundle. * * @param missingLabelsPerBundle * @return String */ 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(); } // Check that calls to getResString use a valid property key name public void checkResourceReferences() { final AtomicInteger errors = new AtomicInteger(0); findFile(srcFiledir, null, new FilenameFilter() { @Override public boolean accept(File dir, String name) { final File file = new File(dir, name); // Look for calls to JMeterUtils.getResString() final Pattern pat = Pattern.compile(".*getResString\\(\"([^\"]+)\"\\).*"); if (name.endsWith(".java")) { BufferedReader fileReader = null; try { fileReader = new BufferedReader(new FileReader(file)); String s; while ((s = fileReader.readLine()) != null) { if (s.matches("\\s*//.*")) { // leading comment continue; } Matcher m = pat.matcher(s); if (m.matches()) { final String key = m.group(1); // Resource keys cannot contain spaces, and are forced to lower case String resKey = key.replace(' ', '_'); // $NON-NLS-1$ // $NON-NLS-2$ resKey = resKey.toLowerCase(java.util.Locale.ENGLISH); if (!key.equals(resKey)) { System.out.println(file+": non-standard message key: '"+key+"'"); } try { messagePRB.getString(resKey); } catch (MissingResourceException e) { System.out.println(file+": missing message key: '"+key+"'"); errors.incrementAndGet(); } } } } catch (IOException e) { e.printStackTrace(); } finally { JOrphanUtils.closeQuietly(fileReader); } } return file.isDirectory(); } }); int errs = errors.get(); if (errs > 0) { fail("Detected "+errs+" missing message property keys"); } } }