001 package hirondelle.web4jtools.metrics.base; 002 003 import static hirondelle.web4j.util.Consts.NEW_LINE; 004 import static hirondelle.web4j.util.Consts.NOT_FOUND; 005 import hirondelle.web4j.model.ModelUtil; 006 import hirondelle.web4j.security.SafeText; 007 import hirondelle.web4j.util.Util; 008 import hirondelle.web4jtools.util.Ensure; 009 010 import java.io.File; 011 import java.io.FileInputStream; 012 import java.io.FileNotFoundException; 013 import java.io.IOException; 014 import java.text.CharacterIterator; 015 import java.text.StringCharacterIterator; 016 import java.util.ArrayList; 017 import java.util.Date; 018 import java.util.Iterator; 019 import java.util.List; 020 import java.util.Scanner; 021 import java.util.StringTokenizer; 022 import java.util.jar.Attributes; 023 import java.util.jar.JarInputStream; 024 import java.util.jar.Manifest; 025 import java.util.logging.Logger; 026 027 import javax.servlet.ServletConfig; 028 029 /** Model Object for basic File Information. */ 030 public final class FileInfo { 031 032 /** 033 * Read in config from web.xml. 034 * 035 * <P>Called only during startup. 036 */ 037 public static void readConfig(ServletConfig aConfig){ 038 String sourceFileExtensions = fetchConfigFor(SOURCE_FILE_EXTENSIONS, aConfig); 039 setFileExtensions(sourceFileExtensions, fSourceFileExtensions); 040 041 String imageFileExtensions = fetchConfigFor(IMAGE_FILE_EXTENSIONS, aConfig); 042 setFileExtensions(imageFileExtensions, fImageFileExtensions); 043 044 String markupFileExtensions = fetchConfigFor(MARKUP_FILE_EXTENSIONS, aConfig); 045 setFileExtensions(markupFileExtensions, fMarkupFileExtensions); 046 047 String ignorableFiles = fetchConfigFor(IGNORABLE_FILES, aConfig); 048 setIgnorableFiles(ignorableFiles); 049 050 fUNIT_TEST_FINGERPRINT = fetchConfigFor(UNIT_TEST_FINGERPRINT, aConfig); 051 } 052 053 /** Returned the list of ignorable files, as configured in <tt>web.xml</tt>. */ 054 static public String getIgnorableFiles(){ 055 return formattedList(fIgnorableFiles); 056 } 057 058 /** Return the list of extensions for source files, as configured in <tt>web.xml</tt>. */ 059 static public String getSourceFileExtensions() { 060 return formattedList(fSourceFileExtensions); 061 } 062 063 /** Return the list of extensions for image files, as configured in <tt>web.xml</tt>. */ 064 static public String getImageFileExtensions() { 065 return formattedList(fImageFileExtensions); 066 } 067 068 /** Return the list of extensions for markup files, as configured in <tt>web.xml</tt>. */ 069 static public String getMarkupFileExtensions() { 070 return formattedList(fMarkupFileExtensions); 071 } 072 073 /** 074 * Determine if a file is an 'ignorable' file (such as a <tt>.class</tt> file, for instance.) 075 * See {@link #getIgnorableFiles()}. 076 */ 077 public static boolean isIgnorable(File aFile) { 078 boolean result = false; 079 for(String ignorable : fIgnorableFiles){ 080 if ( aFile.getAbsolutePath().contains(ignorable) ) { 081 result = true; 082 break; 083 } 084 } 085 return result; 086 } 087 088 /** Full constructor. */ 089 public FileInfo(File aFile) { 090 fFile = aFile; 091 if ( fFile.isDirectory() ) { 092 throw new RuntimeException("File must be a file, not a directory."); 093 } 094 if ( isSourceFile() ) { 095 examineLines(); 096 } 097 if ( isJarFile() ) { 098 examineJarSpecs(); 099 } 100 } 101 102 /** Return the absolute path name of the file. */ 103 public SafeText getName() { 104 return new SafeText(fFile.getAbsolutePath()); 105 } 106 107 /** Return the simple name of the file, without path information. */ 108 public SafeText getSimpleName() { 109 return new SafeText(fFile.getName()); 110 } 111 112 /** Return the size of the file in bytes. */ 113 public Long getSize() { 114 return fFile.length(); 115 } 116 117 /** Return the date the file was last modified. */ 118 public Date getLastModified() { 119 return new Date(fFile.lastModified()); 120 } 121 122 /** Return the file extension. */ 123 public SafeText getExtension() { 124 SafeText result = new SafeText(""); //empty by default 125 String name = getName().getRawString(); 126 int lastPeriod = name.lastIndexOf("."); 127 if ( lastPeriod != NOT_FOUND ) { 128 result = new SafeText(name.substring(lastPeriod)); 129 } 130 return result; 131 } 132 133 /** 134 * Return true only if the extension matches one of the source file extensions configured in <tt>web.xml</tt>. 135 * See {@link #getSourceFileExtensions()}. 136 */ 137 public Boolean isSourceFile() { 138 boolean result = false; 139 String extension = getExtension().getRawString(); 140 for (String sourceFileExtension : fSourceFileExtensions) { 141 if ( extension.equalsIgnoreCase(sourceFileExtension) ) { 142 result = true; 143 break; 144 } 145 } 146 return result; 147 } 148 149 /** 150 * Return true only if the extension matches one of the image file extensions configured in <tt>web.xml</tt>. 151 * See {@link #getImageFileExtensions()}. 152 */ 153 public Boolean isImageFile(){ 154 boolean result = false; 155 String extension = getExtension().getRawString(); 156 for (String imageFileExtension : fImageFileExtensions) { 157 if ( extension.equalsIgnoreCase(imageFileExtension) ) { 158 result = true; 159 break; 160 } 161 } 162 return result; 163 } 164 165 /** 166 * Return true only if the extension matches one of the markup file extensions configured in <tt>web.xml</tt>. 167 * See {@link #getMarkupFileExtensions()}. 168 */ 169 public Boolean isMarkupFile(){ 170 boolean result = false; 171 String extension = getExtension().getRawString(); 172 for (String markupFileExtension : fMarkupFileExtensions) { 173 if ( extension.equalsIgnoreCase(markupFileExtension) ) { 174 result = true; 175 break; 176 } 177 } 178 return result; 179 } 180 181 /** Return true only if the file extension is '.java' */ 182 public Boolean isJavaSourceFile() { 183 return getExtension().getRawString().equalsIgnoreCase(".java"); 184 } 185 186 /** Return true only if the file extension is '.class' */ 187 public Boolean isJavaClassFile() { 188 return getExtension().getRawString().equalsIgnoreCase(".class"); 189 } 190 191 /** Return true only if the file extension is '.jar' */ 192 public Boolean isJarFile(){ 193 return getExtension().getRawString().equalsIgnoreCase(".jar"); 194 } 195 196 /** Return the number of lines (source files only) */ 197 public Integer getNumLines() { 198 return fNumLines; 199 } 200 201 /** Return the number of comments (java files only) */ 202 public Integer getNumCommentLines(){ 203 return fNumCommentLines; 204 } 205 206 /** Return the percentage of comments as part of total lines (java files only) */ 207 public Integer getPercentComments(){ 208 Integer result = 0; 209 if( fNumLines > 0 ) { 210 result = (100*fNumCommentLines)/fNumLines; 211 } 212 return result; 213 } 214 215 /** Return true only if the file contains idenifier text configured in <tt>web.xml</tt> (java files only). */ 216 public Boolean isUnitTest(){ 217 return fHasUnitTestFingerprint; 218 } 219 220 /** Return the name and version of a .jar file's specification (.jar files only). */ 221 public String getSpecification(){ 222 return fSpecification; 223 } 224 225 /** Return the number of tab characters (source files only). */ 226 public Integer getNumTabs() { 227 return fNumTabs; 228 } 229 230 /** Return the contents of the file as a single String (source files only). */ 231 public String getContent(){ 232 if( ! isSourceFile() ) { 233 throw new IllegalArgumentException("Can return content only for source files. The extension of this file is not configured as being a source code file : " + getName()); 234 } 235 StringBuilder result = new StringBuilder(); 236 try { 237 Scanner scanner = new Scanner(fFile); 238 while ( scanner.hasNextLine() ) { 239 result.append(scanner.nextLine() + NEW_LINE); 240 } 241 scanner.close(); 242 } 243 catch(FileNotFoundException ex){ 244 fLogger.severe("Cannot open file named " + fFile.getName()); 245 } 246 return result.toString(); 247 } 248 249 @Override public String toString(){ 250 //don't use ModelUtil, since can have large content. 251 return fFile.getAbsolutePath(); 252 } 253 254 @Override public boolean equals(Object aThat){ 255 Boolean result = ModelUtil.quickEquals(this, aThat); 256 if ( result == null ) { 257 FileInfo that = (FileInfo)aThat; 258 result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields()); 259 } 260 return result; 261 } 262 263 @Override public int hashCode(){ 264 if ( fHashCode == 0 ) { 265 fHashCode = ModelUtil.hashCodeFor(getSignificantFields()); 266 } 267 return fHashCode; 268 } 269 270 // PRIVATE // 271 private final File fFile; 272 private Integer fNumLines = 0; 273 private Integer fNumCommentLines = 0; 274 private Integer fNumTabs = 0; 275 private Boolean fHasUnitTestFingerprint = false; 276 private String fSpecification; 277 private int fHashCode; 278 279 private static final String SOURCE_FILE_EXTENSIONS = "SourceFileExtensions"; 280 private static List<String> fSourceFileExtensions = new ArrayList<String>(); 281 282 private static final String IMAGE_FILE_EXTENSIONS = "ImageFileExtensions"; 283 private static List<String> fImageFileExtensions = new ArrayList<String>(); 284 285 private static final String MARKUP_FILE_EXTENSIONS = "MarkupFileExtensions"; 286 private static List<String> fMarkupFileExtensions = new ArrayList<String>(); 287 288 private static final String IGNORABLE_FILES = "IgnorableFiles"; 289 private static List<String> fIgnorableFiles = new ArrayList<String>(); 290 291 private static final String UNIT_TEST_FINGERPRINT = "UnitTestFingerprint"; 292 private static String fUNIT_TEST_FINGERPRINT; 293 294 private static final String SPECIFICATION_TITLE = "Specification-Title"; 295 private static final String SPECIFICATION_VERSION = "Specification-Version"; 296 297 private static final Logger fLogger = Util.getLogger(FileInfo.class); 298 299 private Object[] getSignificantFields(){ 300 return new Object[] {fFile}; 301 } 302 303 private static String fetchConfigFor(String aSetting, ServletConfig aConfig){ 304 String result = aConfig.getInitParameter(aSetting); 305 Ensure.isPresentInWebXml(aSetting, result); 306 fLogger.config("Config setting for " + aSetting + ": " + result); 307 return result; 308 } 309 310 private static void setFileExtensions(String aRawConfig, List<String> aList){ 311 StringTokenizer parser = new StringTokenizer(aRawConfig, ","); 312 while ( parser.hasMoreTokens() ) { 313 String extension = parser.nextToken().trim(); 314 if( extension.startsWith(".")) { 315 aList.add(extension); 316 } 317 else { 318 fLogger.severe("File extension does not start with a '.' : " + Util.quote(extension)); 319 } 320 } 321 } 322 323 private static void setIgnorableFiles(String aRawConfig){ 324 StringTokenizer parser = new StringTokenizer(aRawConfig, ","); 325 while ( parser.hasMoreTokens() ) { 326 String ignorable = parser.nextToken().trim(); 327 if( Util.textHasContent(ignorable) ) { 328 fIgnorableFiles.add(ignorable); 329 } 330 else { 331 fLogger.severe("IgnorableFiles setting in web.xml not in expected form: " + Util.quote(aRawConfig)); 332 } 333 } 334 } 335 336 private void examineLines(){ 337 try { 338 Scanner scanner = new Scanner(fFile); 339 while ( scanner.hasNextLine() ) { 340 String line = scanner.nextLine(); 341 ++fNumLines; 342 fNumTabs = fNumTabs + numTabsIn(line); 343 if ( isJavaSourceFile() ) { 344 if ( isJavaComment(line) ) { 345 ++fNumCommentLines; 346 } 347 if( hasUnitTestFingerprint(line) ){ 348 fHasUnitTestFingerprint = true; 349 } 350 } 351 } 352 scanner.close(); 353 } 354 catch(FileNotFoundException ex){ 355 fLogger.severe("Cannot open file named " + fFile.getName()); 356 } 357 } 358 359 private boolean isJavaComment(String aLine){ 360 /* 361 This is a block comment which would NOT be picked up by this method. 362 This kind of comment is rare in most java code, and is left out of 363 this implementation. If you wish to include this kind of comment, you 364 will need to amend this implementation. 365 */ 366 return aLine.trim().startsWith("/") || aLine.trim().startsWith("*"); 367 } 368 369 private boolean hasUnitTestFingerprint(String aLine){ 370 return aLine.contains(fUNIT_TEST_FINGERPRINT); 371 } 372 373 private static String formattedList(List<String> aList){ 374 StringBuilder result = new StringBuilder(); 375 for (String item : aList){ 376 result.append(Util.quote(item) + " "); 377 } 378 return result.toString(); 379 } 380 381 private void examineJarSpecs(){ 382 assert( isJarFile() ); 383 JarInputStream jarStream = null; 384 try { 385 jarStream = new JarInputStream(new FileInputStream(fFile)); 386 } 387 catch(IOException ex){ 388 fLogger.severe("Cannot open jar file, to fetch Specification-Version from the jar Manifest."); 389 } 390 fSpecification = fetchSpecNamesAndVersions(jarStream); 391 } 392 393 /** 394 * A Jar can have more than one entry for spec name and version. 395 * For example, the servlet jar implements both servlet spec and jsp spec. 396 * 397 * <P>Search for all attributes named Specification-Title and Specification-Version, 398 * and simply return them in the order they appear. 399 * 400 * <P>If none of these appear, simply return an empty String. 401 */ 402 private String fetchSpecNamesAndVersions(JarInputStream aJarStream) { 403 StringBuilder result = new StringBuilder(); 404 Manifest manifest = aJarStream.getManifest(); 405 if ( manifest != null ){ 406 Attributes attrs = (Attributes)manifest.getMainAttributes(); 407 for (Iterator iter = attrs.keySet().iterator(); iter.hasNext(); ) { 408 Attributes.Name attrName = (Attributes.Name)iter.next(); 409 addSpecAttrIfPresent(SPECIFICATION_TITLE, result, attrs, attrName); 410 addSpecAttrIfPresent(SPECIFICATION_VERSION, result, attrs, attrName); 411 } 412 fLogger.fine("Specification-Version: " + result); 413 } 414 else { 415 fLogger.fine("No manifest."); 416 } 417 return result.toString(); 418 } 419 420 private void addSpecAttrIfPresent(String aName, StringBuilder aResult, Attributes aAttrs, Attributes.Name aAttrName) { 421 if ( aName.equalsIgnoreCase(aAttrName.toString()) ) { 422 if ( Util.textHasContent(aAttrs.getValue(aAttrName)) ){ 423 aResult.append(aAttrs.getValue(aAttrName) + " "); 424 } 425 } 426 } 427 428 private int numTabsIn(String aLine){ 429 int result = 0; 430 StringCharacterIterator iterator = new StringCharacterIterator(aLine); 431 char character = iterator.current(); 432 while (character != CharacterIterator.DONE ){ 433 if( character == '\t') { 434 result = result + 1; 435 } 436 character = iterator.next(); 437 } 438 return result; 439 } 440 }