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