All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.teamscale.commons.toml.TeamscaleIntegrationConfigurationParser Maven / Gradle / Ivy

There is a newer version: 2025.1.0
Show newest version
package com.teamscale.commons.toml;

import java.io.File;
import java.io.IOException;
import java.util.Optional;

import org.checkerframework.checker.nullness.qual.Nullable;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
import com.fasterxml.jackson.dataformat.toml.TomlStreamReadException;
import com.google.common.annotations.VisibleForTesting;

/**
 * Parser that reads configuration files defining how source files on external
 * systems can be mapped to uniform paths on a Teamscale server. For example,
 * the format defines for a given source file as which uniform path on which
 * Teamscale Server (url, project id, branch) the file is known by Teamscale.
 *
 * Use case: IDE plugins (which need to query and upload data for a given source
 * file in an IDE project).
 *
 * The config files are named {@link #CONFIGURATION_FILE_NAME} and can be in a
 * folder hierarchy. Config items not defined in a nested file can be defined in
 * a file higher up in the hierarchy. See
 * architecture-decisions/0016-ide-config-format.md for the details.
 */
public class TeamscaleIntegrationConfigurationParser {

	/**
	 * The name of the config files in the hierarchy. According to the
	 * specification, all file names are ".teamscale.toml".
	 */
	private static final String CONFIGURATION_FILE_NAME = ".teamscale.toml";

	/**
	 * Name of the "version" property. This must be the same as the field name
	 * {@link TeamscaleIntegrationConfigurationFileV1#version}. We need it here to
	 * read the version number from a toml file before deserializing it to a
	 * concrete-version class.
	 */
	public static final String FORMAT_VERSION_PROPERTY_NAME = "version";

	/**
	 * Reads the "aggregated" configuration for the given folder on the local file
	 * system. This includes config files in parent directories of the given folder.
	 */
	public static Optional readAggregatedConfigurationForFolder(File initialFolder)
			throws IOException {
		return readAggregatedConfigurationForFolder(initialFolder, null);
	}

	/**
	 * Reads the "aggregated" configuration for the given folder on the local file
	 * system. This includes config files in parent directories of the given folder.
	 *
	 * Stops parent-file discovery when reaching the given #boundaryFolderName. This
	 * is necessary to avoid reading unrelated configuration files in our unit
	 * tests.
	 */
	@VisibleForTesting
	/* package */ static Optional readAggregatedConfigurationForFolder(
			File initialFolder, @Nullable String boundaryFolderName) throws IOException {
		TeamscaleIntegrationConfiguration aggregatedConfiguration = null;
		File currentFolder = initialFolder;
		while (currentFolder != null && currentFolder.exists()) {
			if (currentFolder.getName().equals(boundaryFolderName)) {
				// we must not explore any further up
				break;
			}
			File potentialTomlFile = new File(currentFolder, CONFIGURATION_FILE_NAME);
			// will try in parent directory in next loop iteration
			currentFolder = currentFolder.getParentFile();
			if (!potentialTomlFile.canRead()) {
				// file does not exist or we are not allowed to read it
				continue;
			}
			TeamscaleIntegrationConfiguration parentTomlFile = readSingleConfigurationFile(potentialTomlFile);
			if (aggregatedConfiguration == null) {
				aggregatedConfiguration = parentTomlFile;
			} else {
				// if some config items are not set in the current aggregated configuration,
				// then we try to import these items from the parentTomlFile
				aggregatedConfiguration = aggregatedConfiguration
						.cloneAndImportMissingConfigurationItemsFrom(parentTomlFile);
			}
			if (aggregatedConfiguration != null && aggregatedConfiguration.isRootConfiguration) {
				// early abort if we read a file with the "isRoot" property set to true
				break;
			}
		}
		if (aggregatedConfiguration != null) {
			aggregatedConfiguration.validate(initialFolder);
		}
		return Optional.ofNullable(aggregatedConfiguration);
	}

	private static TeamscaleIntegrationConfiguration readSingleConfigurationFile(File tomlFile) throws IOException {
		TomlMapper mapper = TomlMapper.builder().build();
		try {
			Optional versionNumber = readFileFormatVersionNumber(tomlFile, mapper);
			if (!versionNumber.isPresent() || versionNumber.get().equals("1.0")) {
				// if no version number is specified, the specification says version 1.0 is used
				// implicitly
				TeamscaleIntegrationConfigurationFileV1 versionedFile = mapper.readValue(tomlFile,
						TeamscaleIntegrationConfigurationFileV1.class);
				return versionedFile.convertToInternalFormat(tomlFile);
			} else {
				throw new IOException("Could not parse Teamscale Integration TOML file " + tomlFile.getAbsolutePath()
						+ ". File format version number " + versionNumber.get() + " not known.");
			}
		} catch (UnrecognizedPropertyException | TomlStreamReadException e) {
			throw new IOException("Could not parse Teamscale Integration TOML file " + tomlFile.getAbsolutePath() + ".",
					e);
		}
	}

	/**
	 * Reads the version number of the toml file (if any is given). This must be
	 * done before deserialization because we need to choose the deserialization
	 * class based on the format version.
	 */
	private static Optional readFileFormatVersionNumber(File tomlFile, TomlMapper mapper) throws IOException {
		JsonNode tree = mapper.readTree(tomlFile);
		JsonNode versionNode = tree.findValue(FORMAT_VERSION_PROPERTY_NAME);
		if (versionNode == null || versionNode.isMissingNode() || !versionNode.isTextual()) {
			return Optional.empty();
		}
		return Optional.of(versionNode.asText());
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy