001/*
002 * Copyright 2012-2016 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.Model;
032import org.apache.maven.model.Repository;
033import org.apache.maven.model.building.DefaultModelBuilder;
034import org.apache.maven.model.building.DefaultModelBuilderFactory;
035import org.apache.maven.model.building.DefaultModelBuildingRequest;
036import org.apache.maven.model.building.ModelSource;
037import org.apache.maven.model.building.UrlModelSource;
038import org.apache.maven.model.resolution.InvalidRepositoryException;
039import org.apache.maven.model.resolution.ModelResolver;
040import org.apache.maven.model.resolution.UnresolvableModelException;
041import org.codehaus.groovy.ast.ASTNode;
042import org.codehaus.groovy.ast.AnnotationNode;
043import org.codehaus.groovy.ast.expr.ConstantExpression;
044import org.codehaus.groovy.ast.expr.Expression;
045import org.codehaus.groovy.ast.expr.ListExpression;
046import org.codehaus.groovy.control.messages.Message;
047import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
048import org.codehaus.groovy.syntax.SyntaxException;
049import org.codehaus.groovy.transform.ASTTransformation;
050
051import org.springframework.boot.cli.compiler.dependencies.MavenModelDependencyManagement;
052import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
053import org.springframework.boot.groovy.DependencyManagementBom;
054import org.springframework.core.Ordered;
055import org.springframework.core.annotation.Order;
056
057/**
058 * {@link ASTTransformation} for processing {@link DependencyManagementBom} annotations.
059 *
060 * @author Andy Wilkinson
061 * @since 1.3.0
062 */
063@Order(DependencyManagementBomTransformation.ORDER)
064public class DependencyManagementBomTransformation
065                extends AnnotatedNodeASTTransformation {
066
067        /**
068         * The order of the transformation.
069         */
070        public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 100;
071
072        private static final Set<String> DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES = Collections
073                        .unmodifiableSet(new HashSet<String>(
074                                        Arrays.asList(DependencyManagementBom.class.getName(),
075                                                        DependencyManagementBom.class.getSimpleName())));
076
077        private final DependencyResolutionContext resolutionContext;
078
079        public DependencyManagementBomTransformation(
080                        DependencyResolutionContext resolutionContext) {
081                super(DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES, true);
082                this.resolutionContext = resolutionContext;
083        }
084
085        @Override
086        protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
087                if (!annotationNodes.isEmpty()) {
088                        if (annotationNodes.size() > 1) {
089                                for (AnnotationNode annotationNode : annotationNodes) {
090                                        handleDuplicateDependencyManagementBomAnnotation(annotationNode);
091                                }
092                        }
093                        else {
094                                processDependencyManagementBomAnnotation(annotationNodes.get(0));
095                        }
096                }
097        }
098
099        private void processDependencyManagementBomAnnotation(AnnotationNode annotationNode) {
100                Expression valueExpression = annotationNode.getMember("value");
101                List<Map<String, String>> bomDependencies = createDependencyMaps(valueExpression);
102                updateDependencyResolutionContext(bomDependencies);
103        }
104
105        private List<Map<String, String>> createDependencyMaps(Expression valueExpression) {
106                Map<String, String> dependency = null;
107                List<ConstantExpression> constantExpressions = getConstantExpressions(
108                                valueExpression);
109                List<Map<String, String>> dependencies = new ArrayList<Map<String, String>>(
110                                constantExpressions.size());
111                for (ConstantExpression expression : constantExpressions) {
112                        Object value = expression.getValue();
113                        if (value instanceof String) {
114                                String[] components = ((String) expression.getValue()).split(":");
115                                if (components.length == 3) {
116                                        dependency = new HashMap<String, String>();
117                                        dependency.put("group", components[0]);
118                                        dependency.put("module", components[1]);
119                                        dependency.put("version", components[2]);
120                                        dependency.put("type", "pom");
121                                        dependencies.add(dependency);
122                                }
123                                else {
124                                        handleMalformedDependency(expression);
125                                }
126                        }
127                }
128                return dependencies;
129        }
130
131        private List<ConstantExpression> getConstantExpressions(Expression valueExpression) {
132                if (valueExpression instanceof ListExpression) {
133                        return getConstantExpressions((ListExpression) valueExpression);
134                }
135                if (valueExpression instanceof ConstantExpression
136                                && ((ConstantExpression) valueExpression).getValue() instanceof String) {
137                        return Arrays.asList((ConstantExpression) valueExpression);
138                }
139                reportError("@DependencyManagementBom requires an inline constant that is a "
140                                + "string or a string array", valueExpression);
141                return Collections.emptyList();
142        }
143
144        private List<ConstantExpression> getConstantExpressions(
145                        ListExpression valueExpression) {
146                List<ConstantExpression> expressions = new ArrayList<ConstantExpression>();
147                for (Expression expression : valueExpression.getExpressions()) {
148                        if (expression instanceof ConstantExpression
149                                        && ((ConstantExpression) expression).getValue() instanceof String) {
150                                expressions.add((ConstantExpression) expression);
151                        }
152                        else {
153                                reportError(
154                                                "Each entry in the array must be an " + "inline string constant",
155                                                expression);
156                        }
157                }
158                return expressions;
159        }
160
161        private void handleMalformedDependency(Expression expression) {
162                Message message = createSyntaxErrorMessage(
163                                String.format(
164                                                "The string must be of the form \"group:module:version\"%n"),
165                                expression);
166                getSourceUnit().getErrorCollector().addErrorAndContinue(message);
167        }
168
169        private void updateDependencyResolutionContext(
170                        List<Map<String, String>> bomDependencies) {
171                URI[] uris = Grape.getInstance().resolve(null,
172                                bomDependencies.toArray(new Map[bomDependencies.size()]));
173                DefaultModelBuilder modelBuilder = new DefaultModelBuilderFactory().newInstance();
174                for (URI uri : uris) {
175                        try {
176                                DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
177                                request.setModelResolver(new GrapeModelResolver());
178                                request.setModelSource(new UrlModelSource(uri.toURL()));
179                                request.setSystemProperties(System.getProperties());
180                                Model model = modelBuilder.build(request).getEffectiveModel();
181                                this.resolutionContext.addDependencyManagement(
182                                                new MavenModelDependencyManagement(model));
183                        }
184                        catch (Exception ex) {
185                                throw new IllegalStateException("Failed to build model for '" + uri
186                                                + "'. Is it a valid Maven bom?", ex);
187                        }
188                }
189        }
190
191        private void handleDuplicateDependencyManagementBomAnnotation(
192                        AnnotationNode annotationNode) {
193                Message message = createSyntaxErrorMessage(
194                                "Duplicate @DependencyManagementBom annotation. It must be declared at most once.",
195                                annotationNode);
196                getSourceUnit().getErrorCollector().addErrorAndContinue(message);
197        }
198
199        private void reportError(String message, ASTNode node) {
200                getSourceUnit().getErrorCollector()
201                                .addErrorAndContinue(createSyntaxErrorMessage(message, node));
202        }
203
204        private Message createSyntaxErrorMessage(String message, ASTNode node) {
205                return new SyntaxErrorMessage(
206                                new SyntaxException(message, node.getLineNumber(), node.getColumnNumber(),
207                                                node.getLastLineNumber(), node.getLastColumnNumber()),
208                                getSourceUnit());
209        }
210
211        private static class GrapeModelResolver implements ModelResolver {
212
213                @Override
214                public ModelSource resolveModel(String groupId, String artifactId, String version)
215                                throws UnresolvableModelException {
216                        Map<String, String> dependency = new HashMap<String, String>();
217                        dependency.put("group", groupId);
218                        dependency.put("module", artifactId);
219                        dependency.put("version", version);
220                        dependency.put("type", "pom");
221                        try {
222                                return new UrlModelSource(
223                                                Grape.getInstance().resolve(null, dependency)[0].toURL());
224                        }
225                        catch (MalformedURLException e) {
226                                throw new UnresolvableModelException(e.getMessage(), groupId, artifactId,
227                                                version);
228                        }
229                }
230
231                @Override
232                public void addRepository(Repository repository)
233                                throws InvalidRepositoryException {
234                }
235
236                @Override
237                public ModelResolver newCopy() {
238                        return this;
239                }
240
241        }
242
243}