1
2
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
140
141 private void processResults(Object data) {
142
143 int threshold = getProperty(THRESHOLD_DESCRIPTOR);
144
145 for (Map.Entry<String, List<ASTLiteral>> entry : literals.entrySet()) {
146 List<ASTLiteral> occurrences = entry.getValue();
147 if (occurrences.size() >= threshold) {
148 Object[] args = new Object[] {
149 entry.getKey(),
150 Integer.valueOf(occurrences.size()),
151 Integer.valueOf(occurrences.get(0).getBeginLine())
152 };
153 addViolation(data, occurrences.get(0), args);
154 }
155 }
156 }
157
158 @Override
159 public Object visit(ASTLiteral node, Object data) {
160 if (!node.isStringLiteral()) {
161 return data;
162 }
163 String image = node.getImage();
164
165
166 if (image.length() < minLength) {
167 return data;
168 }
169
170
171 if (exceptions.contains(image.substring(1, image.length() - 1))) {
172 return data;
173 }
174
175
176 if (getProperty(SKIP_ANNOTATIONS_DESCRIPTOR) && node.getFirstParentOfType(ASTAnnotation.class) != null) {
177 return data;
178 }
179
180 if (literals.containsKey(image)) {
181 List<ASTLiteral> occurrences = literals.get(image);
182 occurrences.add(node);
183 } else {
184 List<ASTLiteral> occurrences = new ArrayList<ASTLiteral>();
185 occurrences.add(node);
186 literals.put(image, occurrences);
187 }
188
189 return data;
190 }
191
192 private static String checkFile(File file) {
193
194 if (!file.exists()) return "File '" + file.getName() + "' does not exist";
195 if (!file.canRead()) return "File '" + file.getName() + "' cannot be read";
196 if (file.length() == 0) return "File '" + file.getName() + "' is empty";
197
198 return null;
199 }
200
201
202
203
204 @Override
205 public String dysfunctionReason() {
206
207 File file = getProperty(EXCEPTION_FILE_DESCRIPTOR);
208 if (file != null) {
209 String issue = checkFile(file);
210 if (issue != null) return issue;
211
212 String ignores = getProperty(EXCEPTION_LIST_DESCRIPTOR);
213 if (StringUtil.isNotEmpty(ignores)) {
214 return "Cannot reference external file AND local values";
215 }
216 }
217
218 return null;
219 }
220 }