/* * 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 org.gradle.api.logging.LogLevel; import org.gradle.internal.SystemProperties; import org.gradle.internal.logging.events.BatchOutputEventListener; 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.util.GUtil; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import static org.gradle.internal.logging.text.StyledTextOutput.Style; /** * An {@code org.gradle.logging.internal.OutputEventListener} implementation which generates output events to log the * progress of operations. */ public class ProgressLogEventGenerator extends BatchOutputEventListener { private static final String EOL = SystemProperties.getInstance().getLineSeparator(); private final OutputEventListener listener; private final boolean deferHeader; private final Map<OperationIdentifier, Operation> operations = new LinkedHashMap<OperationIdentifier, Operation>(); public ProgressLogEventGenerator(OutputEventListener listener, boolean deferHeader) { this.listener = listener; this.deferHeader = deferHeader; } public void onOutput(OutputEvent event) { if (event instanceof ProgressStartEvent) { onStart((ProgressStartEvent) event); } else if (event instanceof ProgressCompleteEvent) { onComplete((ProgressCompleteEvent) event); } else if (event instanceof RenderableOutputEvent) { doOutput((RenderableOutputEvent) event); } else if (!(event instanceof ProgressEvent)) { listener.onOutput(event); } } private void doOutput(RenderableOutputEvent event) { for (Operation operation : operations.values()) { operation.completeHeader(); } listener.onOutput(event); } private void onComplete(ProgressCompleteEvent progressCompleteEvent) { assert !operations.isEmpty(); Operation operation = operations.remove(progressCompleteEvent.getProgressOperationId()); // Didn't find an operation with that id in the map if (operation == null) { // Remove last operation and complete that Iterator<Map.Entry<OperationIdentifier, Operation>> entryIterator = operations.entrySet().iterator(); Map.Entry<OperationIdentifier, Operation> lastEntry = entryIterator.next(); while (entryIterator.hasNext()) { lastEntry = entryIterator.next(); } entryIterator.remove(); operation = lastEntry.getValue(); // TODO: Do we actually run into this case anymore? } completeOperation(progressCompleteEvent, operation); } private void completeOperation(ProgressCompleteEvent progressCompleteEvent, Operation operation) { operation.status = progressCompleteEvent.getStatus(); operation.completeTime = progressCompleteEvent.getTimestamp(); operation.complete(); } private void onStart(ProgressStartEvent progressStartEvent) { Operation operation = new Operation(progressStartEvent.getCategory(), progressStartEvent.getLoggingHeader(), progressStartEvent.getTimestamp(), progressStartEvent.getBuildOperationId()); operations.put(progressStartEvent.getProgressOperationId(), operation); if (!deferHeader || !(progressStartEvent.getLoggingHeader() != null && progressStartEvent.getLoggingHeader().equals(progressStartEvent.getShortDescription()))) { operation.startHeader(); } } enum State {None, HeaderStarted, HeaderCompleted, Completed} private class Operation { private final Object buildOperationIdentifier; private final String category; private final String loggingHeader; private final long startTime; private final boolean hasLoggingHeader; private String status = ""; private State state = State.None; private long completeTime; private Operation(String category, String loggingHeader, long startTime, Object buildOperationIdentifier) { this.category = category; this.loggingHeader = loggingHeader; this.startTime = startTime; this.hasLoggingHeader = GUtil.isTrue(loggingHeader); this.buildOperationIdentifier = buildOperationIdentifier; } private StyledTextOutputEvent plainTextEvent(long timestamp, String text) { return new StyledTextOutputEvent(timestamp, category, LogLevel.LIFECYCLE, buildOperationIdentifier, Collections.singletonList(new StyledTextOutputEvent.Span(text))); } private StyledTextOutputEvent styledTextEvent(long timestamp, StyledTextOutputEvent.Span... spans) { return new StyledTextOutputEvent(timestamp, category, LogLevel.LIFECYCLE, buildOperationIdentifier, Arrays.asList(spans)); } private void doOutput(RenderableOutputEvent event) { for (Operation pending : operations.values()) { if (pending == this) { break; } pending.completeHeader(); } listener.onOutput(event); } public void startHeader() { assert state == State.None; if (hasLoggingHeader) { state = State.HeaderStarted; doOutput(plainTextEvent(startTime, loggingHeader)); } else { state = State.HeaderCompleted; } } public void completeHeader() { switch (state) { case None: if (hasLoggingHeader) { listener.onOutput(plainTextEvent(startTime, loggingHeader + EOL)); } break; case HeaderStarted: listener.onOutput(plainTextEvent(startTime, EOL)); break; case HeaderCompleted: return; default: throw new IllegalStateException("state is " + state); } state = State.HeaderCompleted; } public void complete() { boolean hasStatus = GUtil.isTrue(status); switch (state) { case None: if (hasLoggingHeader && hasStatus) { doOutput(styledTextEvent(completeTime, new StyledTextOutputEvent.Span(loggingHeader + ' '), new StyledTextOutputEvent.Span(Style.ProgressStatus, status), new StyledTextOutputEvent.Span(EOL))); } else if (hasLoggingHeader) { doOutput(plainTextEvent(completeTime, loggingHeader + EOL)); } break; case HeaderStarted: assert hasLoggingHeader; if (hasStatus) { doOutput(styledTextEvent(completeTime, new StyledTextOutputEvent.Span(" "), new StyledTextOutputEvent.Span(Style.ProgressStatus, status), new StyledTextOutputEvent.Span(EOL))); } else { doOutput(plainTextEvent(completeTime, EOL)); } break; case HeaderCompleted: if (hasLoggingHeader && hasStatus) { doOutput(styledTextEvent(completeTime, new StyledTextOutputEvent.Span(loggingHeader + ' '), new StyledTextOutputEvent.Span(Style.ProgressStatus, status), new StyledTextOutputEvent.Span(EOL))); } break; default: throw new IllegalStateException("state is " + state); } state = State.Completed; } } }