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    }