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.collect.Lists;
023import com.google.common.collect.Maps;
024import org.apache.commons.io.IOUtils;
025import org.hamcrest.BaseMatcher;
026import org.hamcrest.Description;
027import org.sonar.test.TestUtils;
028
029import java.io.*;
030import java.net.MalformedURLException;
031import java.net.URL;
032import java.util.Collection;
033import java.util.Map;
034import java.util.Properties;
035import java.util.SortedMap;
036
037import static org.hamcrest.Matchers.is;
038import static org.hamcrest.Matchers.notNullValue;
039import static org.junit.Assert.assertThat;
040import static org.junit.Assert.fail;
041
042public 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}