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

org.gradle.api.tasks.diagnostics.DependencyInsightReportTask Maven / Gradle / Ivy

There is a newer version: 8.11.1
Show newest version
/*
 * 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.api.tasks.diagnostics;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang.StringUtils;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Incubating;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.result.DependencyResult;
import org.gradle.api.artifacts.result.ResolvedComponentResult;
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
import org.gradle.api.artifacts.result.ResolvedVariantResult;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeContainer;
import org.gradle.api.attributes.HasAttributes;
import org.gradle.api.internal.artifacts.ResolverResults;
import org.gradle.api.internal.artifacts.configurations.ConfigurationInternal;
import org.gradle.api.internal.artifacts.configurations.ResolutionResultProvider;
import org.gradle.api.internal.artifacts.configurations.ResolvableDependenciesInternal;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionComparator;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionParser;
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelectorScheme;
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.graph.results.VisitedGraphResults;
import org.gradle.api.internal.artifacts.resolver.ResolutionOutputsInternal;
import org.gradle.api.internal.attributes.AttributeContainerInternal;
import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.diagnostics.internal.ConfigurationFinder;
import org.gradle.api.tasks.diagnostics.internal.dependencies.AttributeMatchDetails;
import org.gradle.api.tasks.diagnostics.internal.dependencies.MatchType;
import org.gradle.api.tasks.diagnostics.internal.dsl.DependencyResultSpecNotationConverter;
import org.gradle.api.tasks.diagnostics.internal.graph.DependencyGraphsRenderer;
import org.gradle.api.tasks.diagnostics.internal.graph.NodeRenderer;
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.RenderableDependency;
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.Section;
import org.gradle.api.tasks.diagnostics.internal.insight.DependencyInsightReporter;
import org.gradle.api.tasks.diagnostics.internal.text.StyledTable;
import org.gradle.api.tasks.options.Option;
import org.gradle.initialization.StartParameterBuildOptions;
import org.gradle.internal.graph.GraphRenderer;
import org.gradle.internal.instrumentation.api.annotations.ToBeReplacedByLazyProperty;
import org.gradle.internal.logging.text.StyledTextOutput;
import org.gradle.internal.logging.text.StyledTextOutputFactory;
import org.gradle.internal.typeconversion.NotationParser;
import org.gradle.work.DisableCachingByDefault;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.gradle.internal.logging.text.StyledTextOutput.Style.Description;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Failure;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Header;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Identifier;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Info;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Normal;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.UserInput;

/**
 * Generates a report that attempts to answer questions like:
 * 
    *
  • Why is this dependency in the dependency graph?
  • *
  • Exactly which dependencies are pulling this dependency into the graph?
  • *
  • What is the actual version (i.e. *selected* version) of the dependency that will be used? Is it the same as what was *requested*?
  • *
  • Why is the *selected* version of a dependency different to the *requested*?
  • *
  • What variants are available for this dependency?
  • *
* * Use this task to get insight into a particular dependency (or dependencies) * and find out what exactly happens during dependency resolution and conflict resolution. * If the dependency version was forced or selected by the conflict resolution * this information will be available in the report. *

* While the regular dependencies report ({@link DependencyReportTask}) shows the path from the top level dependencies down through the transitive dependencies, * the dependency insight report shows the path from a particular dependency to the dependencies that pulled it in. * That is, it is an inverted view of the regular dependencies report. *

* The task requires setting the dependency spec and the configuration. * For more information on how to configure those please refer to docs for {@link #setDependencySpec(Object)} and * {@link #setConfiguration(String)}. *

* The task can also be configured from the command line. * For more information please refer to {@link #setDependencySpec(Object)}, {@link #setConfiguration(String)}, * {@link #setShowSinglePathToDependency(boolean)}, and {@link #getShowingAllVariants()}. */ @DisableCachingByDefault(because = "Produces only non-cacheable console output") public abstract class DependencyInsightReportTask extends DefaultTask { private Spec dependencySpec; private boolean showSinglePathToDependency; private final Property showingAllVariants = getProject().getObjects().property(Boolean.class); private transient Configuration configuration; private final Property rootComponentProperty = getProject().getObjects().property(ResolvedComponentResult.class); // this field is named with a starting `z` to be serialized after `rootComponentProperty` // because the serialization of `rootComponentProperty` can still trigger callback that can affect // a value of `configuration.getAttributes()`. // TODO:configuration-cache find a way to clean up this #23732 private Provider zConfigurationAttributes; private ResolutionErrorRenderer errorHandler; private String configurationName; private String configurationDescription; /** * The root component of the dependency graph to be inspected. * * @since 7.5 */ @Input @Optional @Incubating public Property getRootComponentProperty() { // Required to maintain DslObject mapping Configuration configuration = getConfiguration(); if (!rootComponentProperty.isPresent() && configuration != null && getDependencySpec() != null) { if (getShowingAllVariants().get()) { ConfigurationInternal configurationInternal = (ConfigurationInternal) configuration; if (!configurationInternal.isCanBeMutated()) { throw new IllegalStateException( "The configuration '" + configuration.getName() + "' is not mutable. " + "In order to use the '--all-variants' option, the configuration must not be resolved before this task is executed." ); } configurationInternal.getResolutionStrategy().setIncludeAllSelectableVariantResults(true); } configurationName = configuration.getName(); configurationDescription = configuration.toString(); zConfigurationAttributes = getProject().provider(configuration::getAttributes); ProviderFactory providerFactory = getProject().getProviders(); ResolutionOutputsInternal resolutionOutputs = ((ResolvableDependenciesInternal) configuration.getIncoming()).getResolutionOutputs(); ResolutionResultProvider graphResultsProvider = resolutionOutputs.getRawResults().map(ResolverResults::getVisitedGraph); errorHandler.addErrorSource(providerFactory.provider(() -> graphResultsProvider.getValue().getResolutionFailure() .map(Collections::singletonList) .orElse(Collections.emptyList())) ); rootComponentProperty.set(providerFactory.provider(() -> { // We do not use the public resolution result API to avoid throwing exceptions that we visit above return graphResultsProvider.getValue().getResolutionResult().getRootSource().get(); })); } return rootComponentProperty; } /** * Selects the dependency (or dependencies if multiple matches found) to show the report for. */ @Internal @ToBeReplacedByLazyProperty(comment = "Should Spec be lazy?") public @Nullable Spec getDependencySpec() { return dependencySpec; } /** * The dependency spec selects the dependency (or dependencies if multiple matches found) to show the report for. * The spec receives an instance of {@link DependencyResult} as parameter. */ public void setDependencySpec(@Nullable Spec dependencySpec) { this.dependencySpec = dependencySpec; this.errorHandler = new ResolutionErrorRenderer(dependencySpec); } /** * Configures the dependency to show the report for. * Multiple notation formats are supported: Strings, instances of {@link Spec} * and groovy closures. Spec and closure receive {@link DependencyResult} as parameter. * Examples of String notation: 'org.slf4j:slf4j-api', 'slf4j-api', or simply: 'slf4j'. * The input may potentially match multiple dependencies. * See also {@link #setDependencySpec(Spec)} *

* This method is exposed to the command line interface. Example usage: *

gradle dependencyInsight --dependency slf4j
*/ @Option(option = "dependency", description = "Shows the details of given dependency.") public void setDependencySpec(@Nullable Object dependencyInsightNotation) { NotationParser> parser = DependencyResultSpecNotationConverter.parser(); setDependencySpec(parser.parseNotation(dependencyInsightNotation)); } /** * Configuration to look the dependency in */ @Internal @ToBeReplacedByLazyProperty public @Nullable Configuration getConfiguration() { return configuration; } /** * Sets the configuration to look the dependency in. */ public void setConfiguration(@Nullable Configuration configuration) { this.configuration = configuration; } /** * Sets the configuration (via name) to look the dependency in. *

* This method is exposed to the command line interface. Example usage: *

gradle dependencyInsight --configuration runtime --dependency slf4j
*/ @ToBeReplacedByLazyProperty @Option(option = "configuration", description = "Looks for the dependency in given configuration.") public void setConfiguration(@Nullable String configurationName) { setConfiguration( configurationName == null ? null : ConfigurationFinder.find(getProject().getConfigurations(), configurationName) ); } /** * Tells if the report should only show one path to each dependency. * * @since 4.9 */ @Internal @ToBeReplacedByLazyProperty public boolean isShowSinglePathToDependency() { return showSinglePathToDependency; } /** * Tells if the report should only display a single path to each dependency, which * can be useful when the graph is large. This is false by default, meaning that for * each dependency, the report will display all paths leading to it. * *

* This method is exposed to the command line interface. Example usage: *

gradle dependencyInsight --single-path
* * @since 4.9 */ @Option(option = "single-path", description = "Show at most one path to each dependency") public void setShowSinglePathToDependency(boolean showSinglePathToDependency) { this.showSinglePathToDependency = showSinglePathToDependency; } /** * Show all variants of each displayed dependency. * *

* Due to internal limitations, this option only works when the {@link #getConfiguration() configuration} is * unresolved before the execution of this task. *

* *

* This method is exposed to the command line interface. Example usage: *

gradle dependencyInsight --all-variants
* * @since 7.5 */ @Option(option = "all-variants", description = "Show all variants of each dependency") @Incubating @Internal public Property getShowingAllVariants() { return showingAllVariants; } @Inject protected StyledTextOutputFactory getTextOutputFactory() { throw new UnsupportedOperationException(); } @Inject protected VersionSelectorScheme getVersionSelectorScheme() { throw new UnsupportedOperationException(); } @Inject protected VersionComparator getVersionComparator() { throw new UnsupportedOperationException(); } @Inject protected VersionParser getVersionParser() { throw new UnsupportedOperationException(); } /** * An injected {@link ImmutableAttributesFactory}. * * @since 4.9 */ @Inject protected ImmutableAttributesFactory getAttributesFactory() { throw new UnsupportedOperationException(); } @TaskAction public void report() { assertValidTaskConfiguration(); ResolvedComponentResult rootComponent = getRootComponentProperty().get(); StyledTextOutput output = getTextOutputFactory().create(getClass()); Set selectedDependencies = selectDependencies(rootComponent); if (selectedDependencies.isEmpty()) { output.println("No dependencies matching given input were found in " + configurationDescription); return; } errorHandler.renderErrors(output); renderSelectedDependencies(output, selectedDependencies); renderBuildScanHint(output); } private void renderSelectedDependencies(StyledTextOutput output, Set selectedDependencies) { GraphRenderer renderer = new GraphRenderer(output); DependencyInsightReporter reporter = new DependencyInsightReporter(getVersionSelectorScheme(), getVersionComparator(), getVersionParser()); Collection itemsToRender = reporter.convertToRenderableItems(selectedDependencies, isShowSinglePathToDependency()); RootDependencyRenderer rootRenderer = new RootDependencyRenderer(this, zConfigurationAttributes.get(), getAttributesFactory()); ReplaceProjectWithConfigurationNameRenderer dependenciesRenderer = new ReplaceProjectWithConfigurationNameRenderer(configurationName); DependencyGraphsRenderer dependencyGraphRenderer = new DependencyGraphsRenderer(output, renderer, rootRenderer, dependenciesRenderer); dependencyGraphRenderer.setShowSinglePath(showSinglePathToDependency); dependencyGraphRenderer.render(itemsToRender); dependencyGraphRenderer.complete(); } private void renderBuildScanHint(StyledTextOutput output) { output.println(); output.text("A web-based, searchable dependency report is available by adding the "); output.withStyle(UserInput).format("--%s", StartParameterBuildOptions.BuildScanOption.LONG_OPTION); output.println(" option."); } private void assertValidTaskConfiguration() { if (configurationName == null) { throw new InvalidUserDataException("Dependency insight report cannot be generated because the input configuration was not specified. " + "\nIt can be specified from the command line, e.g: '" + getPath() + " --configuration someConf --dependency someDep'"); } if (getDependencySpec() == null) { throw new InvalidUserDataException("Dependency insight report cannot be generated because the dependency to show was not specified." + "\nIt can be specified from the command line, e.g: '" + getPath() + " --dependency someDep'"); } } private Set selectDependencies(ResolvedComponentResult rootComponent) { final Set selectedDependencies = new LinkedHashSet<>(); eachDependency(rootComponent, dependencyResult -> { if (Objects.requireNonNull(getDependencySpec()).isSatisfiedBy(dependencyResult)) { selectedDependencies.add(dependencyResult); } }, new HashSet<>()); return selectedDependencies; } private void eachDependency( ResolvedComponentResult node, Action dependencyAction, Set visited ) { if (!visited.add(node)) { return; } for (DependencyResult d : node.getDependencies()) { dependencyAction.execute(d); if (d instanceof ResolvedDependencyResult) { eachDependency(((ResolvedDependencyResult) d).getSelected(), dependencyAction, visited); } } } private AttributeMatchDetails match(Attribute actualAttribute, @Nullable Object actualValue, AttributeContainer requestedAttributes) { for (Attribute requested : requestedAttributes.keySet()) { Object requestedValue = requestedAttributes.getAttribute(requested); if (requested.getName().equals(actualAttribute.getName())) { // found an attribute with the same name, but they do not necessarily have the same type if (requested.equals(actualAttribute)) { if (Objects.equals(actualValue, requestedValue)) { return new AttributeMatchDetails(MatchType.EQUAL, requested, requestedValue); } } else { // maybe it matched through coercion Object actualString = actualValue != null ? actualValue.toString() : null; Object requestedString = requestedValue != null ? requestedValue.toString() : null; if (Objects.equals(actualString, requestedString)) { return new AttributeMatchDetails(MatchType.EQUAL, requested, requestedValue); } } // TODO check for COMPATIBLE here, in a way compatible with configuration cache. // The branch ot/captchalogue/dependency-insights-compatibility-logging has the original code that isn't CC compatible. return new AttributeMatchDetails(MatchType.INCOMPATIBLE, requested, requestedValue); } } return new AttributeMatchDetails(MatchType.NOT_REQUESTED, null, null); } private static final class RootDependencyRenderer implements NodeRenderer { private final DependencyInsightReportTask task; private final AttributeContainer configurationAttributes; private final ImmutableAttributesFactory attributesFactory; public RootDependencyRenderer(DependencyInsightReportTask task, AttributeContainer configurationAttributes, ImmutableAttributesFactory attributesFactory) { this.task = task; this.configurationAttributes = configurationAttributes; this.attributesFactory = attributesFactory; } @Override public void renderNode(StyledTextOutput out, RenderableDependency dependency, boolean alreadyRendered) { out.withStyle(Identifier).text(dependency.getName()); if (StringUtils.isNotEmpty(dependency.getDescription())) { out.withStyle(Description).text(" (" + dependency.getDescription() + ")"); } switch (dependency.getResolutionState()) { case FAILED: out.withStyle(Failure).text(" FAILED"); break; case RESOLVED: case RESOLVED_CONSTRAINT: break; case UNRESOLVED: out.withStyle(Failure).text(" (n)"); break; } printVariantDetails(out, dependency); printExtraDetails(out, dependency); } private void printExtraDetails(StyledTextOutput out, RenderableDependency dependency) { List
extraDetails = dependency.getExtraDetails(); if (!extraDetails.isEmpty()) { printSections(out, extraDetails, 1); } } private void printSections(StyledTextOutput out, List
extraDetails, int depth) { for (Section extraDetail : extraDetails) { printSection(out, extraDetail, depth); printSections(out, extraDetail.getChildren(), depth + 1); } } private void printSection(StyledTextOutput out, Section extraDetail, int depth) { out.println(); String indent = StringUtils.leftPad("", 3 * depth) + (depth > 1 ? "- " : ""); String appendix = extraDetail.getChildren().isEmpty() ? "" : ":"; String description = StringUtils.trim(extraDetail.getDescription()); String padding = "\n" + StringUtils.leftPad("", indent.length()); description = description.replaceAll("(?m)(\r?\n)", padding); out.withStyle(Description).text(indent + description + appendix); } private void printVariantDetails(StyledTextOutput out, RenderableDependency dependency) { if (dependency.getResolvedVariants().isEmpty() && dependency.getAllVariants().isEmpty()) { return; } Set selectedVariantNames = dependency.getResolvedVariants() .stream() .map(ResolvedVariantResult::getDisplayName) .collect(Collectors.toSet()); if (task.getShowingAllVariants().get()) { out.style(Header); out.println(); out.text("-------------------").println(); out.text("Selected Variant(s)").println(); out.text("-------------------"); out.style(Normal); out.println(); } for (ResolvedVariantResult variant : dependency.getResolvedVariants()) { printVariant(out, dependency, variant, true); } if (task.getShowingAllVariants().get()) { out.style(Header); out.println(); out.println(); out.text("---------------------").println(); out.text("Unselected Variant(s)").println(); out.text("---------------------"); out.println(); out.style(Normal); for (ResolvedVariantResult variant : dependency.getAllVariants()) { if (selectedVariantNames.contains(variant.getDisplayName())) { continue; } // Currently, since the compatibility column is unusable, pass true for selected to prevent its output. printVariant(out, dependency, variant, true); out.println(); } } } private void printVariant( StyledTextOutput out, RenderableDependency dependency, ResolvedVariantResult variant, boolean selected ) { AttributeContainer attributes = variant.getAttributes(); AttributeContainer requested = getRequestedAttributes(dependency); AttributeBuckets buckets = bucketAttributes(attributes, requested); out.println().style(Normal).text(" Variant "); // For now, do not style -- see ot/captchalogue/dependency-insights-compatibility-logging for the original styling choices out.text(variant.getDisplayName()).style(Normal).text(":").println(); if (!attributes.isEmpty() || !requested.isEmpty()) { writeAttributeBlock(out, attributes, requested, buckets, selected); } } private AttributeContainer getRequestedAttributes(RenderableDependency dependency) { if (dependency instanceof HasAttributes) { AttributeContainer dependencyAttributes = ((HasAttributes) dependency).getAttributes(); return concat(configurationAttributes, dependencyAttributes); } return configurationAttributes; } private AttributeContainer concat(AttributeContainer configAttributes, AttributeContainer dependencyAttributes) { return attributesFactory.concat( ((AttributeContainerInternal) configAttributes).asImmutable(), ((AttributeContainerInternal) dependencyAttributes).asImmutable() ); } private void writeAttributeBlock( StyledTextOutput out, AttributeContainer attributes, AttributeContainer requested, AttributeBuckets buckets, boolean selected ) { new StyledTable.Renderer().render( createAttributeTable(attributes, requested, buckets, selected), out ); } private static final class AttributeBuckets { @SuppressWarnings("checkstyle:constantname") private static final Comparator> sortedByAttributeName = Comparator.comparing(Attribute::getName); Set> providedAttributes = new TreeSet<>(sortedByAttributeName); Map, AttributeMatchDetails> bothAttributes = new TreeMap<>(sortedByAttributeName); Set> requestedAttributes = new TreeSet<>(sortedByAttributeName); } private StyledTable createAttributeTable( AttributeContainer attributes, AttributeContainer requested, AttributeBuckets buckets, boolean selected ) { ImmutableList.Builder header = ImmutableList.builder() .add("Attribute Name", "Provided", "Requested"); if (!selected) { header.add("Compatibility"); } ImmutableList rows = buildRows(attributes, requested, buckets, selected); return new StyledTable(Strings.repeat(" ", 4), header.build(), rows); } private ImmutableList buildRows( AttributeContainer attributes, AttributeContainer requested, AttributeBuckets buckets, boolean selected ) { ImmutableList.Builder rows = ImmutableList.builder(); for (Attribute attribute : buckets.providedAttributes) { rows.add(createProvidedRow(attributes, selected, attribute)); } for (Map.Entry, AttributeMatchDetails> entry : buckets.bothAttributes.entrySet()) { rows.add(createMatchBasedRow(attributes, selected, entry)); } for (Attribute attribute : buckets.requestedAttributes) { rows.add(createRequestedRow(requested, selected, attribute)); } return rows.build(); } private AttributeBuckets bucketAttributes(AttributeContainer attributes, AttributeContainer requested) { // Bucket attributes into three groups: // 1. Attributes that are only in the variant // 2. Attributes that are both in the variant and requested by the configuration // 3. Attributes that are only in the requested configuration AttributeBuckets buckets = new AttributeBuckets(); for (Attribute attribute : attributes.keySet()) { AttributeMatchDetails details = task.match(attribute, attributes.getAttribute(attribute), requested); if (details.matchType() != MatchType.NOT_REQUESTED) { buckets.bothAttributes.put(attribute, details); } else { buckets.providedAttributes.add(attribute); } } for (Attribute attribute : requested.keySet()) { // If it's not in the matches, it's only in the requested attributes if (buckets.bothAttributes.values().stream().map(AttributeMatchDetails::requested).noneMatch(Predicate.isEqual(attribute))) { buckets.requestedAttributes.add(attribute); } } return buckets; } private StyledTable.Row createProvidedRow(AttributeContainer attributes, boolean selected, Attribute attribute) { Object providedValue = attributes.getAttribute(attribute); ImmutableList.Builder text = ImmutableList.builder() .add( attribute.getName(), providedValue == null ? "" : providedValue.toString(), "" ); if (!selected) { text.add("Compatible"); } return new StyledTable.Row(text.build(), Info); } private StyledTable.Row createMatchBasedRow(AttributeContainer attributes, boolean selected, Map.Entry, AttributeMatchDetails> entry) { Object providedValue = attributes.getAttribute(entry.getKey()); AttributeMatchDetails match = entry.getValue(); ImmutableList.Builder text = ImmutableList.builder() .add( entry.getKey().getName(), providedValue == null ? "" : providedValue.toString(), String.valueOf(entry.getValue().requestedValue()) ); if (!selected) { text.add(match.matchType() == MatchType.INCOMPATIBLE ? "Incompatible" : "Compatible"); } // For now, do not style -- see ot/captchalogue/dependency-insights-compatibility-logging for the original styling choices return new StyledTable.Row(text.build(), Normal); } private StyledTable.Row createRequestedRow(AttributeContainer requested, boolean selected, Attribute attribute) { Object requestedValue = requested.getAttribute(attribute); ImmutableList.Builder text = ImmutableList.builder() .add( attribute.getName(), "", String.valueOf(requestedValue) ); if (!selected) { text.add("Compatible"); } return new StyledTable.Row(text.build(), Info); } } private static class ReplaceProjectWithConfigurationNameRenderer implements NodeRenderer { private final String configurationName; public ReplaceProjectWithConfigurationNameRenderer(String configurationName) { this.configurationName = configurationName; } @Override public void renderNode(StyledTextOutput target, RenderableDependency node, boolean alreadyRendered) { boolean leaf = node.getChildren().isEmpty(); target.text(leaf ? configurationName : node.getName()); if (node.getDescription() != null) { target.text(" ").withStyle(Description).text(node.getDescription()); } if (alreadyRendered && !leaf) { target.withStyle(Info).text(" (*)"); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy