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

software.amazon.smithy.build.SmithyBuildImpl Maven / Gradle / Ivy

/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.build;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import software.amazon.smithy.build.model.ProjectionConfig;
import software.amazon.smithy.build.model.SmithyBuildConfig;
import software.amazon.smithy.build.model.TransformConfig;
import software.amazon.smithy.build.transforms.Apply;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.ModelAssembler;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.SmithyBuilder;

final class SmithyBuildImpl {
    private static final Logger LOGGER = Logger.getLogger(SmithyBuild.class.getName());
    private static final Pattern PATTERN = Pattern.compile("^[A-Za-z0-9\\-_.]+$");

    private final SmithyBuildConfig config;
    private final Function fileManifestFactory;
    private final Supplier modelAssemblerSupplier;
    private final Path outputDirectory;
    private final Map>> transformers = new HashMap<>();
    private final ModelTransformer modelTransformer;
    private final Function> transformFactory;
    private final Function> pluginFactory;
    private final Model model;
    private final ClassLoader pluginClassLoader;
    private final Set sources;
    private final Predicate projectionFilter;
    private final Predicate pluginFilter;

    SmithyBuildImpl(SmithyBuild builder) {
        config = prepareConfig(SmithyBuilder.requiredState("config", builder.config));
        sources = builder.sources;
        fileManifestFactory = builder.fileManifestFactory != null
                ? builder.fileManifestFactory
                : FileManifest::create;
        modelAssemblerSupplier = builder.modelAssemblerSupplier != null
                ? builder.modelAssemblerSupplier
                : Model::assembler;
        modelTransformer = builder.modelTransformer != null
                ? builder.modelTransformer
                : ModelTransformer.create();
        transformFactory = builder.transformFactory != null
                ? builder.transformFactory
                : ProjectionTransformer.createServiceFactory(getClass().getClassLoader());
        pluginFactory = builder.pluginFactory != null
                ? builder.pluginFactory
                : SmithyBuildPlugin.createServiceFactory(getClass().getClassLoader());
        model = builder.model != null
                ? builder.model
                : Model.builder().build();

        if (builder.outputDirectory != null) {
            outputDirectory = builder.outputDirectory;
        } else if (config.getOutputDirectory().isPresent()) {
            outputDirectory = Paths.get(config.getOutputDirectory().get());
        } else {
            // Default the output directory to the current working directory + "./build/smithy"
            outputDirectory = Paths.get(".").toAbsolutePath().normalize().resolve("build").resolve("smithy");
        }

        // Create the transformers for each projection.
        config.getProjections().forEach((projectionName, projectionConfig) -> {
            transformers.put(projectionName, createTransformers(projectionName, projectionConfig));
        });

        pluginClassLoader = builder.pluginClassLoader;
        projectionFilter = builder.projectionFilter;
        pluginFilter = builder.pluginFilter;
    }

    private static SmithyBuildConfig prepareConfig(SmithyBuildConfig config) {
        // If we don't have a source projection specified, supply one.
        if (!config.getProjections().containsKey("source")) {
            Map projections = new HashMap<>(config.getProjections());
            projections.put("source", ProjectionConfig.builder().build());
            config = config.toBuilder().projections(projections).build();
        }

        // The `source` projection cannot include mappers or filters.
        ProjectionConfig sourceProjection = config.getProjections().get("source");
        if (!sourceProjection.getTransforms().isEmpty()) {
            throw new SmithyBuildException("The source projection cannot contain any transforms");
        }

        config.getPlugins().keySet().forEach(p -> validatePluginName("[top-level]", p));

        for (Map.Entry entry : config.getProjections().entrySet()) {
            String projectionName = entry.getKey();
            if (!PATTERN.matcher(projectionName).matches()) {
                throw new SmithyBuildException(String.format("Invalid Smithy build projection name `%s`. "
                        + "Projection names must match the following regex: %s", projectionName, PATTERN));
            }
            entry.getValue().getPlugins().keySet().forEach(p -> validatePluginName(entry.getKey(), p));
            entry.getValue().getTransforms().forEach(t -> validateTransformName(entry.getKey(), t.getName()));
        }

        return config;
    }

    private static void validateTransformName(String projection, String transformName) {
        if (!PATTERN.matcher(transformName).matches()) {
            throw new SmithyBuildException(String.format("Invalid transform name `%s` found in the `%s` projection. "
                    + " Transform names must match the following regex: %s", transformName, projection, PATTERN));
        }
    }

    private static void validatePluginName(String projection, String plugin) {
        if (!PATTERN.matcher(plugin).matches()) {
            throw new SmithyBuildException(String.format(
                    "Invalid plugin name `%s` found in the `%s` projection. "
                    + " Plugin names must match the following regex: %s", plugin, projection, PATTERN));
        }
    }

