001/* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2014 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * SonarQube 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 * SonarQube 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 org.apache.commons.io.Charsets; 023import org.apache.commons.io.IOUtils; 024import org.hamcrest.BaseMatcher; 025import org.hamcrest.Description; 026 027import java.io.File; 028import java.io.FileOutputStream; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.OutputStreamWriter; 032import java.io.Writer; 033import java.util.Map; 034import java.util.Properties; 035import java.util.SortedMap; 036import java.util.TreeMap; 037 038import static org.hamcrest.Matchers.is; 039import static org.hamcrest.Matchers.notNullValue; 040import static org.junit.Assert.assertThat; 041import static org.junit.Assert.fail; 042 043public class BundleSynchronizedMatcher extends BaseMatcher<String> { 044 045 public static final String L10N_PATH = "/org/sonar/l10n/"; 046 047 private String bundleName; 048 private SortedMap<String, String> missingKeys; 049 private SortedMap<String, String> additionalKeys; 050 051 @Override 052 public boolean matches(Object arg0) { 053 if (!(arg0 instanceof String)) { 054 return false; 055 } 056 bundleName = (String) arg0; 057 058 // Find the bundle that needs to be verified 059 InputStream bundleInputStream = getBundleFileInputStream(bundleName); 060 061 // Find the default bundle which the provided one should be compared to 062 InputStream defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName); 063 064 // and now let's compare! 065 try { 066 // search for missing keys 067 missingKeys = retrieveMissingTranslations(bundleInputStream, defaultBundleInputStream); 068 069 // and now for additional keys 070 bundleInputStream = getBundleFileInputStream(bundleName); 071 defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName); 072 additionalKeys = retrieveMissingTranslations(defaultBundleInputStream, bundleInputStream); 073 074 // And fail only if there are missing keys 075 return missingKeys.isEmpty(); 076 } catch (IOException e) { 077 fail("An error occurred while reading the bundles: " + e.getMessage()); 078 return false; 079 } finally { 080 IOUtils.closeQuietly(bundleInputStream); 081 IOUtils.closeQuietly(defaultBundleInputStream); 082 } 083 } 084 085 @Override 086 public void describeTo(Description description) { 087 // report file 088 File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); 089 090 // prepare message 091 StringBuilder details = prepareDetailsMessage(dumpFile); 092 description.appendText(details.toString()); 093 094 // print report in target directory 095 printReport(dumpFile, details.toString()); 096 } 097 098 private StringBuilder prepareDetailsMessage(File dumpFile) { 099 StringBuilder details = new StringBuilder("\n=======================\n'"); 100 details.append(bundleName); 101 details.append("' is not up-to-date."); 102 print("\n\n Missing translations are:", missingKeys, details); 103 print("\n\nThe following translations do not exist in the reference bundle:", additionalKeys, details); 104 details.append("\n\nSee report file located at: "); 105 details.append(dumpFile.getAbsolutePath()); 106 details.append("\n======================="); 107 return details; 108 } 109 110 private void print(String title, SortedMap<String, String> translations, StringBuilder to) { 111 if (!translations.isEmpty()) { 112 to.append(title); 113 for (Map.Entry<String, String> entry : translations.entrySet()) { 114 to.append("\n").append(entry.getKey()).append("=").append(entry.getValue()); 115 } 116 } 117 } 118 119 private void printReport(File dumpFile, String details) { 120 if (dumpFile.exists()) { 121 dumpFile.delete(); 122 } 123 dumpFile.getParentFile().mkdirs(); 124 try (Writer writer = new OutputStreamWriter(new FileOutputStream(dumpFile), Charsets.UTF_8)) { 125 writer.write(details); 126 } catch (IOException e) { 127 throw new IllegalStateException("Unable to write the report to 'target/l10n/" + bundleName + ".report.txt'", e); 128 } 129 } 130 131 protected static SortedMap<String, String> retrieveMissingTranslations(InputStream bundle, InputStream referenceBundle) throws IOException { 132 SortedMap<String, String> missingKeys = new TreeMap<>(); 133 134 Properties bundleProps = loadProperties(bundle); 135 Properties referenceProperties = loadProperties(referenceBundle); 136 137 for (Map.Entry<Object, Object> entry : referenceProperties.entrySet()) { 138 String key = (String) entry.getKey(); 139 if (!bundleProps.containsKey(key)) { 140 missingKeys.put(key, (String) entry.getValue()); 141 } 142 } 143 144 return missingKeys; 145 } 146 147 protected static Properties loadProperties(InputStream inputStream) throws IOException { 148 Properties props = new Properties(); 149 props.load(inputStream); 150 return props; 151 } 152 153 protected static InputStream getBundleFileInputStream(String bundleName) { 154 InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + bundleName); 155 assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle, notNullValue()); 156 return bundle; 157 } 158 159 protected static InputStream getDefaultBundleFileInputStream(String bundleName) { 160 String defaultBundleName = extractDefaultBundleName(bundleName); 161 InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + defaultBundleName); 162 assertThat("Default bundle '" + defaultBundleName + "' could not be found: add a dependency to the corresponding plugin in your POM.", bundle, notNullValue()); 163 return bundle; 164 } 165 166 protected static String extractDefaultBundleName(String bundleName) { 167 int firstUnderScoreIndex = bundleName.indexOf('_'); 168 assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0, 169 is(true)); 170 return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; 171 } 172 173}