View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.strings;
5   
6   import java.io.BufferedReader;
7   import java.io.File;
8   import java.io.FileNotFoundException;
9   import java.io.FileReader;
10  import java.io.IOException;
11  import java.io.LineNumberReader;
12  import java.util.ArrayList;
13  import java.util.HashMap;
14  import java.util.HashSet;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Set;
18  
19  import net.sourceforge.pmd.PropertySource;
20  import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
21  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
22  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
23  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
24  import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
25  import net.sourceforge.pmd.lang.rule.properties.CharacterProperty;
26  import net.sourceforge.pmd.lang.rule.properties.FileProperty;
27  import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
28  import net.sourceforge.pmd.lang.rule.properties.StringProperty;
29  import net.sourceforge.pmd.util.StringUtil;
30  
31  import org.apache.commons.io.IOUtils;
32  
33  public class AvoidDuplicateLiteralsRule extends AbstractJavaRule {
34  
35      public static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("maxDuplicateLiterals",
36              "Max duplicate literals", 1, 20, 4, 1.0f);
37  
38      public static final IntegerProperty MINIMUM_LENGTH_DESCRIPTOR = new IntegerProperty("minimumLength",
39              "Minimum string length to check", 1, Integer.MAX_VALUE, 3, 1.5f);
40  
41      public static final BooleanProperty SKIP_ANNOTATIONS_DESCRIPTOR = new BooleanProperty("skipAnnotations",
42              "Skip literals within annotations", false, 2.0f);
43  
44      public static final StringProperty EXCEPTION_LIST_DESCRIPTOR = new StringProperty("exceptionList",
45              "Strings to ignore", null, 3.0f);
46  
47      public static final CharacterProperty SEPARATOR_DESCRIPTOR = new CharacterProperty("separator",
48              "Ignore list separator", ',', 4.0f);
49  
50      public static final FileProperty EXCEPTION_FILE_DESCRIPTOR = new FileProperty("exceptionfile",
51              "File containing strings to skip (one string per line), only used if ignore list is not set", null, 5.0f);
52  
53      public static class ExceptionParser {
54  
55          private static final char ESCAPE_CHAR = '\\';
56          private char delimiter;
57  
58          public ExceptionParser(char delimiter) {
59              this.delimiter = delimiter;
60          }
61  
62          public Set<String> parse(String s) {
63              Set<String> result = new HashSet<String>();
64              StringBuilder currentToken = new StringBuilder();
65              boolean inEscapeMode = false;
66              for (int i = 0; i < s.length(); i++) {
67                  if (inEscapeMode) {
68                      inEscapeMode = false;
69                      currentToken.append(s.charAt(i));
70                      continue;
71                  }
72                  if (s.charAt(i) == ESCAPE_CHAR) {
73                      inEscapeMode = true;
74                      continue;
75                  }
76                  if (s.charAt(i) == delimiter) {
77                      result.add(currentToken.toString());
78                      currentToken = new StringBuilder();
79                  } else {
80                      currentToken.append(s.charAt(i));
81                  }
82              }
83              if (currentToken.length() > 0) {
84                  result.add(currentToken.toString());
85              }
86              return result;
87          }
88      }
89  
90      private Map<String, List<ASTLiteral>> literals = new HashMap<String, List<ASTLiteral>>();
91      private Set<String> exceptions = new HashSet<String>();
92      private int minLength;
93  
94      public AvoidDuplicateLiteralsRule() {
95          definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
96          definePropertyDescriptor(MINIMUM_LENGTH_DESCRIPTOR);
97          definePropertyDescriptor(SKIP_ANNOTATIONS_DESCRIPTOR);
98          definePropertyDescriptor(EXCEPTION_LIST_DESCRIPTOR);
99          definePropertyDescriptor(SEPARATOR_DESCRIPTOR);
100         definePropertyDescriptor(EXCEPTION_FILE_DESCRIPTOR);
101     }
102 
103     private LineNumberReader getLineReader() throws FileNotFoundException {
104         return new LineNumberReader(new BufferedReader(new FileReader(getProperty(EXCEPTION_FILE_DESCRIPTOR))));
105     }
106 
107     @Override
108     public Object visit(ASTCompilationUnit node, Object data) {
109         literals.clear();
110 
111         if (getProperty(EXCEPTION_LIST_DESCRIPTOR) != null) {
112             ExceptionParser p = new ExceptionParser(getProperty(SEPARATOR_DESCRIPTOR));
113             exceptions = p.parse(getProperty(EXCEPTION_LIST_DESCRIPTOR));
114         } else if (getProperty(EXCEPTION_FILE_DESCRIPTOR) != null) {
115             exceptions = new HashSet<String>();
116             LineNumberReader reader = null;
117             try {
118                 reader = getLineReader();
119                 String line;
120                 while ((line = reader.readLine()) != null) {
121                     exceptions.add(line);
122                 }
123             } catch (IOException ioe) {
124                 ioe.printStackTrace();
125             } finally {
126                 IOUtils.closeQuietly(reader);
127             }
128         }
129 
130         minLength = 2 + getProperty(MINIMUM_LENGTH_DESCRIPTOR);
131 
132         super.visit(node, data);
133 
134         processResults(data);
135 
136         return data;
137     }
138 
139     private void processResults(Object data) {
140 
141         int threshold = getProperty(THRESHOLD_DESCRIPTOR);
142 
143         for (Map.Entry<String, List<ASTLiteral>> entry : literals.entrySet()) {
144             List<ASTLiteral> occurrences = entry.getValue();
145             if (occurrences.size() >= threshold) {
146                 Object[] args = new Object[] { entry.getKey(), Integer.valueOf(occurrences.size()),
147                         Integer.valueOf(occurrences.get(0).getBeginLine()) };
148                 addViolation(data, occurrences.get(0), args);
149             }
150         }
151     }
152 
153     @Override
154     public Object visit(ASTLiteral node, Object data) {
155         if (!node.isStringLiteral()) {
156             return data;
157         }
158         String image = node.getImage();
159 
160         // just catching strings of 'minLength' chars or more (including the
161         // enclosing quotes)
162         if (image.length() < minLength) {
163             return data;
164         }
165 
166         // skip any exceptions
167         if (exceptions.contains(image.substring(1, image.length() - 1))) {
168             return data;
169         }
170 
171         // Skip literals in annotations
172         if (getProperty(SKIP_ANNOTATIONS_DESCRIPTOR) && node.getFirstParentOfType(ASTAnnotation.class) != null) {
173             return data;
174         }
175 
176         if (literals.containsKey(image)) {
177             List<ASTLiteral> occurrences = literals.get(image);
178             occurrences.add(node);
179         } else {
180             List<ASTLiteral> occurrences = new ArrayList<ASTLiteral>();
181             occurrences.add(node);
182             literals.put(image, occurrences);
183         }
184 
185         return data;
186     }
187 
188     private static String checkFile(File file) {
189 
190         if (!file.exists()) {
191             return "File '" + file.getName() + "' does not exist";
192         }
193         if (!file.canRead()) {
194             return "File '" + file.getName() + "' cannot be read";
195         }
196         if (file.length() == 0) {
197             return "File '" + file.getName() + "' is empty";
198         }
199 
200         return null;
201     }
202 
203     /**
204      * @see PropertySource#dysfunctionReason()
205      */
206     @Override
207     public String dysfunctionReason() {
208 
209         File file = getProperty(EXCEPTION_FILE_DESCRIPTOR);
210         if (file != null) {
211             String issue = checkFile(file);
212             if (issue != null) {
213                 return issue;
214             }
215 
216             String ignores = getProperty(EXCEPTION_LIST_DESCRIPTOR);
217             if (StringUtil.isNotEmpty(ignores)) {
218                 return "Cannot reference external file AND local values";
219             }
220         }
221 
222         return null;
223     }
224 }