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