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.annotations.VisibleForTesting;
023import com.google.common.collect.Maps;
024import org.apache.commons.io.IOUtils;
025import org.hamcrest.BaseMatcher;
026import org.hamcrest.Description;
027
028import java.io.File;
029import java.io.FileWriter;
030import java.io.IOException;
031import java.io.InputStream;
032import java.util.Map;
033import java.util.Properties;
034import java.util.SortedMap;
035
036import static org.hamcrest.Matchers.is;
037import static org.hamcrest.Matchers.notNullValue;
038import static org.junit.Assert.assertThat;
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  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}