001    package hirondelle.web4jtools.metrics.base;
002    
003    import hirondelle.web4j.action.ActionTemplateShowAndApply;
004    import hirondelle.web4j.action.ResponsePage;
005    import hirondelle.web4j.model.AppException;
006    import hirondelle.web4j.model.ModelCtorException;
007    import hirondelle.web4j.model.ModelFromRequest;
008    import hirondelle.web4j.request.RequestParameter;
009    import hirondelle.web4j.request.RequestParser;
010    import hirondelle.web4j.util.Stopwatch;
011    import hirondelle.web4j.util.Util;
012    
013    import java.io.File;
014    import java.util.ArrayList;
015    import java.util.Arrays;
016    import java.util.Collections;
017    import java.util.Comparator;
018    import java.util.LinkedHashMap;
019    import java.util.List;
020    import java.util.Map;
021    import java.util.Set;
022    import java.util.logging.Logger;
023    
024    /**
025    * Override base project information configured in <tt>web.xml</tt>, and show high level metrics to user.
026    * 
027    * <P>Scans the local file system under the given base directory.
028    * Various statistics and listings are extracted.
029    */
030    public final class BaseInfoAction extends ActionTemplateShowAndApply {
031    
032      public BaseInfoAction(RequestParser aRequestParser) {
033        super(FORWARD, REDIRECT, aRequestParser);
034      }
035      
036      public static final RequestParameter BASE_DIRECTORY = RequestParameter.withLengthCheck("BaseDirectory");
037      public static final RequestParameter PROJECT_NAME = RequestParameter.withLengthCheck("ProjectName");
038      public static final RequestParameter BASE_URI_FOR_FETCHING_IMAGES = RequestParameter.withLengthCheck("BaseURIForFetchingImages");
039      
040      /** Show a form reflecting current settings.  */
041      @Override protected void show() throws AppException {
042         if( listFileInfo() == null ) {
043           addMessage("Please 'Scan Source Code' to analyze your app's source code.");
044         }
045         addToRequest(ITEM_FOR_EDIT, fetchBaseInfo());
046      }
047    
048      /** Validate changes to current settings.  */
049      @Override protected void validateUserInput() throws AppException {
050        ModelFromRequest builder = new ModelFromRequest(getRequestParser());
051        try {
052          fBaseInfo = builder.build(BaseInfo.class, PROJECT_NAME, BASE_DIRECTORY, BASE_URI_FOR_FETCHING_IMAGES);
053        }
054        catch (ModelCtorException ex) {
055          addError(ex);
056        }
057      }
058      
059      /**
060      * Save the changes to current settings, and recursively scan the files under  
061      * the base directory.
062      * 
063      * <P>Creates an internal data structure to represent your application's source code.
064      */
065      @Override protected void apply() throws AppException {
066        fLogger.fine("Saving current settings.");
067        BaseInfoDAO dao  = new BaseInfoDAO(getRequestParser().getRequest());
068        dao.save(fBaseInfo);
069        
070        fLogger.fine("Scanning source files...");
071        Stopwatch stopwatch = new Stopwatch();
072        stopwatch.start();
073        
074        File baseDirectory = new File(fBaseInfo.getBaseDirectory().getRawString());
075        scan(baseDirectory);
076        
077        addToSession(FILE_INFO_MAP_KEY, fAllFiles);
078        addToSession(FILE_INFO_LIST_KEY, asSortedList(fAllFiles));
079        
080        addToSession("numFiles", fNumFiles);
081        addToSession("totalSize", fTotalSize);
082        addToSession("numSourceFiles", fNumSourceFiles);
083        addToSession("numImageFiles", fNumImageFiles);
084        addToSession("numMarkupFiles", fNumMarkupFiles);
085        addToSession("numJavaFiles", fNumJavaFiles);
086        
087        addToSession("sourceFileExtensions", FileInfo.getSourceFileExtensions());
088        addToSession("imageFileExtensions", FileInfo.getImageFileExtensions());
089        addToSession("markupFileExtensions", FileInfo.getMarkupFileExtensions());
090        addToSession("ignorableFiles", FileInfo.getIgnorableFiles());
091        
092        stopwatch.stop();
093        fLogger.fine("Finished scanning source files. Time needed for scan : " + stopwatch + " msec.'");
094      }
095      
096      /**
097      * Key for <tt>Map</tt> stored in session scope. This data structure is created/replaced when 
098      * the user presses the 'Scan Source Code' button.
099      * 
100      * <P>This Map style retains directory information.
101      */
102      public static final String FILE_INFO_MAP_KEY = "fileInfoMap";
103      
104      /**
105      * Key for <tt>List</tt> stored in session scope. This data structure is created/replaced when 
106      * the user presses the 'Scan Source Code' button.
107      * 
108      * <P>This List style removes directory information, and is useful for file listings. 
109      * When the user changes the sorting, the re-sorted list should overwrite the object
110      * in session scope. That way, the latest sort used by the user will be retained.
111      */
112      public static final String FILE_INFO_LIST_KEY = "fileInfoList";
113    
114      // PRIVATE //
115      private BaseInfo fBaseInfo;
116      private Map<File, List<FileInfo>> fAllFiles = new LinkedHashMap<File, List<FileInfo>>();
117      private long fNumFiles;
118      private long fTotalSize;
119      private long fNumSourceFiles;
120      private long fNumJavaFiles;
121      private long fNumImageFiles;
122      private long fNumMarkupFiles;
123      
124      private static final ResponsePage FORWARD = new ResponsePage("Source Code Base Info", "view.jsp", BaseInfoAction.class);
125      private static final ResponsePage REDIRECT = new ResponsePage("BaseInfoAction.do?Operation=Show");
126      private static final Logger fLogger = Util.getLogger(BaseInfoAction.class);
127    
128      private Map<File, List<FileInfo>> listFileInfo() {
129        Map<File, List<FileInfo>> result = (Map<File, List<FileInfo>>)getFromSession(FILE_INFO_MAP_KEY);
130        return result;
131      }
132      
133      private BaseInfo fetchBaseInfo(){
134        BaseInfoDAO dao = new BaseInfoDAO(getRequestParser().getRequest());
135        return dao.fetch();
136      }
137    
138      /**
139      * Recursively scan the base dir. When each file is encountered, it is examined.
140      * A FileInfo thing is created, and it is added to allFiles.
141      * Along the way, the various counters are incremented, in order to avoid 
142      * possible performance problems with large numbers of files.
143      */
144      private void scan(File aDirectory) {
145        assert (aDirectory.isDirectory());
146        List<FileInfo> filesOnly = new ArrayList<FileInfo>();
147        List<File> allInDirectory = Arrays.asList(aDirectory.listFiles());
148        for(File file : allInDirectory){
149          if(! file.isDirectory() ) {
150            addFile(filesOnly, file);
151          }
152        }
153        fAllFiles.put(aDirectory, filesOnly);
154        for( File dir : allInDirectory ) {
155          if ( dir.isDirectory() ) {
156            scan(dir); //recursive call!!
157          }
158        }
159      }
160    
161      private void addFile(List<FileInfo> aFilesOnly, File aFile) {
162        if ( FileInfo.isIgnorable(aFile)) {
163          //do nothing
164        }
165        else{
166          FileInfo fileInfo = new FileInfo(aFile);
167          aFilesOnly.add(fileInfo);
168          fNumFiles = fNumFiles + 1; 
169          fTotalSize = fTotalSize + aFile.length();
170          if ( fileInfo.isSourceFile() ) {
171            ++fNumSourceFiles;
172          }
173          if ( fileInfo.isJavaSourceFile() ) {
174            ++fNumJavaFiles;
175          }
176          if ( fileInfo.isImageFile() ) {
177            ++fNumImageFiles;
178          }
179          if ( fileInfo.isMarkupFile() ) {
180            ++fNumMarkupFiles;
181          }
182        }
183      }
184      
185      private List<FileInfo> asSortedList(Map<File, List<FileInfo>> aAllFiles){
186        List<FileInfo> result = new ArrayList<FileInfo>();
187        Set<File> dirs =  aAllFiles.keySet();
188        for ( File dir : dirs ) {
189          List<FileInfo> files = aAllFiles.get(dir);
190          result.addAll(files);
191        }
192        Collections.sort(result, byTotalSize());
193        return result;
194      }
195      
196      private Comparator<FileInfo> byTotalSize() {
197        return new Comparator<FileInfo>() {
198          public int compare(FileInfo aThis, FileInfo aThat) {
199            return aThis.getSize().compareTo(aThat.getSize());
200          }
201        };
202      }
203    }