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