001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.cli.compiler; 018 019import java.net.MalformedURLException; 020import java.net.URI; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029 030import groovy.grape.Grape; 031import org.apache.maven.model.Dependency; 032import org.apache.maven.model.Model; 033import org.apache.maven.model.Parent; 034import org.apache.maven.model.Repository; 035import org.apache.maven.model.building.DefaultModelBuilder; 036import org.apache.maven.model.building.DefaultModelBuilderFactory; 037import org.apache.maven.model.building.DefaultModelBuildingRequest; 038import org.apache.maven.model.building.ModelSource; 039import org.apache.maven.model.building.UrlModelSource; 040import org.apache.maven.model.resolution.InvalidRepositoryException; 041import org.apache.maven.model.resolution.ModelResolver; 042import org.apache.maven.model.resolution.UnresolvableModelException; 043import org.codehaus.groovy.ast.ASTNode; 044import org.codehaus.groovy.ast.AnnotationNode; 045import org.codehaus.groovy.ast.expr.ConstantExpression; 046import org.codehaus.groovy.ast.expr.Expression; 047import org.codehaus.groovy.ast.expr.ListExpression; 048import org.codehaus.groovy.control.messages.Message; 049import org.codehaus.groovy.control.messages.SyntaxErrorMessage; 050import org.codehaus.groovy.syntax.SyntaxException; 051import org.codehaus.groovy.transform.ASTTransformation; 052 053import org.springframework.boot.cli.compiler.dependencies.MavenModelDependencyManagement; 054import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext; 055import org.springframework.boot.groovy.DependencyManagementBom; 056import org.springframework.core.Ordered; 057import org.springframework.core.annotation.Order; 058 059/** 060 * {@link ASTTransformation} for processing {@link DependencyManagementBom} annotations. 061 * 062 * @author Andy Wilkinson 063 * @since 1.3.0 064 */ 065@Order(DependencyManagementBomTransformation.ORDER) 066@SuppressWarnings("deprecation") 067public class DependencyManagementBomTransformation 068 extends AnnotatedNodeASTTransformation { 069 070 /** 071 * The order of the transformation. 072 */ 073 public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 100; 074 075 private static final Set<String> DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES = Collections 076 .unmodifiableSet( 077 new HashSet<>(Arrays.asList(DependencyManagementBom.class.getName(), 078 DependencyManagementBom.class.getSimpleName()))); 079 080 private final DependencyResolutionContext resolutionContext; 081 082 public DependencyManagementBomTransformation( 083 DependencyResolutionContext resolutionContext) { 084 super(DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES, true); 085 this.resolutionContext = resolutionContext; 086 } 087 088 @Override 089 protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) { 090 if (!annotationNodes.isEmpty()) { 091 if (annotationNodes.size() > 1) { 092 for (AnnotationNode annotationNode : annotationNodes) { 093 handleDuplicateDependencyManagementBomAnnotation(annotationNode); 094 } 095 } 096 else { 097 processDependencyManagementBomAnnotation(annotationNodes.get(0)); 098 } 099 } 100 } 101 102 private void processDependencyManagementBomAnnotation(AnnotationNode annotationNode) { 103 Expression valueExpression = annotationNode.getMember("value"); 104 List<Map<String, String>> bomDependencies = createDependencyMaps(valueExpression); 105 updateDependencyResolutionContext(bomDependencies); 106 } 107 108 private List<Map<String, String>> createDependencyMaps(Expression valueExpression) { 109 Map<String, String> dependency = null; 110 List<ConstantExpression> constantExpressions = getConstantExpressions( 111 valueExpression); 112 List<Map<String, String>> dependencies = new ArrayList<>( 113 constantExpressions.size()); 114 for (ConstantExpression expression : constantExpressions) { 115 Object value = expression.getValue(); 116 if (value instanceof String) { 117 String[] components = ((String) expression.getValue()).split(":"); 118 if (components.length == 3) { 119 dependency = new HashMap<>(); 120 dependency.put("group", components[0]); 121 dependency.put("module", components[1]); 122 dependency.put("version", components[2]); 123 dependency.put("type", "pom"); 124 dependencies.add(dependency); 125 } 126 else { 127 handleMalformedDependency(expression); 128 } 129 } 130 } 131 return dependencies; 132 } 133 134 private List<ConstantExpression> getConstantExpressions(Expression valueExpression) { 135 if (valueExpression instanceof ListExpression) { 136 return getConstantExpressions((ListExpression) valueExpression); 137 } 138 if (valueExpression instanceof ConstantExpression 139 && ((ConstantExpression) valueExpression).getValue() instanceof String) { 140 return Arrays.asList((ConstantExpression) valueExpression); 141 } 142 reportError("@DependencyManagementBom requires an inline constant that is a " 143 + "string or a string array", valueExpression); 144 return Collections.emptyList(); 145 } 146 147 private List<ConstantExpression> getConstantExpressions( 148 ListExpression valueExpression) { 149 List<ConstantExpression> expressions = new ArrayList<>(); 150 for (Expression expression : valueExpression.getExpressions()) { 151 if (expression instanceof ConstantExpression 152 && ((ConstantExpression) expression).getValue() instanceof String) { 153 expressions.add((ConstantExpression) expression); 154 } 155 else { 156 reportError( 157 "Each entry in the array must be an " + "inline string constant", 158 expression); 159 } 160 } 161 return expressions; 162 } 163 164 private void handleMalformedDependency(Expression expression) { 165 Message message = createSyntaxErrorMessage( 166 String.format( 167 "The string must be of the form \"group:module:version\"%n"), 168 expression); 169 getSourceUnit().getErrorCollector().addErrorAndContinue(message); 170 } 171 172 private void updateDependencyResolutionContext( 173 List<Map<String, String>> bomDependencies) { 174 URI[] uris = Grape.getInstance().resolve(null, 175 bomDependencies.toArray(new Map[0])); 176 DefaultModelBuilder modelBuilder = new DefaultModelBuilderFactory().newInstance(); 177 for (URI uri : uris) { 178 try { 179 DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); 180 request.setModelResolver(new GrapeModelResolver()); 181 request.setModelSource(new UrlModelSource(uri.toURL())); 182 request.setSystemProperties(System.getProperties()); 183 Model model = modelBuilder.build(request).getEffectiveModel(); 184 this.resolutionContext.addDependencyManagement( 185 new MavenModelDependencyManagement(model)); 186 } 187 catch (Exception ex) { 188 throw new IllegalStateException("Failed to build model for '" + uri 189 + "'. Is it a valid Maven bom?", ex); 190 } 191 } 192 } 193 194 private void handleDuplicateDependencyManagementBomAnnotation( 195 AnnotationNode annotationNode) { 196 Message message = createSyntaxErrorMessage( 197 "Duplicate @DependencyManagementBom annotation. It must be declared at most once.", 198 annotationNode); 199 getSourceUnit().getErrorCollector().addErrorAndContinue(message); 200 } 201 202 private void reportError(String message, ASTNode node) { 203 getSourceUnit().getErrorCollector() 204 .addErrorAndContinue(createSyntaxErrorMessage(message, node)); 205 } 206 207 private Message createSyntaxErrorMessage(String message, ASTNode node) { 208 return new SyntaxErrorMessage( 209 new SyntaxException(message, node.getLineNumber(), node.getColumnNumber(), 210 node.getLastLineNumber(), node.getLastColumnNumber()), 211 getSourceUnit()); 212 } 213 214 private static class GrapeModelResolver implements ModelResolver { 215 216 @Override 217 public ModelSource resolveModel(Parent parent) throws UnresolvableModelException { 218 return resolveModel(parent.getGroupId(), parent.getArtifactId(), 219 parent.getVersion()); 220 } 221 222 @Override 223 public ModelSource resolveModel(Dependency dependency) 224 throws UnresolvableModelException { 225 return resolveModel(dependency.getGroupId(), dependency.getArtifactId(), 226 dependency.getVersion()); 227 } 228 229 @Override 230 public ModelSource resolveModel(String groupId, String artifactId, String version) 231 throws UnresolvableModelException { 232 Map<String, String> dependency = new HashMap<>(); 233 dependency.put("group", groupId); 234 dependency.put("module", artifactId); 235 dependency.put("version", version); 236 dependency.put("type", "pom"); 237 try { 238 return new UrlModelSource( 239 Grape.getInstance().resolve(null, dependency)[0].toURL()); 240 } 241 catch (MalformedURLException ex) { 242 throw new UnresolvableModelException(ex.getMessage(), groupId, artifactId, 243 version); 244 } 245 } 246 247 @Override 248 public void addRepository(Repository repository) 249 throws InvalidRepositoryException { 250 } 251 252 @Override 253 public void addRepository(Repository repository, boolean replace) 254 throws InvalidRepositoryException { 255 } 256 257 @Override 258 public ModelResolver newCopy() { 259 return this; 260 } 261 262 } 263 264}