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}