001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.test.i18n; 021 022import java.io.File; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStreamWriter; 027import java.io.Writer; 028import java.nio.charset.StandardCharsets; 029import java.util.Map; 030import java.util.Properties; 031import java.util.SortedMap; 032import java.util.TreeMap; 033import org.apache.commons.io.IOUtils; 034import org.hamcrest.BaseMatcher; 035import org.hamcrest.Description; 036 037import static org.junit.Assert.assertNotNull; 038import static org.junit.Assert.assertTrue; 039import static org.junit.Assert.fail; 040 041public class BundleSynchronizedMatcher extends BaseMatcher<String> { 042 043 public static final String L10N_PATH = "/org/sonar/l10n/"; 044 045 private String bundleName; 046 private SortedMap<String, String> missingKeys; 047 private SortedMap<String, String> additionalKeys; 048 049 @Override 050 public boolean matches(Object arg0) { 051 if (!(arg0 instanceof String)) { 052 return false; 053 } 054 bundleName = (String) arg0; 055 056 // Find the bundle that needs to be verified 057 InputStream bundleInputStream = getBundleFileInputStream(bundleName); 058 059 // Find the default bundle which the provided one should be compared to 060 InputStream defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName); 061 062 // and now let's compare! 063 try { 064 // search for missing keys 065 missingKeys = retrieveMissingTranslations(bundleInputStream, defaultBundleInputStream); 066 067 // and now for additional keys 068 bundleInputStream = getBundleFileInputStream(bundleName); 069 defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName); 070 additionalKeys = retrieveMissingTranslations(defaultBundleInputStream, bundleInputStream); 071 072 // And fail only if there are missing keys 073 return missingKeys.isEmpty(); 074 } catch (IOException e) { 075 fail("An error occurred while reading the bundles: " + e.getMessage()); 076 return false; 077 } finally { 078 IOUtils.closeQuietly(bundleInputStream); 079 IOUtils.closeQuietly(defaultBundleInputStream); 080 } 081 } 082 083 @Override 084 public void describeTo(Description description) { 085 // report file 086 File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); 087 088 // prepare message 089 StringBuilder details = prepareDetailsMessage(dumpFile); 090 description.appendText(details.toString()); 091 092 // print report in target directory 093 printReport(dumpFile, details.toString()); 094 } 095 096 private StringBuilder prepareDetailsMessage(File dumpFile) { 097 StringBuilder details = new StringBuilder("\n=======================\n'"); 098 details.append(bundleName); 099 details.append("' is not up-to-date."); 100 print("\n\n Missing translations are:", missingKeys, details); 101 print("\n\nThe following translations do not exist in the reference bundle:", additionalKeys, details); 102 details.append("\n\nSee report file located at: "); 103 details.append(dumpFile.getAbsolutePath()); 104 details.append("\n======================="); 105 return details; 106 } 107 108 private void print(String title, SortedMap<String, String> translations, StringBuilder to) { 109 if (!translations.isEmpty()) { 110 to.append(title); 111 for (Map.Entry<String, String> entry : translations.entrySet()) { 112 to.append("\n").append(entry.getKey()).append("=").append(entry.getValue()); 113 } 114 } 115 } 116 117 private void printReport(File dumpFile, String details) { 118 if (dumpFile.exists()) { 119 dumpFile.delete(); 120 } 121 dumpFile.getParentFile().mkdirs(); 122 try (Writer writer = new OutputStreamWriter(new FileOutputStream(dumpFile), StandardCharsets.UTF_8)) { 123 writer.write(details); 124 } catch (IOException e) { 125 throw new IllegalStateException("Unable to write the report to 'target/l10n/" + bundleName + ".report.txt'", e); 126 } 127 } 128 129 protected static SortedMap<String, String> retrieveMissingTranslations(InputStream bundle, InputStream referenceBundle) throws IOException { 130 SortedMap<String, String> missingKeys = new TreeMap<>(); 131 132 Properties bundleProps = loadProperties(bundle); 133 Properties referenceProperties = loadProperties(referenceBundle); 134 135 for (Map.Entry<Object, Object> entry : referenceProperties.entrySet()) { 136 String key = (String) entry.getKey(); 137 if (!bundleProps.containsKey(key)) { 138 missingKeys.put(key, (String) entry.getValue()); 139 } 140 } 141 142 return missingKeys; 143 } 144 145 protected static Properties loadProperties(InputStream inputStream) throws IOException { 146 Properties props = new Properties(); 147 props.load(inputStream); 148 return props; 149 } 150 151 protected static InputStream getBundleFileInputStream(String bundleName) { 152 InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + bundleName); 153 assertNotNull("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle); 154 return bundle; 155 } 156 157 protected static InputStream getDefaultBundleFileInputStream(String bundleName) { 158 String defaultBundleName = extractDefaultBundleName(bundleName); 159 InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + defaultBundleName); 160 assertNotNull("Default bundle '" + defaultBundleName + "' could not be found: add a dependency to the corresponding plugin in your POM.", bundle); 161 return bundle; 162 } 163 164 protected static String extractDefaultBundleName(String bundleName) { 165 int firstUnderScoreIndex = bundleName.indexOf('_'); 166 assertTrue("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0); 167 return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; 168 } 169 170}