/* * Copyright 2016 the original author or authors. * * Licensed 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. */ package org.gradle.internal.logging.sink; import com.google.common.collect.Lists; import org.gradle.api.Nullable; import org.gradle.internal.SystemProperties; import org.gradle.internal.logging.events.BatchOutputEventListener; import org.gradle.internal.logging.events.EndOutputEvent; import org.gradle.internal.logging.events.LogEvent; import org.gradle.internal.logging.events.OperationIdentifier; import org.gradle.internal.logging.events.OutputEvent; import org.gradle.internal.logging.events.OutputEventListener; import org.gradle.internal.logging.events.ProgressCompleteEvent; import org.gradle.internal.logging.events.ProgressEvent; import org.gradle.internal.logging.events.ProgressStartEvent; import org.gradle.internal.logging.events.RenderableOutputEvent; import org.gradle.internal.logging.events.StyledTextOutputEvent; import org.gradle.internal.logging.text.StyledTextOutput; import org.gradle.internal.progress.BuildOperationCategory; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * An {@code org.gradle.logging.internal.OutputEventListener} implementation which generates output events to log the * progress of operations. */ public class GroupingProgressLogEventGenerator extends BatchOutputEventListener { static final String EOL = SystemProperties.getInstance().getLineSeparator(); private final OutputEventListener listener; // Maintain a hierarchy of all build operation ids — heads up: this is a *forest*, not just 1 tree private final Map<Object, Object> buildOpIdHierarchy = new HashMap<Object, Object>(); private final Map<Object, OperationGroup> operationsInProgress = new LinkedHashMap<Object, OperationGroup>(); private final Map<OperationIdentifier, Object> progressToBuildOpIdMap = new HashMap<OperationIdentifier, Object>(); private Object lastRenderedBuildOpId; private boolean needsHeader; public GroupingProgressLogEventGenerator(OutputEventListener listener) { this.listener = listener; } public void onOutput(OutputEvent event) { if (event instanceof ProgressStartEvent) { onStart((ProgressStartEvent) event); } else if (event instanceof RenderableOutputEvent) { handleOutput((RenderableOutputEvent) event); } else if (event instanceof ProgressCompleteEvent) { onComplete((ProgressCompleteEvent) event); } else if (event instanceof EndOutputEvent) { onEnd((EndOutputEvent) event); } else if (!(event instanceof ProgressEvent)) { listener.onOutput(event); } } private void onStart(ProgressStartEvent startEvent) { Object buildOpId = startEvent.getBuildOperationId(); if (buildOpId != null) { buildOpIdHierarchy.put(buildOpId, startEvent.getParentBuildOperationId()); progressToBuildOpIdMap.put(startEvent.getProgressOperationId(), buildOpId); // Create a new group for tasks or configure project if (isGroupedOperation(startEvent.getBuildOperationCategory())) { String header = startEvent.getLoggingHeader() != null ? startEvent.getLoggingHeader() : startEvent.getDescription(); operationsInProgress.put(buildOpId, new OperationGroup(startEvent.getCategory(), header, startEvent.getTimestamp(), startEvent.getBuildOperationId())); } } } private boolean isGroupedOperation(BuildOperationCategory buildOperationCategory) { return buildOperationCategory == BuildOperationCategory.TASK || buildOperationCategory == BuildOperationCategory.CONFIGURE_PROJECT; } private void handleOutput(RenderableOutputEvent event) { Object operationId = getOperationId(event.getBuildOperationId()); if (operationId != null) { operationsInProgress.get(operationId).bufferOutput(event); } else { onUngroupedOutput(event); } } private void onComplete(ProgressCompleteEvent completeEvent) { Object buildOpId = progressToBuildOpIdMap.remove(completeEvent.getProgressOperationId()); buildOpIdHierarchy.remove(buildOpId); OperationGroup group = operationsInProgress.remove(buildOpId); if (group != null) { group.flushOutput(); } } private void onEnd(EndOutputEvent event) { for (OperationGroup group : operationsInProgress.values()) { group.flushOutput(); } listener.onOutput(event); buildOpIdHierarchy.clear(); operationsInProgress.clear(); progressToBuildOpIdMap.clear(); } private void onUngroupedOutput(RenderableOutputEvent event) { needsHeader = true; listener.onOutput(event); } // Return the id of the operation/group, checking up the build operation hierarchy private Object getOperationId(@Nullable final Object buildOpId) { Object current = buildOpId; while (current != null) { if (operationsInProgress.containsKey(current)) { return current; } current = buildOpIdHierarchy.get(current); } return null; } private class OperationGroup { private final Object buildOpIdentifier; private final String category; private final String loggingHeader; private final long startTime; private List<RenderableOutputEvent> bufferedLogs = new ArrayList<RenderableOutputEvent>(); private OperationGroup(String category, @Nullable String loggingHeader, long startTime, Object buildOpIdentifier) { this.category = category; this.loggingHeader = loggingHeader; this.startTime = startTime; this.buildOpIdentifier = buildOpIdentifier; } private LogEvent spacerLine() { return new LogEvent(startTime, category, null, "", null); } StyledTextOutputEvent header(final String message) { List<StyledTextOutputEvent.Span> spans = Lists.newArrayList(new StyledTextOutputEvent.Span(StyledTextOutput.Style.Header, "> " + message), new StyledTextOutputEvent.Span(EOL)); return new StyledTextOutputEvent(startTime, category, null, buildOpIdentifier, spans); } void bufferOutput(RenderableOutputEvent output) { bufferedLogs.add(output); } void flushOutput() { if (!bufferedLogs.isEmpty()) { // Visually indicate group by adding surrounding lines if (needsHeader) { listener.onOutput(spacerLine()); needsHeader = false; } listener.onOutput(header(loggingHeader)); for (RenderableOutputEvent renderableEvent : bufferedLogs) { listener.onOutput(renderableEvent); } // Visually indicate a new group by adding a line if not appending to last rendered group if (!buildOpIdentifier.equals(lastRenderedBuildOpId)) { listener.onOutput(spacerLine()); } lastRenderedBuildOpId = buildOpIdentifier; } } } }