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 * @author Alexandra Bunge
056 * @author lkuehne
057 */
058public class TranslationCheck
059    extends AbstractFileSetCheck
060{
061    /** The property files to process. */
062    private final List<File> mPropertyFiles = Lists.newArrayList();
063
064    /**
065     * Creates a new <code>TranslationCheck</code> instance.
066     */
067    public TranslationCheck()
068    {
069        setFileExtensions(new String[]{"properties"});
070    }
071
072    @Override
073    public void beginProcessing(String aCharset)
074    {
075        super.beginProcessing(aCharset);
076        mPropertyFiles.clear();
077    }
078
079    @Override
080    protected void processFiltered(File aFile, List<String> aLines)
081    {
082        mPropertyFiles.add(aFile);
083    }
084
085    @Override
086    public void finishProcessing()
087    {
088        super.finishProcessing();
089        final Map<String, Set<File>> propFilesMap =
090            arrangePropertyFiles(mPropertyFiles);
091        checkPropertyFileSets(propFilesMap);
092    }
093
094    /**
095     * Gets the basename (the unique prefix) of a property file. For example
096     * "xyz/messages" is the basename of "xyz/messages.properties",
097     * "xyz/messages_de_AT.properties", "xyz/messages_en.properties", etc.
098     *
099     * @param aFile the file
100     * @return the extracted basename
101     */
102    private static String extractPropertyIdentifier(final File aFile)
103    {
104        final String filePath = aFile.getPath();
105        final int dirNameEnd = filePath.lastIndexOf(File.separatorChar);
106        final int baseNameStart = dirNameEnd + 1;
107        final int underscoreIdx = filePath.indexOf('_', baseNameStart);
108        final int dotIdx = filePath.indexOf('.', baseNameStart);
109        final int cutoffIdx = (underscoreIdx != -1) ? underscoreIdx : dotIdx;
110        return filePath.substring(0, cutoffIdx);
111    }
112
113    /**
114     * Arranges a set of property files by their prefix.
115     * The method returns a Map object. The filename prefixes
116     * work as keys each mapped to a set of files.
117     * @param aPropFiles the set of property files
118     * @return a Map object which holds the arranged property file sets
119     */
120    private static Map<String, Set<File>> arrangePropertyFiles(
121        List<File> aPropFiles)
122    {
123        final Map<String, Set<File>> propFileMap = Maps.newHashMap();
124
125        for (final File f : aPropFiles) {
126            final String identifier = extractPropertyIdentifier(f);
127
128            Set<File> fileSet = propFileMap.get(identifier);
129            if (fileSet == null) {
130                fileSet = Sets.newHashSet();
131                propFileMap.put(identifier, fileSet);
132            }
133            fileSet.add(f);
134        }
135        return propFileMap;
136    }
137
138    /**
139     * Loads the keys of the specified property file into a set.
140     * @param aFile the property file
141     * @return a Set object which holds the loaded keys
142     */
143    private Set<Object> loadKeys(File aFile)
144    {
145        final Set<Object> keys = Sets.newHashSet();
146        InputStream inStream = null;
147
148        try {
149            // Load file and properties.
150            inStream = new FileInputStream(aFile);
151            final Properties props = new Properties();
152            props.load(inStream);
153
154            // Gather the keys and put them into a set
155            final Enumeration<?> e = props.propertyNames();
156            while (e.hasMoreElements()) {
157                keys.add(e.nextElement());
158            }
159        }
160        catch (final IOException e) {
161            logIOException(e, aFile);
162        }
163        finally {
164            Utils.closeQuietly(inStream);
165        }
166        return keys;
167    }
168
169    /**
170     * helper method to log an io exception.
171     * @param aEx the exception that occured
172     * @param aFile the file that could not be processed
173     */
174    private void logIOException(IOException aEx, File aFile)
175    {
176        String[] args = null;
177        String key = "general.fileNotFound";
178        if (!(aEx instanceof FileNotFoundException)) {
179            args = new String[] {aEx.getMessage()};
180            key = "general.exception";
181        }
182        final LocalizedMessage message =
183            new LocalizedMessage(
184                0,
185                Defn.CHECKSTYLE_BUNDLE,
186                key,
187                args,
188                getId(),
189                this.getClass(), null);
190        final TreeSet<LocalizedMessage> messages = Sets.newTreeSet();
191        messages.add(message);
192        getMessageDispatcher().fireErrors(aFile.getPath(), messages);
193        Utils.getExceptionLogger().debug("IOException occured.", aEx);
194    }
195
196
197    /**
198     * Compares the key sets of the given property files (arranged in a map)
199     * with the specified key set. All missing keys are reported.
200     * @param aKeys the set of keys to compare with
201     * @param aFileMap a Map from property files to their key sets
202     */
203    private void compareKeySets(Set<Object> aKeys,
204            Map<File, Set<Object>> aFileMap)
205    {
206        final Set<Entry<File, Set<Object>>> fls = aFileMap.entrySet();
207
208        for (Entry<File, Set<Object>> entry : fls) {
209            final File currentFile = entry.getKey();
210            final MessageDispatcher dispatcher = getMessageDispatcher();
211            final String path = currentFile.getPath();
212            dispatcher.fireFileStarted(path);
213            final Set<Object> currentKeys = entry.getValue();
214
215            // Clone the keys so that they are not lost
216            final Set<Object> keysClone = Sets.newHashSet(aKeys);
217            keysClone.removeAll(currentKeys);
218
219            // Remaining elements in the key set are missing in the current file
220            if (!keysClone.isEmpty()) {
221                for (Object key : keysClone) {
222                    log(0, "translation.missingKey", key);
223                }
224            }
225            fireErrors(path);
226            dispatcher.fireFileFinished(path);
227        }
228    }
229
230
231    /**
232     * Tests whether the given property files (arranged by their prefixes
233     * in a Map) contain the proper keys.
234     *
235     * Each group of files must have the same keys. If this is not the case
236     * an error message is posted giving information which key misses in
237     * which file.
238     *
239     * @param aPropFiles the property files organized as Map
240     */
241    private void checkPropertyFileSets(Map<String, Set<File>> aPropFiles)
242    {
243        final Set<Entry<String, Set<File>>> entrySet = aPropFiles.entrySet();
244
245        for (Entry<String, Set<File>> entry : entrySet) {
246            final Set<File> files = entry.getValue();
247
248            if (files.size() >= 2) {
249                // build a map from files to the keys they contain
250                final Set<Object> keys = Sets.newHashSet();
251                final Map<File, Set<Object>> fileMap = Maps.newHashMap();
252
253                for (File file : files) {
254                    final Set<Object> fileKeys = loadKeys(file);
255                    keys.addAll(fileKeys);
256                    fileMap.put(file, fileKeys);
257                }
258
259                // check the map for consistency
260                compareKeySets(keys, fileMap);
261            }
262        }
263    }
264}