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 
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         // just catching strings of 'minLength' chars or more (including the enclosing quotes)
166         if (image.length() < minLength) {
167             return data;
168         }
169 
170         // skip any exceptions
171         if (exceptions.contains(image.substring(1, image.length() - 1))) {
172             return data;
173         }
174 
175         // Skip literals in annotations
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 	  * @see PropertySource#dysfunctionReason()
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 }