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