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.Lists; 024import com.google.common.collect.Maps; 025import org.apache.commons.io.IOUtils; 026import org.hamcrest.BaseMatcher; 027import org.hamcrest.Description; 028import org.sonar.test.TestUtils; 029 030import java.io.BufferedInputStream; 031import java.io.File; 032import java.io.FileInputStream; 033import java.io.FileOutputStream; 034import java.io.FileWriter; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.OutputStream; 038import java.net.MalformedURLException; 039import java.net.URI; 040import java.net.URL; 041import java.util.Collection; 042import java.util.Map; 043import java.util.Properties; 044import java.util.SortedMap; 045 046import static org.hamcrest.Matchers.is; 047import static org.hamcrest.Matchers.notNullValue; 048import static org.junit.Assert.assertThat; 049import static org.junit.Assert.fail; 050 051public class BundleSynchronizedMatcher extends BaseMatcher<String> { 052 053 public static final String L10N_PATH = "/org/sonar/l10n/"; 054 private static final Collection<String> CORE_BUNDLES = Lists.newArrayList("checkstyle.properties", "core.properties", 055 "findbugs.properties", "gwt.properties", "pmd.properties", "squidjava.properties"); 056 private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/"; 057 058 private String sonarVersion; 059 private URI referenceEnglishBundleURI; 060 // we use this variable to be able to unit test this class without looking at the real Github core bundles that change all the time 061 private String remote_file_path; 062 private String bundleName; 063 private SortedMap<String, String> missingKeys; 064 private SortedMap<String, String> additionalKeys; 065 066 public BundleSynchronizedMatcher() { 067 this(null, GITHUB_RAW_FILE_PATH); 068 } 069 070 public BundleSynchronizedMatcher(URI referenceEnglishBundleURI) { 071 this.referenceEnglishBundleURI = referenceEnglishBundleURI; 072 } 073 074 public BundleSynchronizedMatcher(String sonarVersion) { 075 this(sonarVersion, GITHUB_RAW_FILE_PATH); 076 } 077 078 @VisibleForTesting 079 BundleSynchronizedMatcher(String sonarVersion, String remote_file_path) { 080 this.sonarVersion = sonarVersion; 081 this.remote_file_path = remote_file_path; 082 } 083 084 public boolean matches(Object arg0) { 085 if (!(arg0 instanceof String)) { 086 return false; 087 } 088 bundleName = (String) arg0; 089 090 File bundle = getBundleFileFromClasspath(bundleName); 091 092 // Find the default bundle name which should be compared to 093 String defaultBundleName = extractDefaultBundleName(bundleName); 094 File defaultBundle; 095 if (isCoreBundle(defaultBundleName)) { 096 defaultBundle = getBundleFileFromGithub(defaultBundleName); 097 } else if (referenceEnglishBundleURI != null) { 098 defaultBundle = getBundleFileFromProvidedURI(defaultBundleName); 099 } else { 100 defaultBundle = getBundleFileFromClasspath(defaultBundleName); 101 } 102 103 // and now let's compare 104 try { 105 missingKeys = retrieveMissingTranslations(bundle, defaultBundle); 106 additionalKeys = retrieveMissingTranslations(defaultBundle, bundle); 107 return missingKeys.isEmpty(); 108 } catch (IOException e) { 109 fail("An error occured while reading the bundles: " + e.getMessage()); 110 return false; 111 } 112 } 113 114 public void describeTo(Description description) { 115 // report file 116 File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); 117 118 // prepare message 119 StringBuilder details = prepareDetailsMessage(dumpFile); 120 description.appendText(details.toString()); 121 122 // print report in target directory 123 printReport(dumpFile, details.toString()); 124 } 125 126 private StringBuilder prepareDetailsMessage(File dumpFile) { 127 StringBuilder details = new StringBuilder("\n=======================\n'"); 128 details.append(bundleName); 129 details.append("' is not up-to-date."); 130 print("\n\n Missing translations are:", missingKeys, details); 131 print("\n\nThe following translations do not exist in the reference bundle:", additionalKeys, details); 132 details.append("\n\nSee report file located at: " + dumpFile.getAbsolutePath()); 133 details.append("\n======================="); 134 return details; 135 } 136 137 private void print(String title, SortedMap<String, String> translations, StringBuilder to) { 138 if (!translations.isEmpty()) { 139 to.append(title); 140 for (Map.Entry<String, String> entry : translations.entrySet()) { 141 to.append("\n").append(entry.getKey()).append("=").append(entry.getValue()); 142 } 143 } 144 } 145 146 private void printReport(File dumpFile, String details) { 147 if (dumpFile.exists()) { 148 dumpFile.delete(); 149 } 150 dumpFile.getParentFile().mkdirs(); 151 FileWriter writer = null; 152 try { 153 writer = new FileWriter(dumpFile); 154 writer.write(details); 155 } catch (IOException e) { 156 throw new IllegalStateException("Unable to write the report to 'target/l10n/" + bundleName + ".report.txt'", e); 157 } finally { 158 IOUtils.closeQuietly(writer); 159 } 160 } 161 162 protected SortedMap<String, String> retrieveMissingTranslations(File bundle, File referenceBundle) throws IOException { 163 SortedMap<String, String> missingKeys = Maps.newTreeMap(); 164 165 Properties bundleProps = loadProperties(bundle); 166 Properties referenceProperties = loadProperties(referenceBundle); 167 168 for (Map.Entry<Object, Object> entry : referenceProperties.entrySet()) { 169 String key = (String) entry.getKey(); 170 if (!bundleProps.containsKey(key)) { 171 missingKeys.put(key, (String) entry.getValue()); 172 } 173 } 174 175 return missingKeys; 176 } 177 178 private Properties loadProperties(File f) throws IOException { 179 Properties props = new Properties(); 180 FileInputStream input = new FileInputStream(f); 181 try { 182 props.load(input); 183 return props; 184 185 } finally { 186 IOUtils.closeQuietly(input); 187 } 188 } 189 190 protected File getBundleFileFromGithub(String defaultBundleName) { 191 String remoteFile = computeGitHubURL(defaultBundleName, sonarVersion); 192 URL remoteFileURL = null; 193 try { 194 remoteFileURL = new URL(remoteFile); 195 } catch (MalformedURLException e) { 196 fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); 197 } 198 return downloadRemoteFile(defaultBundleName, remoteFileURL); 199 } 200 201 protected String computeGitHubURL(String defaultBundleName, String sonarVersion) { 202 String computedURL = remote_file_path + defaultBundleName; 203 if (sonarVersion != null && !sonarVersion.contains("-SNAPSHOT")) { 204 computedURL = computedURL.replace("/master/", "/" + sonarVersion + "/"); 205 } 206 return computedURL; 207 } 208 209 protected File getBundleFileFromClasspath(String bundleName) { 210 File bundle = TestUtils.getResource(L10N_PATH + bundleName); 211 assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle, notNullValue()); 212 assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle.exists(), is(true)); 213 return bundle; 214 } 215 216 private File getBundleFileFromProvidedURI(String defaultBundleName) { 217 URL remoteFileURL = null; 218 try { 219 remoteFileURL = referenceEnglishBundleURI.toURL(); 220 } catch (MalformedURLException e) { 221 fail("Could not download the original bundle at: " + remote_file_path + defaultBundleName); 222 } 223 return downloadRemoteFile(defaultBundleName, remoteFileURL); 224 } 225 226 private File downloadRemoteFile(String defaultBundleName, URL remoteFileUrl) { 227 File localBundle = new File("target/l10n/download/" + defaultBundleName); 228 try { 229 saveUrlToLocalFile(remoteFileUrl, localBundle); 230 } catch (IOException e) { 231 fail("Could not download the original core bundle at: " + remoteFileUrl.toString() + defaultBundleName); 232 } 233 assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle, notNullValue()); 234 assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle.exists(), is(true)); 235 return localBundle; 236 } 237 238 private void saveUrlToLocalFile(URL url, File localFile) throws IOException { 239 if (localFile.exists()) { 240 localFile.delete(); 241 } 242 localFile.getParentFile().mkdirs(); 243 244 InputStream in = null; 245 OutputStream fout = null; 246 try { 247 in = new BufferedInputStream(url.openStream()); 248 fout = new FileOutputStream(localFile); 249 250 byte data[] = new byte[1024]; 251 int count; 252 while ((count = in.read(data, 0, 1024)) != -1) { 253 fout.write(data, 0, count); 254 } 255 } finally { 256 IOUtils.closeQuietly(in); 257 IOUtils.closeQuietly(fout); 258 } 259 } 260 261 public static String extractDefaultBundleName(String bundleName) { 262 int firstUnderScoreIndex = bundleName.indexOf('_'); 263 assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0, 264 is(true)); 265 return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; 266 } 267 268 public static boolean isCoreBundle(String defaultBundleName) { 269 return CORE_BUNDLES.contains(defaultBundleName); 270 } 271 272}