| package org.apache.continuum.web.action; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.commons.lang.time.DateUtils; |
| import org.apache.maven.continuum.model.project.BuildResult; |
| import org.apache.maven.continuum.model.project.Project; |
| import org.apache.maven.continuum.model.project.ProjectGroup; |
| import org.apache.maven.continuum.project.ContinuumProjectState; |
| import org.apache.maven.continuum.web.action.ContinuumActionSupport; |
| import org.apache.maven.continuum.web.exception.AuthorizationRequiredException; |
| import org.apache.struts2.interceptor.ServletResponseAware; |
| import org.codehaus.plexus.component.annotations.Component; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.servlet.http.HttpServletResponse; |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.text.DateFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| @Component( role = com.opensymphony.xwork2.Action.class, hint = "projectBuildsReport", instantiationStrategy = "per-lookup" ) |
| public class ViewBuildsReportAction |
| extends ContinuumActionSupport |
| implements ServletResponseAware |
| { |
| private static final Logger log = LoggerFactory.getLogger( ViewBuildsReportAction.class ); |
| |
| private static final int MAX_BROWSE_SIZE = 500; |
| |
| private static final int EXPORT_BATCH_SIZE = 4000; |
| |
| private static final int MAX_EXPORT_SIZE = 100000; |
| |
| private static final String[] datePatterns = |
| new String[] { "MM/dd/yy", "MM/dd/yyyy", "MMMMM/dd/yyyy", "MMMMM/dd/yy", "dd MMMMM yyyy", "dd/MM/yy", |
| "dd/MM/yyyy", "yyyy/MM/dd", "yyyy-MM-dd", "yyyy-dd-MM", "MM-dd-yyyy", "MM-dd-yy" }; |
| |
| /** |
| * Encapsulates constants relevant for build results and makes them localizable. |
| */ |
| public enum ResultState |
| { |
| OK( ContinuumProjectState.OK, "projectBuilds.report.resultOk" ), |
| FAILED( ContinuumProjectState.FAILED, "projectBuilds.report.resultFailed" ), |
| ERROR( ContinuumProjectState.ERROR, "projectBuilds.report.resultError" ), |
| BUILDING( ContinuumProjectState.BUILDING, "projectBuilds.report.resultBuilding" ), |
| CANCELLED( ContinuumProjectState.CANCELLED, "projectBuilds.report.resultCanceled" ); |
| |
| private static final Map<Integer, ResultState> dataMap; |
| |
| static |
| { |
| dataMap = new HashMap<Integer, ResultState>(); |
| for ( ResultState val : ResultState.values() ) |
| { |
| dataMap.put( val.dataId, val ); |
| } |
| } |
| |
| private int dataId; |
| |
| private String textKey; |
| |
| ResultState( int dataId, String textKey ) |
| { |
| this.dataId = dataId; |
| this.textKey = textKey; |
| } |
| |
| public int getDataId() |
| { |
| return dataId; |
| } |
| |
| public String getTextKey() |
| { |
| return textKey; |
| } |
| |
| public static ResultState fromId( int state ) |
| { |
| return dataMap.get( state ); |
| } |
| |
| public static boolean knownState( int state ) |
| { |
| return dataMap.containsKey( state ); |
| } |
| } |
| |
| private int buildStatus; |
| |
| private String triggeredBy = ""; |
| |
| private String startDate = ""; |
| |
| private String endDate = ""; |
| |
| private int projectGroupId; |
| |
| private int rowCount = 25; |
| |
| private int page = 1; |
| |
| private int pageTotal; |
| |
| private Map<Integer, String> buildStatuses; |
| |
| private Map<Integer, String> groupSelections; |
| |
| private Map<String, Integer> permittedGroupMap; |
| |
| private List<BuildResult> filteredResults; |
| |
| private HttpServletResponse rawResponse; |
| |
| public void setServletResponse( HttpServletResponse response ) |
| { |
| this.rawResponse = response; |
| } |
| |
| protected boolean isAuthorized( String projectGroupName ) |
| { |
| try |
| { |
| checkViewProjectGroupAuthorization( projectGroupName ); |
| return true; |
| } |
| catch ( AuthorizationRequiredException authzE ) |
| { |
| return false; |
| } |
| } |
| |
| public void prepare() |
| throws Exception |
| { |
| super.prepare(); |
| |
| Collection<ProjectGroup> permittedGroups = getAuthorizedGroups(); |
| |
| groupSelections = createGroupSelections( permittedGroups ); |
| permittedGroupMap = createPermittedGroupMap( permittedGroups ); |
| buildStatuses = createStatusSelections(); |
| } |
| |
| protected Map<String, Integer> createPermittedGroupMap( Collection<ProjectGroup> allowedGroups ) |
| { |
| Map<String, Integer> result = new HashMap<String, Integer>(); |
| for ( ProjectGroup group : allowedGroups ) |
| { |
| result.put( group.getName(), group.getId() ); |
| } |
| return result; |
| } |
| |
| protected Map<Integer, String> createGroupSelections( Collection<ProjectGroup> permittedGroups ) |
| { |
| Map<Integer, String> result = new LinkedHashMap<Integer, String>(); |
| result.put( 0, "ALL" ); |
| for ( ProjectGroup group : permittedGroups ) |
| { |
| result.put( group.getId(), group.getName() ); |
| } |
| return result; |
| } |
| |
| protected Map<Integer, String> createStatusSelections() |
| { |
| Map<Integer, String> result = new LinkedHashMap<Integer, String>(); |
| result.put( 0, "ALL" ); |
| for ( ResultState state : ResultState.values() ) |
| { |
| result.put( state.getDataId(), getText( state.getTextKey() ) ); |
| } |
| return result; |
| } |
| |
| protected Collection<ProjectGroup> getAuthorizedGroups() |
| { |
| Collection<ProjectGroup> permitted = new ArrayList<ProjectGroup>(); |
| List<ProjectGroup> groups = getContinuum().getAllProjectGroups(); |
| if ( groups != null ) |
| { |
| for ( ProjectGroup group : groups ) |
| { |
| String groupName = group.getName(); |
| if ( isAuthorized( groupName ) ) |
| { |
| permitted.add( group ); |
| } |
| } |
| } |
| return permitted; |
| } |
| |
| public String init() |
| { |
| try |
| { |
| checkViewReportsAuthorization(); |
| } |
| catch ( AuthorizationRequiredException authzE ) |
| { |
| addActionError( authzE.getMessage() ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| if ( permittedGroupMap.isEmpty() ) |
| { |
| addActionError( getText( "projectBuilds.report.noGroupsAuthorized" ) ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| // action class was called from the Menu; do not generate report first |
| return SUCCESS; |
| } |
| |
| public String execute() |
| { |
| try |
| { |
| checkViewReportsAuthorization(); |
| } |
| catch ( AuthorizationRequiredException authzE ) |
| { |
| addActionError( authzE.getMessage() ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| if ( permittedGroupMap.isEmpty() ) |
| { |
| addActionError( getText( "projectBuilds.report.noGroupsAuthorized" ) ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| Date fromDate; |
| Date toDate; |
| |
| try |
| { |
| fromDate = getStartDateInDateFormat(); |
| toDate = getEndDateInDateFormat(); |
| } |
| catch ( ParseException e ) |
| { |
| addActionError( getText( "projectBuilds.report.badDates", new String[] { e.getMessage() } ) ); |
| return INPUT; |
| } |
| |
| if ( fromDate != null && toDate != null && fromDate.after( toDate ) ) |
| { |
| addFieldError( "startDate", getText( "projectBuilds.report.endBeforeStartDate" ) ); |
| return INPUT; |
| } |
| |
| // Limit query to scan only what the user is permitted to see |
| Collection<Integer> groupIds = new HashSet<Integer>(); |
| if ( projectGroupId > 0 ) |
| { |
| groupIds.add( projectGroupId ); |
| } |
| else |
| { |
| groupIds.addAll( permittedGroupMap.values() ); |
| } |
| |
| filteredResults = getContinuum().getBuildResultsInRange( |
| groupIds, fromDate, toDate, buildStatus, triggeredBy, 0, MAX_BROWSE_SIZE ); |
| |
| int resultSize = filteredResults.size(); |
| if ( filteredResults.size() == MAX_BROWSE_SIZE ) |
| { |
| addActionMessage( getText( "projectBuilds.report.limitedResults" ) ); |
| } |
| |
| pageTotal = resultSize / rowCount + ( resultSize % rowCount == 0 ? 0 : 1 ); |
| |
| if ( resultSize > 0 && ( page < 1 || page > pageTotal ) ) |
| { |
| addActionError( getText( "projectBuilds.report.invalidPage" ) ); |
| return INPUT; |
| } |
| |
| int pageStart = rowCount * ( page - 1 ), pageEnd = rowCount * page; |
| |
| // Restrict results to just the page we will show |
| filteredResults = filteredResults.subList( pageStart, pageEnd > resultSize ? resultSize : pageEnd ); |
| |
| return SUCCESS; |
| } |
| |
| /* |
| * Export Builds Report to .csv |
| */ |
| public String downloadBuildsReport() |
| { |
| try |
| { |
| checkViewReportsAuthorization(); |
| } |
| catch ( AuthorizationRequiredException authzE ) |
| { |
| addActionError( authzE.getMessage() ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| if ( permittedGroupMap.isEmpty() ) |
| { |
| addActionError( getText( "projectBuilds.report.noGroupsAuthorized" ) ); |
| return REQUIRES_AUTHORIZATION; |
| } |
| |
| Date fromDate; |
| Date toDate; |
| |
| try |
| { |
| fromDate = getStartDateInDateFormat(); |
| toDate = getEndDateInDateFormat(); |
| } |
| catch ( ParseException e ) |
| { |
| addActionError( getText( "projectBuilds.report.badDates", new String[] { e.getMessage() } ) ); |
| return INPUT; |
| } |
| |
| if ( fromDate != null && toDate != null && fromDate.after( toDate ) ) |
| { |
| addFieldError( "startDate", getText( "projectBuilds.report.endBeforeStartDate" ) ); |
| return INPUT; |
| } |
| |
| try |
| { |
| // HTTP headers so the browser treats the file nicely for the user |
| rawResponse.setContentType( "text/csv" ); |
| rawResponse.addHeader( "Content-disposition", "attachment;filename=continuum_project_builds_report.csv" ); |
| |
| Writer output = rawResponse.getWriter(); |
| DateFormat dateTimeFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ); |
| |
| // Now, stream the file to the browser in pieces |
| try |
| { |
| // Write the CSV file header |
| output.append( "Group,Project,ID,Build#,Started,Duration,Triggered By,Status\n" ); |
| |
| // Limit query to scan only what the user is permitted to see |
| Collection<Integer> groupIds = new HashSet<Integer>(); |
| if ( projectGroupId > 0 ) |
| { |
| groupIds.add( projectGroupId ); |
| } |
| else |
| { |
| groupIds.addAll( permittedGroupMap.values() ); |
| } |
| |
| // Build the output file by walking through the results in batches |
| int offset = 0, exported = 0; |
| List<BuildResult> results; |
| export: |
| do |
| { |
| results = getContinuum().getBuildResultsInRange( groupIds, fromDate, toDate, buildStatus, |
| triggeredBy, offset, EXPORT_BATCH_SIZE ); |
| |
| // Ensure we advance through results |
| offset += EXPORT_BATCH_SIZE; |
| |
| // Convert each build result to a line in the CSV file |
| for ( BuildResult result : results ) |
| { |
| Project project = result.getProject(); |
| ProjectGroup projectGroup = project.getProjectGroup(); |
| |
| int resultState = result.getState(); |
| String stateName = ResultState.knownState( resultState ) ? |
| getText( ResultState.fromId( resultState ).getTextKey() ) : |
| getText( "projectBuilds.report.resultUnknown" ); |
| |
| String buildTime = dateTimeFormat.format( new Date( result.getStartTime() ) ); |
| long buildDuration = ( result.getEndTime() - result.getStartTime() ) / 1000; |
| |
| String formattedLine = String.format( "%s,%s,%s,%s,%s,%s,%s,%s\n", |
| projectGroup.getName(), |
| project.getName(), |
| result.getId(), |
| result.getBuildNumber(), |
| buildTime, |
| buildDuration, |
| result.getUsername(), |
| stateName ); |
| |
| output.append( formattedLine ); |
| |
| exported += 1; |
| |
| if ( exported >= MAX_EXPORT_SIZE ) |
| { |
| log.warn( "build report export hit limit of {} records", MAX_EXPORT_SIZE ); |
| break export; |
| } |
| } |
| } |
| while ( results.size() == EXPORT_BATCH_SIZE ); |
| } |
| finally |
| { |
| output.flush(); |
| } |
| } |
| catch ( IOException e ) |
| { |
| addActionError( getText( "projectBuilds.report.exportIOError", new String[] { e.getMessage() } ) ); |
| return INPUT; |
| } |
| |
| return null; |
| } |
| |
| private Date getStartDateInDateFormat() |
| throws ParseException |
| { |
| Date date = null; |
| |
| if ( !StringUtils.isEmpty( startDate ) ) |
| { |
| date = DateUtils.parseDate( startDate, datePatterns ); |
| } |
| |
| return date; |
| } |
| |
| private Date getEndDateInDateFormat() |
| throws ParseException |
| { |
| Date date = null; |
| |
| if ( !StringUtils.isEmpty( endDate ) ) |
| { |
| date = DateUtils.parseDate( endDate, datePatterns ); |
| } |
| |
| return date; |
| } |
| |
| public int getBuildStatus() |
| { |
| return this.buildStatus; |
| } |
| |
| public void setBuildStatus( int buildStatus ) |
| { |
| this.buildStatus = buildStatus; |
| } |
| |
| public int getProjectGroupId() |
| { |
| return this.projectGroupId; |
| } |
| |
| public void setProjectGroupId( int projectGroupId ) |
| { |
| this.projectGroupId = projectGroupId; |
| } |
| |
| public String getTriggeredBy() |
| { |
| return this.triggeredBy; |
| } |
| |
| public void setTriggeredBy( String triggeredBy ) |
| { |
| this.triggeredBy = triggeredBy; |
| } |
| |
| public String getStartDate() |
| { |
| return this.startDate; |
| } |
| |
| public void setStartDate( String startDate ) |
| { |
| this.startDate = startDate; |
| } |
| |
| public String getEndDate() |
| { |
| return this.endDate; |
| } |
| |
| public void setEndDate( String endDate ) |
| { |
| this.endDate = endDate; |
| } |
| |
| public int getRowCount() |
| { |
| return rowCount; |
| } |
| |
| public Map<Integer, String> getBuildStatuses() |
| { |
| return buildStatuses; |
| } |
| |
| public int getPage() |
| { |
| return page; |
| } |
| |
| public void setPage( int page ) |
| { |
| this.page = page; |
| } |
| |
| public int getPageTotal() |
| { |
| return pageTotal; |
| } |
| |
| public List<BuildResult> getFilteredResults() |
| { |
| return filteredResults; |
| } |
| |
| public Map<Integer, String> getProjectGroups() |
| { |
| return groupSelections; |
| } |
| } |