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}