001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2013 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * SonarQube 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     * SonarQube 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 License
017     * along with this program; if not, write to the Free Software Foundation,
018     * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019     */
020    package org.sonar.test.i18n;
021    
022    import com.google.common.annotations.VisibleForTesting;
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    
028    import java.io.File;
029    import java.io.FileWriter;
030    import java.io.IOException;
031    import java.io.InputStream;
032    import java.util.Map;
033    import java.util.Properties;
034    import java.util.SortedMap;
035    
036    import static org.hamcrest.Matchers.is;
037    import static org.hamcrest.Matchers.notNullValue;
038    import static org.junit.Assert.assertThat;
039    import static org.junit.Assert.fail;
040    
041    public class BundleSynchronizedMatcher extends BaseMatcher<String> {
042    
043      public static final String L10N_PATH = "/org/sonar/l10n/";
044    
045      private String bundleName;
046      private SortedMap<String, String> missingKeys;
047      private SortedMap<String, String> additionalKeys;
048    
049      public boolean matches(Object arg0) {
050        if (!(arg0 instanceof String)) {
051          return false;
052        }
053        bundleName = (String) arg0;
054    
055        // Find the bundle that needs to be verified
056        InputStream bundleInputStream = getBundleFileInputStream(bundleName);
057    
058        // Find the default bundle which the provided one should be compared to
059        InputStream defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName);
060    
061        // and now let's compare!
062        try {
063          // search for missing keys
064          missingKeys = retrieveMissingTranslations(bundleInputStream, defaultBundleInputStream);
065    
066          // and now for additional keys
067          bundleInputStream = getBundleFileInputStream(bundleName);
068          defaultBundleInputStream = getDefaultBundleFileInputStream(bundleName);
069          additionalKeys = retrieveMissingTranslations(defaultBundleInputStream, bundleInputStream);
070    
071          // And fail only if there are missing keys
072          return missingKeys.isEmpty();
073        } catch (IOException e) {
074          fail("An error occured while reading the bundles: " + e.getMessage());
075          return false;
076        } finally {
077          IOUtils.closeQuietly(bundleInputStream);
078          IOUtils.closeQuietly(defaultBundleInputStream);
079        }
080      }
081    
082      public void describeTo(Description description) {
083        // report file
084        File dumpFile = new File("target/l10n/" + bundleName + ".report.txt");
085    
086        // prepare message
087        StringBuilder details = prepareDetailsMessage(dumpFile);
088        description.appendText(details.toString());
089    
090        // print report in target directory
091        printReport(dumpFile, details.toString());
092      }
093    
094      private StringBuilder prepareDetailsMessage(File dumpFile) {
095        StringBuilder details = new StringBuilder("\n=======================\n'");
096        details.append(bundleName);
097        details.append("' is not up-to-date.");
098        print("\n\n Missing translations are:", missingKeys, details);
099        print("\n\nThe following translations do not exist in the reference bundle:", additionalKeys, details);
100        details.append("\n\nSee report file located at: ");
101        details.append(dumpFile.getAbsolutePath());
102        details.append("\n=======================");
103        return details;
104      }
105    
106      private void print(String title, SortedMap<String, String> translations, StringBuilder to) {
107        if (!translations.isEmpty()) {
108          to.append(title);
109          for (Map.Entry<String, String> entry : translations.entrySet()) {
110            to.append("\n").append(entry.getKey()).append("=").append(entry.getValue());
111          }
112        }
113      }
114    
115      private void printReport(File dumpFile, String details) {
116        if (dumpFile.exists()) {
117          dumpFile.delete();
118        }
119        dumpFile.getParentFile().mkdirs();
120        FileWriter writer = null;
121        try {
122          writer = new FileWriter(dumpFile);
123          writer.write(details);
124        } catch (IOException e) {
125          throw new IllegalStateException("Unable to write the report to 'target/l10n/" + bundleName + ".report.txt'", e);
126        } finally {
127          IOUtils.closeQuietly(writer);
128        }
129      }
130    
131      @VisibleForTesting
132      protected static SortedMap<String, String> retrieveMissingTranslations(InputStream bundle, InputStream referenceBundle) throws IOException {
133        SortedMap<String, String> missingKeys = Maps.newTreeMap();
134    
135        Properties bundleProps = loadProperties(bundle);
136        Properties referenceProperties = loadProperties(referenceBundle);
137    
138        for (Map.Entry<Object, Object> entry : referenceProperties.entrySet()) {
139          String key = (String) entry.getKey();
140          if (!bundleProps.containsKey(key)) {
141            missingKeys.put(key, (String) entry.getValue());
142          }
143        }
144    
145        return missingKeys;
146      }
147    
148      @VisibleForTesting
149      protected static Properties loadProperties(InputStream inputStream) throws IOException {
150        Properties props = new Properties();
151        props.load(inputStream);
152        return props;
153      }
154    
155      @VisibleForTesting
156      protected static InputStream getBundleFileInputStream(String bundleName) {
157        InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + bundleName);
158        assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle, notNullValue());
159        return bundle;
160      }
161    
162      @VisibleForTesting
163      protected static InputStream getDefaultBundleFileInputStream(String bundleName) {
164        String defaultBundleName = extractDefaultBundleName(bundleName);
165        InputStream bundle = BundleSynchronizedMatcher.class.getResourceAsStream(L10N_PATH + defaultBundleName);
166        assertThat("Default bundle '" + defaultBundleName + "' could not be found: add a dependency to the corresponding plugin in your POM.", bundle, notNullValue());
167        return bundle;
168      }
169    
170      @VisibleForTesting
171      protected static String extractDefaultBundleName(String bundleName) {
172        int firstUnderScoreIndex = bundleName.indexOf('_');
173        assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0,
174            is(true));
175        return bundleName.substring(0, firstUnderScoreIndex) + ".properties";
176      }
177    
178    }