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

org.openrewrite.config.YamlResourceLoader Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 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 *

* https://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.openrewrite.config; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import lombok.Getter; import org.intellij.lang.annotations.Language; import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.internal.PropertyPlaceholderHelper; import org.openrewrite.style.NamedStyles; import org.openrewrite.style.Style; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.function.Consumer; import java.util.stream.Stream; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.openrewrite.RecipeSerializer.maybeAddKotlinModule; import static org.openrewrite.Tree.randomId; import static org.openrewrite.Validated.invalid; public class YamlResourceLoader implements ResourceLoader { int refCount = 0; private static final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}", ":"); private final URI source; private final String yamlSource; private final ObjectMapper mapper; @Nullable private final ClassLoader classLoader; private final Collection dependencyResourceLoaders; @Nullable private Map> contributors; @Nullable private Map> recipeNameToExamples; @Getter private enum ResourceType { Recipe("specs.openrewrite.org/v1beta/recipe"), Style("specs.openrewrite.org/v1beta/style"), Category("specs.openrewrite.org/v1beta/category"), Example("specs.openrewrite.org/v1beta/example"), Attribution("specs.openrewrite.org/v1beta/attribution"); private final String spec; ResourceType(String spec) { this.spec = spec; } public static @Nullable ResourceType fromSpec(@Nullable String spec) { return Arrays.stream(values()) .filter(type -> type.getSpec().equals(spec)) .findAny() .orElse(null); } } /** * Load a declarative recipe using the runtime classloader * * @param yamlInput Declarative recipe yaml input stream * @param source Declarative recipe source * @param properties Placeholder properties * @throws UncheckedIOException On unexpected IOException */ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properties) throws UncheckedIOException { this(yamlInput, source, properties, null); } /** * Load a declarative recipe, optionally using the specified classloader * * @param yamlInput Declarative recipe yaml input stream * @param source Declarative recipe source * @param properties Placeholder properties * @param classLoader Optional classloader to use with jackson. If not specified, the runtime classloader will be used. * @throws UncheckedIOException On unexpected IOException */ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properties, @Nullable ClassLoader classLoader) throws UncheckedIOException { this(yamlInput, source, properties, classLoader, emptyList()); } /** * Load a declarative recipe, optionally using the specified classloader and optionally including resource loaders * for recipes from dependencies. * * @param yamlInput Declarative recipe yaml input stream * @param source Declarative recipe source * @param properties Placeholder properties * @param classLoader Optional classloader to use with jackson. If not specified, the runtime classloader will be used. * @param dependencyResourceLoaders Optional resource loaders for recipes from dependencies * @throws UncheckedIOException On unexpected IOException */ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properties, @Nullable ClassLoader classLoader, Collection dependencyResourceLoaders) throws UncheckedIOException { this(yamlInput, source, properties, classLoader, dependencyResourceLoaders, jsonMapper -> { }); } /** * Load a declarative recipe, optionally using the specified classloader and optionally including resource loaders * for recipes from dependencies. * * @param yamlInput Declarative recipe yaml input stream * @param source Declarative recipe source * @param properties Placeholder properties * @param classLoader Optional classloader to use with jackson. If not specified, the runtime classloader will be used. * @param dependencyResourceLoaders Optional resource loaders for recipes from dependencies * @param mapperCustomizer Customizer for the ObjectMapper * @throws UncheckedIOException On unexpected IOException */ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properties, @Nullable ClassLoader classLoader, Collection dependencyResourceLoaders, Consumer mapperCustomizer) { this.source = source; this.dependencyResourceLoaders = dependencyResourceLoaders; mapper = JsonMapper.builder() .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) .build() .registerModule(new ParameterNamesModule()) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapperCustomizer.accept(mapper); maybeAddKotlinModule(mapper); this.classLoader = classLoader; if (classLoader != null) { TypeFactory tf = TypeFactory.defaultInstance().withClassLoader(classLoader); mapper.setTypeFactory(tf); } try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int nRead; byte[] data = new byte[1024]; while ((nRead = yamlInput.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } this.yamlSource = propertyPlaceholderHelper.replacePlaceholders( new String(buffer.toByteArray(), StandardCharsets.UTF_8), properties); } catch (IOException e) { throw new UncheckedIOException(e); } } private Collection> loadResources(ResourceType resourceType) { Collection> resources = new ArrayList<>(); Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); for (Object resource : yaml.loadAll(yamlSource)) { if (resource instanceof Map) { @SuppressWarnings("unchecked") Map resourceMap = (Map) resource; if (resourceType.equals(ResourceType.fromSpec((String) resourceMap.get("type")))) { resources.add(resourceMap); } } } return resources; } @SuppressWarnings("unchecked") @Override public Collection listRecipes() { Collection> resources = loadResources(ResourceType.Recipe); List recipes = new ArrayList<>(resources.size()); Map> contributors = listContributors(); for (Map r : resources) { if (!r.containsKey("name")) { continue; } @Language("markdown") String name = (String) r.get("name"); @Language("markdown") String displayName = (String) r.get("displayName"); if (displayName == null) { displayName = name; } @Language("markdown") String description = (String) r.get("description"); Set tags = Collections.emptySet(); List rawTags = (List) r.get("tags"); if (rawTags != null) { tags = new HashSet<>(rawTags); } String estimatedEffortPerOccurrenceStr = (String) r.get("estimatedEffortPerOccurrence"); Duration estimatedEffortPerOccurrence = null; if (estimatedEffortPerOccurrenceStr != null) { estimatedEffortPerOccurrence = Duration.parse(estimatedEffortPerOccurrenceStr); } List rawMaintainers = (List) r.getOrDefault("maintainers", emptyList()); List maintainers; if (rawMaintainers.isEmpty()) { maintainers = emptyList(); } else { maintainers = new ArrayList<>(rawMaintainers.size()); for (Object rawMaintainer : rawMaintainers) { if (rawMaintainer instanceof Map) { Map maintainerMap = (Map) rawMaintainer; String maintainerName = (String) maintainerMap.get("maintainer"); String logoString = (String) maintainerMap.get("logo"); URI logo = (logoString == null) ? null : URI.create(logoString); maintainers.add(new Maintainer(maintainerName, logo)); } } } DeclarativeRecipe recipe = new DeclarativeRecipe( name, displayName, description, tags, estimatedEffortPerOccurrence, source, (boolean) r.getOrDefault("causesAnotherCycle", false), maintainers); List recipeList = (List) r.get("recipeList"); if (recipeList == null) { throw new RecipeException("Invalid Recipe [" + name + "] recipeList is null"); } for (int i = 0; i < recipeList.size(); i++) { loadRecipe( name, i, recipeList.get(i), recipe::addUninitialized, recipe::addUninitialized, recipe::addValidation); } List preconditions = (List) r.get("preconditions"); if (preconditions != null) { for (int i = 0; i < preconditions.size(); i++) { loadRecipe( name, i, preconditions.get(i), recipe::addUninitializedPrecondition, recipe::addUninitializedPrecondition, recipe::addValidation); } } recipe.setContributors(contributors.get(recipe.getName())); recipes.add(recipe); } return recipes; } @SuppressWarnings("unchecked") void loadRecipe(@Language("markdown") String name, int i, Object recipeData, Consumer addLazyLoadRecipe, Consumer addRecipe, Consumer> addValidation) { if (recipeData instanceof String) { String recipeName = (String) recipeData; try { // first try an explicitly-declared zero-arg constructor addRecipe.accept((Recipe) Class.forName(recipeName, true, classLoader == null ? this.getClass().getClassLoader() : classLoader) .getDeclaredConstructor() .newInstance()); } catch (ReflectiveOperationException e) { try { // then try jackson addRecipe.accept(instantiateRecipe(recipeName, new HashMap<>())); } catch (IllegalArgumentException ignored) { // else, it's probably declarative addLazyLoadRecipe.accept(recipeName); } } catch (NoClassDefFoundError e) { addInvalidRecipeValidation( addValidation, recipeName, null, "Recipe class " + recipeName + " cannot be found"); } } else if (recipeData instanceof Map) { Map.Entry nameAndConfig = ((Map) recipeData).entrySet().iterator().next(); String recipeName = nameAndConfig.getKey(); Object recipeArgs = nameAndConfig.getValue(); try { if (recipeArgs instanceof Map) { try { addRecipe.accept(instantiateRecipe(recipeName, (Map) recipeArgs)); } catch (IllegalArgumentException e) { if (e.getCause() instanceof InvalidTypeIdException) { addInvalidRecipeValidation( addValidation, recipeName, recipeArgs, "Recipe class " + recipeName + " cannot be found"); } else { addInvalidRecipeValidation( addValidation, recipeName, recipeArgs, "Unable to load Recipe: " + e); } } catch (NoClassDefFoundError e) { addInvalidRecipeValidation( addValidation, recipeName, recipeArgs, "Recipe class " + nameAndConfig.getKey() + " cannot be found"); } } else { addInvalidRecipeValidation( addValidation, recipeName, recipeArgs, "Declarative recipeList entries are expected to be strings or mappings"); } } catch (Exception e) { addInvalidRecipeValidation( addValidation, recipeName, recipeArgs, "Unexpected declarative recipe parsing exception " + e.getClass().getName()); } } else { addValidation.accept(invalid( name + ".recipeList[" + i + "] (in " + source + ")", recipeData, "is an object type that isn't recognized as a recipe.", null)); } } private Recipe instantiateRecipe(String recipeName, Map args) throws IllegalArgumentException { Map withJsonType = new HashMap<>(args); withJsonType.put("@c", recipeName); return mapper.convertValue(withJsonType, Recipe.class); } private void addInvalidRecipeValidation(Consumer> addValidation, String recipeName, Object recipeArgs, String message) { addValidation.accept(Validated.invalid(recipeName, recipeArgs, message)); } @Override public Collection listRecipeDescriptors() { return listRecipeDescriptors(emptyList(), listContributors(), listRecipeExamples()); } public Collection listRecipeDescriptors(Collection externalRecipes, Map> recipeNamesToContributors, Map> recipeNamesToExamples) { Collection internalRecipes = listRecipes(); Collection allRecipes = Stream.concat( Stream.concat( externalRecipes.stream(), internalRecipes.stream() ), dependencyResourceLoaders.stream().flatMap(rl -> rl.listRecipes().stream()) ).collect(toList()); List recipeDescriptors = new ArrayList<>(); for (Recipe recipe : internalRecipes) { DeclarativeRecipe declarativeRecipe = (DeclarativeRecipe) recipe; declarativeRecipe.initialize(allRecipes, recipeNamesToContributors); declarativeRecipe.setContributors(recipeNamesToContributors.get(recipe.getName())); declarativeRecipe.setExamples(recipeNamesToExamples.get(recipe.getName())); recipeDescriptors.add(declarativeRecipe.getDescriptor()); } return recipeDescriptors; } @SuppressWarnings("unchecked") @Override public Collection listStyles() { return loadResources(ResourceType.Style).stream() .filter(r -> r.containsKey("name")) .map(s -> { List