001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.checks;
020
021import com.google.common.collect.Lists;
022import com.google.common.collect.Maps;
023import com.google.common.collect.Sets;
024import com.puppycrawl.tools.checkstyle.Defn;
025import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
026import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
027import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
028import com.puppycrawl.tools.checkstyle.api.Utils;
029import java.io.File;
030import java.io.FileInputStream;
031import java.io.FileNotFoundException;
032import java.io.IOException;
033import java.io.InputStream;
034import java.util.Enumeration;
035import java.util.List;
036import java.util.Map;
037import java.util.Properties;
038import java.util.Set;
039import java.util.TreeSet;
040import java.util.Map.Entry;
041
042/**
043 * <p>
044 * The TranslationCheck class helps to ensure the correct translation of code by
045 * checking property files for consistency regarding their keys.
046 * Two property files describing one and the same context are consistent if they
047 * contain the same keys.
048 * </p>
049 * <p>
050 * An example of how to configure the check is:
051 * </p>
052 * <pre>
053 * &lt;module name="Translation"/&gt;
054 * </pre>
055 * Check has a property <b>basenameSeparator</b> which allows setting separator in file names,
056 * default value is '_'.
057 * <p>
058 * E.g.:
059 * </p>
060 * <p>
061 * messages_test.properties //separator is '_'
062 * </p>
063 * <p>
064 * app-dev.properties //separator is '-'
065 * </p>
066 * <br>
067 * @author Alexandra Bunge
068 * @author lkuehne
069 */
070public class TranslationCheck
071    extends AbstractFileSetCheck
072{
073    /** The property files to process. */
074    private final List<File> propertyFiles = Lists.newArrayList();
075
076    /** The separator string used to separate translation files */
077    private String basenameSeparator;
078
079    /**
080     * Creates a new <code>TranslationCheck</code> instance.
081     */
082    public TranslationCheck()
083    {
084        setFileExtensions(new String[]{"properties"});
085        setBasenameSeparator("_");
086    }
087
088    @Override
089    public void beginProcessing(String charset)
090    {
091        super.beginProcessing(charset);
092        propertyFiles.clear();
093    }
094
095    @Override
096    protected void processFiltered(File file, List<String> lines)
097    {
098        propertyFiles.add(file);
099    }
100
101    @Override
102    public void finishProcessing()
103    {
104        super.finishProcessing();
105        final Map<String, Set<File>> propFilesMap =
106            arrangePropertyFiles(propertyFiles, basenameSeparator);
107        checkPropertyFileSets(propFilesMap);
108    }
109
110    /**
111     * Gets the basename (the unique prefix) of a property file. For example
112     * "xyz/messages" is the basename of "xyz/messages.properties",
113     * "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc.
114     *
115     * @param file the file
116     * @param basenameSeparator the basename separator
117     * @return the extracted basename
118     */
119    private static String extractPropertyIdentifier(final File file,
120            final String basenameSeparator)
121    {
122        final String filePath = file.getPath();
123        final int dirNameEnd = filePath.lastIndexOf(File.separatorChar);
124        final int baseNameStart = dirNameEnd + 1;
125        final int underscoreIdx = filePath.indexOf(basenameSeparator,
126            baseNameStart);
127        final int dotIdx = filePath.indexOf('.', baseNameStart);
128        final int cutoffIdx = (underscoreIdx != -1) ? underscoreIdx : dotIdx;
129        return filePath.substring(0, cutoffIdx);
130    }
131
132   /**
133    * Sets the separator used to determine the basename of a property file.
134    * This defaults to "_"
135    *
136    * @param basenameSeparator the basename separator
137    */
138    public void setBasenameSeparator(String basenameSeparator)
139    {
140        this.basenameSeparator = basenameSeparator;
141    }
142
143    /**
144     * Arranges a set of property files by their prefix.
145     * The method returns a Map object. The filename prefixes
146     * work as keys each mapped to a set of files.
147     * @param propFiles the set of property files
148     * @param basenameSeparator the basename separator
149     * @return a Map object which holds the arranged property file sets
150     */
151    private static Map<String, Set<File>> arrangePropertyFiles(
152        List<File> propFiles, String basenameSeparator)
153    {
154        final Map<String, Set<File>> propFileMap = Maps.newHashMap();
155
156        for (final File f : propFiles) {
157            final String identifier = extractPropertyIdentifier(f,
158                basenameSeparator);
159
160            Set<File> fileSet = propFileMap.get(identifier);
161            if (fileSet == null) {
162                fileSet = Sets.newHashSet();
163                propFileMap.put(identifier, fileSet);
164            }
165            fileSet.add(f);
166        }
167        return propFileMap;
168    }
169
170    /**
171     * Loads the keys of the specified property file into a set.
172     * @param file the property file
173     * @return a Set object which holds the loaded keys
174     */
175    private Set<Object> loadKeys(File file)
176    {
177        final Set<Object> keys = Sets.newHashSet();
178        InputStream inStream = null;
179
180        try {
181            // Load file and properties.
182            inStream = new FileInputStream(file);
183            final Properties props = new Properties();
184            props.load(inStream);
185
186            // Gather the keys and put them into a set
187            final Enumeration<?> e = props.propertyNames();
188            while (e.hasMoreElements()) {
189                keys.add(e.nextElement());
190            }
191        }
192        catch (final IOException e) {
193            logIOException(e, file);
194        }
195        finally {
196            Utils.closeQuietly(inStream);
197        }
198        return keys;
199    }
200
201    /**
202     * helper method to log an io exception.
203     * @param ex the exception that occured
204     * @param file the file that could not be processed
205     */
206    private void logIOException(IOException ex, File file)
207    {
208        String[] args = null;
209        String key = "general.fileNotFound";
210        if (!(ex instanceof FileNotFoundException)) {
211            args = new String[] {ex.getMessage()};
212            key = "general.exception";
213        }
214        final LocalizedMessage message =
215            new LocalizedMessage(
216                0,
217                Defn.CHECKSTYLE_BUNDLE,
218                key,
219                args,
220                getId(),
221                this.getClass(), null);
222        final TreeSet<LocalizedMessage> messages = Sets.newTreeSet();
223        messages.add(message);
224        getMessageDispatcher().fireErrors(file.getPath(), messages);
225        Utils.getExceptionLogger().debug("IOException occured.", ex);
226    }
227
228
229    /**
230     * Compares the key sets of the given property files (arranged in a map)
231     * with the specified key set. All missing keys are reported.
232     * @param keys the set of keys to compare with
233     * @param fileMap a Map from property files to their key sets
234     */
235    private void compareKeySets(Set<Object> keys,
236            Map<File, Set<Object>> fileMap)
237    {
238        final Set<Entry<File, Set<Object>>> fls = fileMap.entrySet();
239
240        for (Entry<File, Set<Object>> entry : fls) {
241            final File currentFile = entry.getKey();
242            final MessageDispatcher dispatcher = getMessageDispatcher();
243            final String path = currentFile.getPath();
244            dispatcher.fireFileStarted(path);
245            final Set<Object> currentKeys = entry.getValue();
246
247            // Clone the keys so that they are not lost
248            final Set<Object> keysClone = Sets.newHashSet(keys);
249            keysClone.removeAll(currentKeys);
250
251            // Remaining elements in the key set are missing in the current file
252            if (!keysClone.isEmpty()) {
253                for (Object key : keysClone) {
254                    log(0, "translation.missingKey", key);
255                }
256            }
257            fireErrors(path);
258            dispatcher.fireFileFinished(path);
259        }
260    }
261
262
263    /**
264     * Tests whether the given property files (arranged by their prefixes
265     * in a Map) contain the proper keys.
266     *
267     * Each group of files must have the same keys. If this is not the case
268     * an error message is posted giving information which key misses in
269     * which file.
270     *
271     * @param propFiles the property files organized as Map
272     */
273    private void checkPropertyFileSets(Map<String, Set<File>> propFiles)
274    {
275        final Set<Entry<String, Set<File>>> entrySet = propFiles.entrySet();
276
277        for (Entry<String, Set<File>> entry : entrySet) {
278            final Set<File> files = entry.getValue();
279
280            if (files.size() >= 2) {
281                // build a map from files to the keys they contain
282                final Set<Object> keys = Sets.newHashSet();
283                final Map<File, Set<Object>> fileMap = Maps.newHashMap();
284
285                for (File file : files) {
286                    final Set<Object> fileKeys = loadKeys(file);
287                    keys.addAll(fileKeys);
288                    fileMap.put(file, fileKeys);
289                }
290
291                // check the map for consistency
292                compareKeySets(keys, fileMap);
293            }
294        }
295    }
296}