    void applyAllProjections(
            Consumer projectionResultConsumer,
            BiConsumer projectionExceptionConsumer
    ) {
        Model resolvedModel = createBaseModel();

        // The projections are being split up here because we need to be able
        // to break out non-parallelizeable plugins. Right now the only
        // parallelization that occurs is at the projection level.
        List> parallelProjections = new ArrayList<>();
        List parallelProjectionNameOrder = new ArrayList<>();

        for (Map.Entry entry : config.getProjections().entrySet()) {
            String name = entry.getKey();
            ProjectionConfig config = entry.getValue();

            if (config.isAbstract() || !projectionFilter.test(name)) {
                continue;
            }

            // Check to see if any of the plugins in the projection require the projection be run serially
            boolean isSerial = resolvePlugins(config).keySet().stream().anyMatch(pluginName -> {
                Optional plugin = pluginFactory.apply(pluginName);
                return plugin.isPresent() && plugin.get().isSerial();
            });

            if (isSerial) {
                executeSerialProjection(resolvedModel, name, config,
                                        projectionResultConsumer, projectionExceptionConsumer);
            } else {
                parallelProjectionNameOrder.add(name);
                parallelProjections.add(() -> {
                    executeSerialProjection(resolvedModel, name, config,
                                            projectionResultConsumer, projectionExceptionConsumer);
                    return null;
                });
            }
        }

        // Common case of only executing a single plugin per/projection.
        if (parallelProjections.size() == 1) {
            try {
                parallelProjections.get(0).call();
            } catch (Throwable e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                } else {
                    throw new RuntimeException(e);
                }
            }
        } else if (!parallelProjections.isEmpty()) {
            executeParallelProjections(parallelProjections, parallelProjectionNameOrder, projectionExceptionConsumer);
        }
    }

    private void executeSerialProjection(
            Model resolvedModel,
            String name,
            ProjectionConfig config,
            Consumer projectionResultConsumer,
            BiConsumer projectionExceptionConsumer
    ) {
        // Errors that occur while invoking the result callback must not
        // cause the exception callback to be invoked.
        ProjectionResult result = null;

        try {
            result = applyProjection(name, config, resolvedModel);
        } catch (Throwable e) {
            projectionExceptionConsumer.accept(name, e);
        }

        if (result != null) {
            projectionResultConsumer.accept(result);
        }
    }

    private void executeParallelProjections(
            List> parallelProjections,
            List parallelProjectionNameOrder,
            BiConsumer projectionExceptionConsumer
    ) {
        ExecutorService executor = ForkJoinPool.commonPool();

        try {
            List> futures = executor.invokeAll(parallelProjections);
            // Futures are returned in the same order they were added, so
            // use the list of ordered names to know which projections failed.
            for (int i = 0; i < futures.size(); i++) {
                try {
                    futures.get(i).get();
                } catch (ExecutionException e) {
                    Throwable cause = e.getCause() != null ? e.getCause() : e;
                    String failedProjectionName = parallelProjectionNameOrder.get(i);
                    projectionExceptionConsumer.accept(failedProjectionName, cause);
                }
            }
        } catch (InterruptedException e) {
            throw new SmithyBuildException(e.getMessage(), e);
        }
    }

    private Model createBaseModel() {
        Model resolvedModel = model;

        if (!config.getImports().isEmpty()) {
            LOGGER.fine(() -> "Merging the following imports into the loaded model: " + config.getImports());
            ModelAssembler assembler = modelAssemblerSupplier.get().addModel(model);
            config.getImports().forEach(assembler::addImport);
            resolvedModel = assembler.assemble().unwrap();
        }

        return resolvedModel;
    }

    private ProjectionResult applyProjection(String projectionName, ProjectionConfig projection, Model resolvedModel) {
        LOGGER.fine(() -> String.format("Creating the `%s` projection", projectionName));

        // Resolve imports.
        if (!projection.getImports().isEmpty()) {
            LOGGER.fine(() -> String.format(
                    "Merging the following `%s` projection imports into the loaded model: %s",
                    projectionName, projection.getImports()));
            ModelAssembler assembler = modelAssemblerSupplier.get().addModel(resolvedModel);
            projection.getImports().forEach(assembler::addImport);
            ValidatedResult resolvedResult = assembler.assemble();

            // Fail if the model can't be merged with the imports.
            if (!resolvedResult.getResult().isPresent()) {
                LOGGER.severe(String.format(
                        "The model could not be merged with the following imports: [%s]",
                        projection.getImports()));
                return ProjectionResult.builder()
                        .projectionName(projectionName)
                        .events(resolvedResult.getValidationEvents())
                        .build();
            }

            resolvedModel = resolvedResult.unwrap();
        }

        // Create the base directory where all projection artifacts are stored.
        Path baseProjectionDir = outputDirectory.resolve(projectionName);

        // Transform the model and collect the results.
        Model projectedModel = applyProjectionTransforms(
                resolvedModel, resolvedModel, projectionName, Collections.emptySet());

        ValidatedResult modelResult = modelAssemblerSupplier.get().addModel(projectedModel).assemble();

        ProjectionResult.Builder resultBuilder = ProjectionResult.builder()
                .projectionName(projectionName)
                .model(projectedModel)
                .events(modelResult.getValidationEvents());

        for (Map.Entry entry : resolvePlugins(projection).entrySet()) {
            if (pluginFilter.test(entry.getKey())) {
                applyPlugin(projectionName, projection, baseProjectionDir, entry.getKey(), entry.getValue(),
                            projectedModel, resolvedModel, modelResult, resultBuilder);
            }
        }

        return resultBuilder.build();
    }

    private Model applyProjectionTransforms(
            Model inputModel,
            Model originalModel,
            String projectionName,
            Set visited
    ) {
        // Transform the model and collect the results.
        Model projectedModel = inputModel;

        for (Pair transformerBinding : transformers.get(projectionName)) {
            TransformContext context = TransformContext.builder()
                    .model(projectedModel)
                    .originalModel(originalModel)
                    .transformer(modelTransformer)
                    .projectionName(projectionName)
                    .sources(sources)
                    .settings(transformerBinding.left)
                    .visited(visited)
                    .build();
            projectedModel = transformerBinding.right.transform(context);
        }

        return projectedModel;
    }

    private void applyPlugin(
            String projectionName,
            ProjectionConfig projection,
            Path baseProjectionDir,
            String pluginName,
            ObjectNode pluginSettings,
            Model projectedModel,
            Model resolvedModel,
            ValidatedResult modelResult,
            ProjectionResult.Builder resultBuilder
    ) {
        // Create the manifest where plugin artifacts are stored.
        Path pluginBaseDir = baseProjectionDir.resolve(pluginName);
        FileManifest manifest = fileManifestFactory.apply(pluginBaseDir);

        // Find the desired plugin in the SPI found plugins.
        SmithyBuildPlugin resolved = pluginFactory.apply(pluginName).orElse(null);

        if (resolved == null) {
            LOGGER.info(() -> String.format(
                    "Unable to find a plugin for `%s` in the `%s` projection",
                    pluginName, projectionName));
        } else if (resolved.requiresValidModel() && modelResult.isBroken()) {
            LOGGER.fine(() -> String.format(
                    "Skipping `%s` plugin for `%s` projection because the model is broken",
                    pluginName, projectionName));
        } else {
            LOGGER.info(() -> String.format(
                    "Applying `%s` plugin to `%s` projection",
                    pluginName, projectionName));
            resolved.execute(PluginContext.builder()
                    .model(projectedModel)
                    .originalModel(resolvedModel)
                    .projection(projectionName, projection)
                    .events(modelResult.getValidationEvents())
                    .settings(pluginSettings)
                    .fileManifest(manifest)
                    .pluginClassLoader(pluginClassLoader)
                    .sources(sources)
                    .build());
            resultBuilder.addPluginManifest(pluginName, manifest);
        }
    }

    private Map resolvePlugins(ProjectionConfig projection) {
        Map result = new TreeMap<>(config.getPlugins());
        result.putAll(projection.getPlugins());
        return result;
    }

    // Creates pairs where the left value is the configuration arguments of the
    // transformer, and the right value is the instantiated transformer.
    private List> createTransformers(
            String projectionName,
            ProjectionConfig config
    ) {
        List> resolved = new ArrayList<>(config.getTransforms().size());

        for (TransformConfig transformConfig : config.getTransforms()) {
            String name = transformConfig.getName();

            if (name.equals("apply")) {
                resolved.add(createApplyTransformer(projectionName, transformConfig));
                continue;
            }

            ProjectionTransformer transformer = transformFactory.apply(name)
                    .orElseThrow(() -> new UnknownTransformException(String.format(
                            "Unable to find a transform for `%s` in the `%s` projection.", name, projectionName)));
            resolved.add(Pair.of(transformConfig.getArgs(), transformer));
        }

        return resolved;
    }

    // This is a somewhat hacky special case that allows the "apply" transform to apply
    // transformations defined on other projections.
    private Pair createApplyTransformer(
            String projectionName,
            TransformConfig transformConfig
    ) {
        Apply.ApplyCallback callback = (currentModel, projectionTarget, visited) -> {
            if (projectionTarget.equals(projectionName)) {
                throw new SmithyBuildException(
                        "Cannot recursively apply the same projection: " + projectionName);
            } else if (visited.contains(projectionTarget)) {
                visited.add(projectionTarget);
                throw new SmithyBuildException(String.format(
                        "Cycle found in apply transforms: %s -> ...", String.join(" -> ", visited)));
            } else if (!transformers.containsKey(projectionTarget)) {
                throw new UnknownProjectionException(String.format(
                        "Unable to find projection named `%s` referenced by `%s`",
                        projectionTarget, projectionName));
            }
            Set updatedVisited = new LinkedHashSet<>(visited);
            updatedVisited.add(projectionTarget);
            return applyProjectionTransforms(currentModel, model, projectionTarget, updatedVisited);
        };

        return Pair.of(transformConfig.getArgs(), new Apply(callback));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy