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