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    }