]> source.dussan.org Git - archiva.git/blob
981ad900e53759bc2487f530e59a5634377b849b
[archiva.git] /
1 package org.apache.maven.archiva.web.action.reports;
2
3 /*
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *   http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21
22 import com.opensymphony.xwork2.Preparable;
23 import org.apache.archiva.metadata.repository.MetadataRepository;
24 import org.apache.archiva.metadata.repository.MetadataRepositoryException;
25 import org.apache.archiva.metadata.repository.RepositorySession;
26 import org.apache.archiva.metadata.repository.stats.RepositoryStatistics;
27 import org.apache.archiva.metadata.repository.stats.RepositoryStatisticsManager;
28 import org.apache.archiva.reports.RepositoryProblemFacet;
29 import org.apache.commons.io.IOUtils;
30 import org.apache.commons.lang.StringUtils;
31 import org.apache.commons.lang.time.DateUtils;
32 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
33 import org.apache.archiva.security.ArchivaRoleConstants;
34 import org.apache.maven.archiva.web.action.AbstractRepositoryBasedAction;
35 import org.codehaus.plexus.redback.rbac.Resource;
36 import org.codehaus.redback.integration.interceptor.SecureAction;
37 import org.codehaus.redback.integration.interceptor.SecureActionBundle;
38 import org.codehaus.redback.integration.interceptor.SecureActionException;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.springframework.context.annotation.Scope;
42 import org.springframework.stereotype.Controller;
43
44 import javax.inject.Inject;
45 import java.io.ByteArrayInputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.StringReader;
49 import java.text.ParseException;
50 import java.util.ArrayList;
51 import java.util.Calendar;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.Date;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.TreeMap;
58
59 /**
60  * plexus.component role="com.opensymphony.xwork2.Action" role-hint="generateReport" instantiation-strategy="per-lookup"
61  */
62 @Controller( "generateReport" )
63 @Scope( "prototype" )
64 public class GenerateReportAction
65     extends AbstractRepositoryBasedAction
66     implements SecureAction, Preparable
67 {
68     public static final String ALL_REPOSITORIES = "All Repositories";
69
70     public static final String BLANK = "blank";
71
72     private static final String[] datePatterns =
73         new String[]{ "MM/dd/yy", "MM/dd/yyyy", "MMMMM/dd/yyyy", "MMMMM/dd/yy", "dd MMMMM yyyy", "dd/MM/yy",
74             "dd/MM/yyyy", "yyyy/MM/dd", "yyyy-MM-dd", "yyyy-dd-MM", "MM-dd-yyyy", "MM-dd-yy" };
75
76     public static final String SEND_FILE = "send-file";
77
78     private Logger log = LoggerFactory.getLogger( GenerateReportAction.class );
79
80     /**
81      * plexus.requirement
82      */
83     @Inject
84     private ArchivaConfiguration archivaConfiguration;
85
86     /**
87      * plexus.requirement
88      */
89     @Inject
90     private RepositoryStatisticsManager repositoryStatisticsManager;
91
92     private String groupId;
93
94     private String repositoryId;
95
96     private int page = 1;
97
98     private int rowCount = 100;
99
100     private List<String> selectedRepositories = new ArrayList<String>();
101
102     private String startDate;
103
104     private String endDate;
105
106     private int numPages;
107
108     private Collection<String> repositoryIds;
109
110     private Map<String, List<RepositoryProblemFacet>> repositoriesMap =
111         new TreeMap<String, List<RepositoryProblemFacet>>();
112
113     private List<String> availableRepositories;
114
115     private List<RepositoryStatistics> repositoryStatistics = new ArrayList<RepositoryStatistics>();
116
117     private InputStream inputStream;
118
119     private boolean lastPage;
120
121     @SuppressWarnings( "unchecked" )
122     public void prepare()
123     {
124         repositoryIds = new ArrayList<String>();
125         repositoryIds.add( ALL_REPOSITORIES ); // comes first to be first in the list
126         repositoryIds.addAll( getObservableRepos() );
127
128         availableRepositories = new ArrayList<String>();
129
130         // remove selected repositories in the option for the statistics report
131         availableRepositories.addAll( archivaConfiguration.getConfiguration().getManagedRepositoriesAsMap().keySet() );
132         for ( String repo : selectedRepositories )
133         {
134             if ( availableRepositories.contains( repo ) )
135             {
136                 availableRepositories.remove( repo );
137             }
138         }
139     }
140
141     /**
142      * Generate the statistics report.
143      * <p/>
144      * check whether single repo report or comparison report
145      * 1. if it is a single repository, get all the statistics for the repository on the specified date
146      * - if no date is specified, get only the latest
147      * (total page = 1 --> no pagination since only the most recent stats will be displayed)
148      * - otherwise, get everything within the date range (total pages = repo stats / rows per page)
149      * - required params: repository, startDate, endDate
150      * <p/>
151      * 2. if multiple repositories, get the latest statistics on each repository on the specified date
152      * - if no date is specified, use the current date endDate
153      * - required params: repositories, endDate
154      * - total pages = repositories / rows per page
155      *
156      * @return action result
157      */
158     public String generateStatistics()
159     {
160         if ( rowCount < 10 )
161         {
162             // TODO: move to validation framework
163             addFieldError( "rowCount", "Row count must be larger than 10." );
164             return INPUT;
165         }
166         Date startDateInDF;
167         Date endDateInDF;
168
169         RepositorySession repositorySession = repositorySessionFactory.createSession();
170         try
171         {
172             MetadataRepository metadataRepository = repositorySession.getRepository();
173             if ( selectedRepositories.size() > 1 )
174             {
175                 numPages = 1;
176
177                 try
178                 {
179                     startDateInDF = getStartDateInDateFormat();
180                     endDateInDF = getEndDateInDateFormat();
181                 }
182                 catch ( ParseException e )
183                 {
184                     addActionError( "Error parsing date(s)." );
185                     return ERROR;
186                 }
187
188                 if ( startDateInDF != null && endDateInDF != null && startDateInDF.after( endDateInDF ) )
189                 {
190                     addFieldError( "startDate", "Start Date must be earlier than the End Date" );
191                     return INPUT;
192                 }
193
194                 // multiple repos
195                 for ( String repo : selectedRepositories )
196                 {
197                     List<RepositoryStatistics> stats = null;
198                     try
199                     {
200                         stats =
201                             repositoryStatisticsManager.getStatisticsInRange( metadataRepository, repo, startDateInDF,
202                                                                               endDateInDF );
203                     }
204                     catch ( MetadataRepositoryException e )
205                     {
206                         log.warn( "Unable to retrieve stats, assuming is empty: " + e.getMessage(), e );
207                     }
208                     if ( stats == null || stats.isEmpty() )
209                     {
210                         log.info( "No statistics available for repository '" + repo + "'." );
211                         // TODO set repo's stats to 0
212                         continue;
213                     }
214
215                     repositoryStatistics.add( stats.get( 0 ) );
216                 }
217             }
218             else if ( selectedRepositories.size() == 1 )
219             {
220                 repositoryId = selectedRepositories.get( 0 );
221                 try
222                 {
223                     startDateInDF = getStartDateInDateFormat();
224                     endDateInDF = getEndDateInDateFormat();
225
226                     if ( startDateInDF != null && endDateInDF != null && startDateInDF.after( endDateInDF ) )
227                     {
228                         addFieldError( "startDate", "Start Date must be earlier than the End Date" );
229                         return INPUT;
230                     }
231
232                     List<RepositoryStatistics> stats = null;
233                     try
234                     {
235                         stats = repositoryStatisticsManager.getStatisticsInRange( metadataRepository, repositoryId,
236                                                                                   startDateInDF, endDateInDF );
237                     }
238                     catch ( MetadataRepositoryException e )
239                     {
240                         log.warn( "Unable to retrieve stats, assuming is empty: " + e.getMessage(), e );
241                     }
242                     if ( stats == null || stats.isEmpty() )
243                     {
244                         addActionError(
245                             "No statistics available for repository. Repository might not have been scanned." );
246                         return ERROR;
247                     }
248
249                     int rowCount = getRowCount();
250                     int extraPage = ( stats.size() % rowCount ) != 0 ? 1 : 0;
251                     int totalPages = ( stats.size() / rowCount ) + extraPage;
252                     numPages = totalPages;
253
254                     int currentPage = getPage();
255                     if ( currentPage > totalPages )
256                     {
257                         addActionError(
258                             "Error encountered while generating report :: The requested page exceeds the total number of pages." );
259                         return ERROR;
260                     }
261
262                     int start = rowCount * ( currentPage - 1 );
263                     int end = ( start + rowCount ) - 1;
264
265                     if ( end >= stats.size() )
266                     {
267                         end = stats.size() - 1;
268                     }
269
270                     repositoryStatistics = stats.subList( start, end + 1 );
271                 }
272                 catch ( ParseException pe )
273                 {
274                     addActionError( pe.getMessage() );
275                     return ERROR;
276                 }
277             }
278             else
279             {
280                 addFieldError( "availableRepositories", "Please select a repository (or repositories) from the list." );
281                 return INPUT;
282             }
283         }
284         finally
285         {
286             repositorySession.close();
287         }
288
289         if ( repositoryStatistics.isEmpty() )
290         {
291             return BLANK;
292         }
293
294         return SUCCESS;
295     }
296
297     /**
298      * Export report to CSV.
299      *
300      * @return action result
301      */
302     public String downloadStatisticsReport()
303     {
304         Date startDateInDF;
305         Date endDateInDF;
306
307         selectedRepositories = parseSelectedRepositories();
308         List<RepositoryStatistics> repositoryStatistics = new ArrayList<RepositoryStatistics>();
309
310         StringBuilder input;
311         RepositorySession repositorySession = repositorySessionFactory.createSession();
312         try
313         {
314             MetadataRepository metadataRepository = repositorySession.getRepository();
315             if ( selectedRepositories.size() > 1 )
316             {
317                 try
318                 {
319                     startDateInDF = getStartDateInDateFormat();
320                     endDateInDF = getEndDateInDateFormat();
321                 }
322                 catch ( ParseException e )
323                 {
324                     addActionError( "Error parsing date(s)." );
325                     return ERROR;
326                 }
327
328                 if ( startDateInDF != null && endDateInDF != null && startDateInDF.after( endDateInDF ) )
329                 {
330                     addFieldError( "startDate", "Start Date must be earlier than the End Date" );
331                     return INPUT;
332                 }
333
334                 input = new StringBuilder(
335                     "Repository,Total File Count,Total Size,Artifact Count,Group Count,Project Count,Plugins,Archetypes,"
336                         + "Jars,Wars\n" );
337
338                 // multiple repos
339                 for ( String repo : selectedRepositories )
340                 {
341                     List<RepositoryStatistics> stats = null;
342                     try
343                     {
344                         stats =
345                             repositoryStatisticsManager.getStatisticsInRange( metadataRepository, repo, startDateInDF,
346                                                                               endDateInDF );
347                     }
348                     catch ( MetadataRepositoryException e )
349                     {
350                         log.warn( "Unable to retrieve stats, assuming is empty: " + e.getMessage(), e );
351                     }
352                     if ( stats == null || stats.isEmpty() )
353                     {
354                         log.info( "No statistics available for repository '" + repo + "'." );
355                         // TODO set repo's stats to 0
356                         continue;
357                     }
358
359                     // only the first one
360                     RepositoryStatistics repositoryStats = stats.get( 0 );
361                     repositoryStatistics.add( repositoryStats );
362
363                     input.append( repo ).append( "," );
364                     input.append( repositoryStats.getTotalFileCount() ).append( "," );
365                     input.append( repositoryStats.getTotalArtifactFileSize() ).append( "," );
366                     input.append( repositoryStats.getTotalArtifactCount() ).append( "," );
367                     input.append( repositoryStats.getTotalGroupCount() ).append( "," );
368                     input.append( repositoryStats.getTotalProjectCount() ).append( "," );
369                     input.append( repositoryStats.getTotalCountForType( "maven-plugin" ) ).append( "," );
370                     input.append( repositoryStats.getTotalCountForType( "maven-archetype" ) ).append( "," );
371                     input.append( repositoryStats.getTotalCountForType( "jar" ) ).append( "," );
372                     input.append( repositoryStats.getTotalCountForType( "war" ) );
373                     input.append( "\n" );
374                 }
375             }
376             else if ( selectedRepositories.size() == 1 )
377             {
378                 repositoryId = selectedRepositories.get( 0 );
379                 try
380                 {
381                     startDateInDF = getStartDateInDateFormat();
382                     endDateInDF = getEndDateInDateFormat();
383
384                     if ( startDateInDF != null && endDateInDF != null && startDateInDF.after( endDateInDF ) )
385                     {
386                         addFieldError( "startDate", "Start Date must be earlier than the End Date" );
387                         return INPUT;
388                     }
389
390                     List<RepositoryStatistics> stats = null;
391                     try
392                     {
393                         stats = repositoryStatisticsManager.getStatisticsInRange( metadataRepository, repositoryId,
394                                                                                   startDateInDF, endDateInDF );
395                     }
396                     catch ( MetadataRepositoryException e )
397                     {
398                         log.warn( "Unable to retrieve stats, assuming is empty: " + e.getMessage(), e );
399                     }
400                     if ( stats == null || stats.isEmpty() )
401                     {
402                         addActionError(
403                             "No statistics available for repository. Repository might not have been scanned." );
404                         return ERROR;
405                     }
406
407                     input = new StringBuilder(
408                         "Date of Scan,Total File Count,Total Size,Artifact Count,Group Count,Project Count,Plugins,"
409                             + "Archetypes,Jars,Wars\n" );
410
411                     for ( RepositoryStatistics repositoryStats : stats )
412                     {
413                         input.append( repositoryStats.getScanStartTime() ).append( "," );
414                         input.append( repositoryStats.getTotalFileCount() ).append( "," );
415                         input.append( repositoryStats.getTotalArtifactFileSize() ).append( "," );
416                         input.append( repositoryStats.getTotalArtifactCount() ).append( "," );
417                         input.append( repositoryStats.getTotalGroupCount() ).append( "," );
418                         input.append( repositoryStats.getTotalProjectCount() ).append( "," );
419                         input.append( repositoryStats.getTotalCountForType( "maven-plugin" ) ).append( "," );
420                         input.append( repositoryStats.getTotalCountForType( "maven-archetype" ) ).append( "," );
421                         input.append( repositoryStats.getTotalCountForType( "jar" ) ).append( "," );
422                         input.append( repositoryStats.getTotalCountForType( "war" ) );
423                         input.append( "\n" );
424                     }
425
426                     repositoryStatistics = stats;
427                 }
428                 catch ( ParseException pe )
429                 {
430                     addActionError( pe.getMessage() );
431                     return ERROR;
432                 }
433             }
434             else
435             {
436                 addFieldError( "availableRepositories", "Please select a repository (or repositories) from the list." );
437                 return INPUT;
438             }
439         }
440         finally
441         {
442             repositorySession.close();
443         }
444
445         if ( repositoryStatistics.isEmpty() )
446         {
447             return BLANK;
448         }
449
450         // write output stream depending on single or comparison report
451         StringReader reader = new StringReader( input.toString() );
452
453         try
454         {
455             inputStream = new ByteArrayInputStream( IOUtils.toByteArray( reader ) );
456         }
457         catch ( IOException i )
458         {
459             addActionError( "Error occurred while generating CSV file." );
460             return ERROR;
461         }
462
463         return SEND_FILE;
464     }
465
466     // hack for parsing the struts list passed as param in <s:url ../>
467
468     private List<String> parseSelectedRepositories()
469     {
470         List<String> parsedSelectedRepos = new ArrayList<String>();
471
472         for ( String repo : selectedRepositories )
473         {
474             String[] tokens = StringUtils.split( repo, ',' );
475             if ( tokens.length > 1 )
476             {
477                 for ( String token : tokens )
478                 {
479                     parsedSelectedRepos.add( StringUtils.remove( StringUtils.remove( token, '[' ), ']' ).trim() );
480                 }
481             }
482             else
483             {
484                 parsedSelectedRepos.add( StringUtils.remove( StringUtils.remove( repo, '[' ), ']' ).trim() );
485             }
486         }
487         return parsedSelectedRepos;
488     }
489
490     private Date getStartDateInDateFormat()
491         throws ParseException
492     {
493         Date startDateInDF;
494         if ( startDate == null || "".equals( startDate ) )
495         {
496             startDateInDF = null;
497         }
498         else
499         {
500             startDateInDF = DateUtils.parseDate( startDate, datePatterns );
501         }
502         return startDateInDF;
503     }
504
505     private Date getEndDateInDateFormat()
506         throws ParseException
507     {
508         Date endDateInDF;
509         if ( endDate == null || "".equals( endDate ) )
510         {
511             endDateInDF = null;
512         }
513         else
514         {
515             endDateInDF = DateUtils.parseDate( endDate, datePatterns );
516
517             // add a day, since we don't inclue time and want the date to be inclusive
518             Calendar cal = Calendar.getInstance();
519             cal.setTime( endDateInDF );
520             cal.add( Calendar.DAY_OF_MONTH, 1 );
521             endDateInDF = cal.getTime();
522         }
523
524         return endDateInDF;
525     }
526
527     public String execute()
528         throws Exception
529     {
530         if ( repositoryId == null )
531         {
532             addFieldError( "repositoryId", "You must provide a repository id." );
533             return INPUT;
534         }
535
536         if ( rowCount < 10 )
537         {
538             addFieldError( "rowCount", "Row count must be larger than 10." );
539             return INPUT;
540         }
541
542         List<String> observableRepos = getObservableRepos();
543         Collection<String> repoIds;
544         if ( StringUtils.isEmpty( repositoryId ) || ALL_REPOSITORIES.equals( repositoryId ) )
545         {
546             repoIds = observableRepos;
547         }
548         else if ( observableRepos.contains( repositoryId ) )
549         {
550             repoIds = Collections.singletonList( repositoryId );
551         }
552         else
553         {
554             repoIds = Collections.emptyList();
555         }
556
557         List<RepositoryProblemFacet> problemArtifacts = new ArrayList<RepositoryProblemFacet>();
558         RepositorySession repositorySession = repositorySessionFactory.createSession();
559         try
560         {
561             MetadataRepository metadataRepository = repositorySession.getRepository();
562             for ( String repoId : repoIds )
563             {
564                 // TODO: improve performance by navigating into a group subtree. Currently group is property, not part of name of item
565                 for ( String name : metadataRepository.getMetadataFacets( repoId, RepositoryProblemFacet.FACET_ID ) )
566                 {
567                     RepositoryProblemFacet metadataFacet =
568                         (RepositoryProblemFacet) metadataRepository.getMetadataFacet( repoId,
569                                                                                       RepositoryProblemFacet.FACET_ID,
570                                                                                       name );
571
572                     if ( StringUtils.isEmpty( groupId ) || groupId.equals( metadataFacet.getNamespace() ) )
573                     {
574                         problemArtifacts.add( metadataFacet );
575                     }
576                 }
577             }
578         }
579         finally
580         {
581             repositorySession.close();
582         }
583
584         // TODO: getting range only after reading is not efficient for a large number of artifacts
585         int lowerBound = ( page - 1 ) * rowCount;
586         int upperBound = ( page * rowCount ) + 1; // Add 1 to check if it's the last page or not.
587         if ( upperBound <= problemArtifacts.size() )
588         {
589             problemArtifacts = problemArtifacts.subList( lowerBound, upperBound );
590         }
591         else
592         {
593             problemArtifacts = problemArtifacts.subList( lowerBound, problemArtifacts.size() );
594         }
595
596         for ( RepositoryProblemFacet problem : problemArtifacts )
597         {
598             List<RepositoryProblemFacet> problemsList;
599             if ( repositoriesMap.containsKey( problem.getRepositoryId() ) )
600             {
601                 problemsList = repositoriesMap.get( problem.getRepositoryId() );
602             }
603             else
604             {
605                 problemsList = new ArrayList<RepositoryProblemFacet>();
606                 repositoriesMap.put( problem.getRepositoryId(), problemsList );
607             }
608
609             problemsList.add( problem );
610         }
611
612         // TODO: handling should be improved
613         if ( problemArtifacts.size() <= rowCount )
614         {
615             lastPage = true;
616         }
617
618         if ( problemArtifacts.isEmpty() && page == 1 )
619         {
620             return BLANK;
621         }
622         else
623         {
624             return SUCCESS;
625         }
626     }
627
628     public SecureActionBundle getSecureActionBundle()
629         throws SecureActionException
630     {
631         SecureActionBundle bundle = new SecureActionBundle();
632
633         bundle.setRequiresAuthentication( true );
634         bundle.addRequiredAuthorization( ArchivaRoleConstants.OPERATION_ACCESS_REPORT, Resource.GLOBAL );
635
636         return bundle;
637     }
638
639     public Collection<String> getRepositoryIds()
640     {
641         return repositoryIds;
642     }
643
644     public String getGroupId()
645     {
646         return groupId;
647     }
648
649     public void setGroupId( String groupId )
650     {
651         this.groupId = groupId;
652     }
653
654     public String getRepositoryId()
655     {
656         return repositoryId;
657     }
658
659     public void setRepositoryId( String repositoryId )
660     {
661         this.repositoryId = repositoryId;
662     }
663
664     public int getPage()
665     {
666         return page;
667     }
668
669     public void setPage( int page )
670     {
671         this.page = page;
672     }
673
674     public int getRowCount()
675     {
676         return rowCount;
677     }
678
679     public void setRowCount( int rowCount )
680     {
681         this.rowCount = rowCount;
682     }
683
684     public void setRepositoriesMap( Map<String, List<RepositoryProblemFacet>> repositoriesMap )
685     {
686         this.repositoriesMap = repositoriesMap;
687     }
688
689     public Map<String, List<RepositoryProblemFacet>> getRepositoriesMap()
690     {
691         return repositoriesMap;
692     }
693
694     public List<String> getSelectedRepositories()
695     {
696         return selectedRepositories;
697     }
698
699     public void setSelectedRepositories( List<String> selectedRepositories )
700     {
701         this.selectedRepositories = selectedRepositories;
702     }
703
704     public List<String> getAvailableRepositories()
705     {
706         return availableRepositories;
707     }
708
709     public void setAvailableRepositories( List<String> availableRepositories )
710     {
711         this.availableRepositories = availableRepositories;
712     }
713
714     public String getStartDate()
715     {
716         return startDate;
717     }
718
719     public void setStartDate( String startDate )
720     {
721         this.startDate = startDate;
722     }
723
724     public String getEndDate()
725     {
726         return endDate;
727     }
728
729     public void setEndDate( String endDate )
730     {
731         this.endDate = endDate;
732     }
733
734     public List<RepositoryStatistics> getRepositoryStatistics()
735     {
736         return repositoryStatistics;
737     }
738
739     public void setRepositoryStatistics( List<RepositoryStatistics> repositoryStatistics )
740     {
741         this.repositoryStatistics = repositoryStatistics;
742     }
743
744     public boolean isLastPage()
745     {
746         return lastPage;
747     }
748
749     public void setLastPage( boolean lastPage )
750     {
751         this.lastPage = lastPage;
752     }
753
754     public InputStream getInputStream()
755     {
756         return inputStream;
757     }
758
759     public int getNumPages()
760     {
761         return numPages;
762     }
763
764     public void setRepositoryStatisticsManager( RepositoryStatisticsManager repositoryStatisticsManager )
765     {
766         this.repositoryStatisticsManager = repositoryStatisticsManager;
767     }
768 }