LogProgressCallback.java
/*
* okhttp-client-extensions - A set of helpful extensions to support okhttp clients
* Copyright © 2025 Andy Miles (andy.miles@amilesend.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.amilesend.client.connection.file;
import com.google.common.annotations.VisibleForTesting;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* A log-based implementation of {@link TransferProgressCallback} that logs transfer progress.
* @see TransferProgressCallback
*/
public class LogProgressCallback implements TransferProgressCallback {
private static final Duration DEFAULT_UPDATE_FREQUENCY = Duration.ofMillis(100L);
/**
* The Transfer type. Default is {@link TransferType#UNDEFINED}.
* @see TransferType
*/
private final TransferType transferType;
/** The logging level to record progress updates. Default is {@link Level#INFO} */
private final Level loggingLevel;
/** Duration between logging updates. */
private final Duration updateFrequency;
/** The logger instance. */
private final Logger log;
/** Prefix to include in every logging statement. */
private final String prefix;
/** Used to chain multiple callbacks. */
private final Optional<TransferProgressCallback> chainedCallback;
// Used to limit unnecessarily entries to the log.
@VisibleForTesting
@Getter(AccessLevel.PACKAGE)
private final AtomicInteger lastUpdateProgressValue = new AtomicInteger();
@VisibleForTesting
@Getter(AccessLevel.PACKAGE)
private final AtomicReference<Instant> lastUpdateTimestamp = new AtomicReference<>(Instant.now());
@Builder
private LogProgressCallback(
final TransferType transferType,
final Level loggingLevel,
final Duration updateFrequency,
final String prefix,
final TransferProgressCallback chainedCallback) {
log = LoggerFactory.getLogger(LogProgressCallback.class);
this.transferType = Optional.ofNullable(transferType).orElse(TransferType.UNDEFINED);
this.loggingLevel = Optional.ofNullable(loggingLevel).orElse(Level.INFO);
this.updateFrequency = Optional.ofNullable(updateFrequency).orElse(DEFAULT_UPDATE_FREQUENCY);
this.prefix = Optional.ofNullable(prefix).orElse(StringUtils.EMPTY);
this.chainedCallback = Optional.ofNullable(chainedCallback);
}
/**
* Helper method to format the logging prefix to use.
*
* @param source the source
* @param destination the destination
* @return the logging prefix
*/
public static String formatPrefix(final String source, final String destination) {
Validate.notBlank(source, "source must not be blank");
Validate.notBlank(destination, "destination must not be blank");
return new StringBuilder("[")
.append(source)
.append(" -> ")
.append(destination)
.append("] ")
.toString();
}
@Override
public void onUpdate(final long currentBytes, final long totalBytes) {
try {
if (Duration.between(lastUpdateTimestamp.get(), Instant.now()).compareTo(updateFrequency) < 0) {
return;
}
final int currentProgressPercent = (int) Math.floor(((double) currentBytes / (double) totalBytes) * 100D);
if (currentProgressPercent == lastUpdateProgressValue.get()) {
return;
}
log.atLevel(loggingLevel)
.log("{}{} Status: {}% ({} of {} bytes)",
prefix,
transferType.getLogPrefix(),
currentProgressPercent,
currentBytes,
totalBytes);
lastUpdateTimestamp.set(Instant.now());
lastUpdateProgressValue.set(currentProgressPercent);
} finally {
chainedCallback.ifPresent(c -> c.onUpdate(currentBytes, totalBytes));
}
}
@Override
public void onFailure(final Throwable cause) {
try {
log.error("{}An error occurred during {}: {}", prefix, transferType.getLogPrefix(), cause.getMessage(), cause);
} finally {
chainedCallback.ifPresent(c -> c.onFailure(cause));
}
}
@Override
public void onComplete(final long bytesTransferred) {
try {
log.atLevel(loggingLevel).log(
"{}{} complete with {} bytes transferred",
prefix,
transferType.getLogPrefix(),
bytesTransferred);
} finally {
chainedCallback.ifPresent(c -> c.onComplete(bytesTransferred));
}
}
/** Describes that transfer type used for logging progress. */
@RequiredArgsConstructor
public enum TransferType {
/** Indicates that the transfer is for a file upload. */
UPLOAD("Upload"),
/** Indicates that the transfer is for a file download. */
DOWNLOAD("Download"),
/** Used when the type is not defined and defaults to a generic "Transfer".*/
UNDEFINED("Transfer");
/** The type formatted for log records. */
@Getter
private final String logPrefix;
}
}