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 }