OperationsReportUtils.java
/*-
* ========================LICENSE_START=================================
* flyway-reports
* ========================================================================
* Copyright (C) 2010 - 2025 Red Gate Software Ltd
* ========================================================================
* 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.
* =========================LICENSE_END==================================
*/
package org.flywaydb.reports.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.CustomLog;
import lombok.NoArgsConstructor;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.output.CompositeResult;
import org.flywaydb.core.api.output.HtmlResult;
import org.flywaydb.core.api.output.OperationResult;
import org.flywaydb.core.internal.configuration.ConfigUtils;
import org.flywaydb.core.internal.configuration.models.FlywayModel;
import org.flywaydb.core.internal.plugin.PluginRegister;
import org.flywaydb.core.internal.reports.ReportDetails;
import org.flywaydb.core.internal.util.FileUtils;
import org.flywaydb.core.internal.util.StringUtils;
import org.flywaydb.core.internal.util.JsonUtils;
import org.flywaydb.reports.json.HtmlResultDeserializer;
@CustomLog
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OperationsReportUtils {
private static final String DEFAULT_REPORT_FILENAME = FlywayModel.DEFAULT_REPORT_FILENAME;
private static final String JSON_REPORT_EXTENSION = ".json";
private static final String HTML_REPORT_EXTENSION = ".html";
private static final String HTM_REPORT_EXTENSION = ".htm";
private static final Pattern REPORT_FILE_PATTERN = Pattern.compile("\\.html?$");
static String getBaseFilename(final String filename) {
if (REPORT_FILE_PATTERN.matcher(filename).find()) {
return filename.replaceAll(REPORT_FILE_PATTERN.pattern(), "");
}
return filename;
}
static String createHtmlReport(final Configuration configuration,
final CompositeResult<HtmlResult> htmlCompositeResult,
final String tmpHtmlReportFilename) {
return HtmlUtils.toHtmlFile(tmpHtmlReportFilename, htmlCompositeResult, configuration);
}
static String createJsonReport(final CompositeResult<HtmlResult> htmlCompositeResult,
final String tmpJsonReportFilename) {
return JsonUtils.jsonToFile(tmpJsonReportFilename, htmlCompositeResult);
}
public static ReportDetails writeReport(final Configuration configuration,
final OperationResult filteredResults,
final LocalDateTime executionTime) {
final ReportDetails reportDetails = new ReportDetails();
CompositeResult<HtmlResult> htmlCompositeResult = removeRedundantHtmlResults(flattenHtmlResults(filteredResults),
configuration.isReportEnabled());
if (htmlCompositeResult != null && !htmlCompositeResult.individualResults.isEmpty()) {
htmlCompositeResult.individualResults.forEach(r -> r.setTimestamp(executionTime));
final String reportFilename = configuration.getReportFilename();
final String baseReportFilename = getBaseFilename(reportFilename);
String tmpJsonReportFilename = baseReportFilename + JSON_REPORT_EXTENSION;
String tmpHtmlReportFilename = baseReportFilename + (reportFilename.endsWith(HTM_REPORT_EXTENSION)
? HTM_REPORT_EXTENSION
: HTML_REPORT_EXTENSION);
tmpJsonReportFilename = ConfigUtils.getFilenameWithWorkingDirectory(tmpJsonReportFilename, configuration);
tmpHtmlReportFilename = ConfigUtils.getFilenameWithWorkingDirectory(tmpHtmlReportFilename, configuration);
try {
htmlCompositeResult = appendIfExists(tmpJsonReportFilename,
htmlCompositeResult,
configuration.getPluginRegister());
reportDetails.setJsonReportFilename(createJsonReport(htmlCompositeResult, tmpJsonReportFilename));
reportDetails.setHtmlReportFilename(createHtmlReport(configuration,
htmlCompositeResult,
tmpHtmlReportFilename));
} catch (final FlywayException e) {
if (DEFAULT_REPORT_FILENAME.equals(reportFilename)) {
LOG.warn("Unable to create default report files.");
if (LOG.isDebugEnabled()) {
e.printStackTrace(System.out);
}
} else {
LOG.error("Unable to create report files", e);
}
}
if (reportDetails.getHtmlReportFilename() != null) {
LOG.info("A Flyway report has been generated here: " + reportDetails.getHtmlReportFilename());
}
}
return reportDetails;
}
public static <T extends OperationResult> CompositeResult<T> appendIfExists(final String filename,
final CompositeResult<T> newObject,
final PluginRegister pluginRegister) {
final Path path = Path.of(filename);
if (!Files.exists(path)) {
return newObject;
}
final String jsonText = FileUtils.readAsString(path);
if (!StringUtils.hasText(jsonText)) {
return newObject;
}
final JsonMapper mapper = JsonUtils.getJsonMapper();
final ReportsDeserializer reportsDeserializer = new ReportsDeserializer(pluginRegister);
mapper.registerModule(new SimpleModule().addDeserializer(OperationResult.class, reportsDeserializer));
try {
final CompositeResult<T> existingObject = mapper.readValue(jsonText, new TypeReference<>() {});
if (existingObject == null || existingObject.individualResults.isEmpty()) {
throw new FlywayException("Unable to deserialize existing JSON file: " + filename);
}
existingObject.individualResults.addAll(newObject.individualResults);
return existingObject;
} catch (final Exception e) {
throw new FlywayException("Unable to read filename: " + filename, e);
}
}
public static OperationResult filterHtmlResults(final OperationResult result) {
if (result instanceof CompositeResult<?>) {
final List<OperationResult> filteredResults = ((CompositeResult<?>) result).individualResults.stream().map(
OperationsReportUtils::filterHtmlResults).filter(Objects::nonNull).collect(Collectors.toList());
if (filteredResults.isEmpty()) {
return null;
}
final CompositeResult<OperationResult> htmlCompositeResult = new CompositeResult<>();
htmlCompositeResult.individualResults.addAll(filteredResults);
return htmlCompositeResult;
} else if (result instanceof HtmlResult) {
return result;
}
return null;
}
public static Exception getAggregateExceptions(final OperationResult result) {
if (result instanceof CompositeResult<?>) {
Exception aggregate = null;
final List<Exception> exceptions = ((CompositeResult<?>) result).individualResults.stream().map(
OperationsReportUtils::getAggregateExceptions).filter(Objects::nonNull).collect(Collectors.toList());
for (final Exception e : exceptions) {
if (aggregate == null) {
aggregate = e;
} else {
aggregate.addSuppressed(e);
}
}
return aggregate;
} else if (result instanceof HtmlResult) {
return ((HtmlResult) result).exceptionObject;
}
return null;
}
static CompositeResult<HtmlResult> flattenHtmlResults(final OperationResult result) {
final CompositeResult<HtmlResult> htmlCompositeResult = new CompositeResult<>();
if (result instanceof CompositeResult<?>) {
final List<HtmlResult> htmlResults = ((CompositeResult<?>) result).individualResults.stream().map(
OperationsReportUtils::flattenHtmlResults).flatMap(r -> r.individualResults.stream()).toList();
htmlCompositeResult.individualResults.addAll(htmlResults);
} else if (result instanceof HtmlResult) {
htmlCompositeResult.individualResults.add((HtmlResult) result);
}
return htmlCompositeResult;
}
static CompositeResult<HtmlResult> removeRedundantHtmlResults(final CompositeResult<HtmlResult> htmlCompositeResult,
final boolean isReportEnabled) {
if (htmlCompositeResult == null || htmlCompositeResult.individualResults == null) {
return null;
}
if (!isReportEnabled) {
htmlCompositeResult.individualResults = htmlCompositeResult.individualResults.stream().filter(r -> List.of(
"changes",
"drift",
"dryrun",
"code").contains(r.getOperation().toLowerCase())).collect(Collectors.toList());
}
return htmlCompositeResult;
}
}