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 groovy.lang.Grab;
020import groovy.lang.GroovyClassLoader;
021import org.codehaus.groovy.ast.AnnotationNode;
022import org.codehaus.groovy.ast.ClassNode;
023import org.codehaus.groovy.ast.ModuleNode;
024import org.codehaus.groovy.ast.expr.ConstantExpression;
025
026import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
027import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
028
029/**
030 * Customizer that allows dependencies to be added during compilation. Adding a dependency
031 * results in a {@link Grab @Grab} annotation being added to the primary {@link ClassNode
032 * class} is the {@link ModuleNode module} that's being customized.
033 * <p>
034 * This class provides a fluent API for conditionally adding dependencies. For example:
035 * {@code dependencies.ifMissing("com.corp.SomeClass").add(module)}.
036 *
037 * @author Phillip Webb
038 * @author Andy Wilkinson
039 */
040public class DependencyCustomizer {
041
042        private final GroovyClassLoader loader;
043
044        private final ClassNode classNode;
045
046        private final DependencyResolutionContext dependencyResolutionContext;
047
048        /**
049         * Create a new {@link DependencyCustomizer} instance.
050         * @param loader the current classloader
051         * @param moduleNode the current module
052         * @param dependencyResolutionContext the context for dependency resolution
053         */
054        public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode,
055                        DependencyResolutionContext dependencyResolutionContext) {
056                this.loader = loader;
057                this.classNode = moduleNode.getClasses().get(0);
058                this.dependencyResolutionContext = dependencyResolutionContext;
059        }
060
061        /**
062         * Create a new nested {@link DependencyCustomizer}.
063         * @param parent the parent customizer
064         */
065        protected DependencyCustomizer(DependencyCustomizer parent) {
066                this.loader = parent.loader;
067                this.classNode = parent.classNode;
068                this.dependencyResolutionContext = parent.dependencyResolutionContext;
069        }
070
071        public String getVersion(String artifactId) {
072                return getVersion(artifactId, "");
073
074        }
075
076        public String getVersion(String artifactId, String defaultVersion) {
077                String version = this.dependencyResolutionContext.getArtifactCoordinatesResolver()
078                                .getVersion(artifactId);
079                if (version == null) {
080                        version = defaultVersion;
081                }
082                return version;
083        }
084
085        /**
086         * Create a nested {@link DependencyCustomizer} that only applies if any of the
087         * specified class names are not on the class path.
088         * @param classNames the class names to test
089         * @return a nested {@link DependencyCustomizer}
090         */
091        public DependencyCustomizer ifAnyMissingClasses(final String... classNames) {
092                return new DependencyCustomizer(this) {
093                        @Override
094                        protected boolean canAdd() {
095                                for (String className : classNames) {
096                                        try {
097                                                DependencyCustomizer.this.loader.loadClass(className);
098                                        }
099                                        catch (Exception ex) {
100                                                return true;
101                                        }
102                                }
103                                return false;
104                        }
105                };
106        }
107
108        /**
109         * Create a nested {@link DependencyCustomizer} that only applies if all of the
110         * specified class names are not on the class path.
111         * @param classNames the class names to test
112         * @return a nested {@link DependencyCustomizer}
113         */
114        public DependencyCustomizer ifAllMissingClasses(final String... classNames) {
115                return new DependencyCustomizer(this) {
116                        @Override
117                        protected boolean canAdd() {
118                                for (String className : classNames) {
119                                        try {
120                                                DependencyCustomizer.this.loader.loadClass(className);
121                                                return false;
122                                        }
123                                        catch (Exception ex) {
124                                                // swallow exception and continue
125                                        }
126                                }
127                                return DependencyCustomizer.this.canAdd();
128                        }
129                };
130        }
131
132        /**
133         * Create a nested {@link DependencyCustomizer} that only applies if the specified
134         * paths are on the class path.
135         * @param paths the paths to test
136         * @return a nested {@link DependencyCustomizer}
137         */
138        public DependencyCustomizer ifAllResourcesPresent(final String... paths) {
139                return new DependencyCustomizer(this) {
140                        @Override
141                        protected boolean canAdd() {
142                                for (String path : paths) {
143                                        try {
144                                                if (DependencyCustomizer.this.loader.getResource(path) == null) {
145                                                        return false;
146                                                }
147                                                return true;
148                                        }
149                                        catch (Exception ex) {
150                                                // swallow exception and continue
151                                        }
152                                }
153                                return DependencyCustomizer.this.canAdd();
154                        }
155                };
156        }
157
158        /**
159         * Create a nested {@link DependencyCustomizer} that only applies at least one of the
160         * specified paths is on the class path.
161         * @param paths the paths to test
162         * @return a nested {@link DependencyCustomizer}
163         */
164        public DependencyCustomizer ifAnyResourcesPresent(final String... paths) {
165                return new DependencyCustomizer(this) {
166                        @Override
167                        protected boolean canAdd() {
168                                for (String path : paths) {
169                                        try {
170                                                if (DependencyCustomizer.this.loader.getResource(path) != null) {
171                                                        return true;
172                                                }
173                                                return false;
174                                        }
175                                        catch (Exception ex) {
176                                                // swallow exception and continue
177                                        }
178                                }
179                                return DependencyCustomizer.this.canAdd();
180                        }
181                };
182        }
183
184        /**
185         * Add dependencies and all of their dependencies. The group ID and version of the
186         * dependencies are resolved from the modules using the customizer's
187         * {@link ArtifactCoordinatesResolver}.
188         * @param modules The module IDs
189         * @return this {@link DependencyCustomizer} for continued use
190         */
191        public DependencyCustomizer add(String... modules) {
192                for (String module : modules) {
193                        add(module, null, null, true);
194                }
195                return this;
196        }
197
198        /**
199         * Add a single dependency and, optionally, all of its dependencies. The group ID and
200         * version of the dependency are resolved from the module using the customizer's
201         * {@link ArtifactCoordinatesResolver}.
202         * @param module The module ID
203         * @param transitive {@code true} if the transitive dependencies should also be added,
204         * otherwise {@code false}.
205         * @return this {@link DependencyCustomizer} for continued use
206         */
207        public DependencyCustomizer add(String module, boolean transitive) {
208                return add(module, null, null, transitive);
209        }
210
211        /**
212         * Add a single dependency with the specified classifier and type and, optionally, all
213         * of its dependencies. The group ID and version of the dependency are resolved from
214         * the module by using the customizer's {@link ArtifactCoordinatesResolver}.
215         * @param module The module ID
216         * @param classifier The classifier, may be {@code null}
217         * @param type The type, may be {@code null}
218         * @param transitive {@code true} if the transitive dependencies should also be added,
219         * otherwise {@code false}.
220         * @return this {@link DependencyCustomizer} for continued use
221         */
222        public DependencyCustomizer add(String module, String classifier, String type,
223                        boolean transitive) {
224                if (canAdd()) {
225                        ArtifactCoordinatesResolver artifactCoordinatesResolver = this.dependencyResolutionContext
226                                        .getArtifactCoordinatesResolver();
227                        this.classNode.addAnnotation(
228                                        createGrabAnnotation(artifactCoordinatesResolver.getGroupId(module),
229                                                        artifactCoordinatesResolver.getArtifactId(module),
230                                                        artifactCoordinatesResolver.getVersion(module), classifier,
231                                                        type, transitive));
232                }
233                return this;
234        }
235
236        private AnnotationNode createGrabAnnotation(String group, String module,
237                        String version, String classifier, String type, boolean transitive) {
238                AnnotationNode annotationNode = new AnnotationNode(new ClassNode(Grab.class));
239                annotationNode.addMember("group", new ConstantExpression(group));
240                annotationNode.addMember("module", new ConstantExpression(module));
241                annotationNode.addMember("version", new ConstantExpression(version));
242                if (classifier != null) {
243                        annotationNode.addMember("classifier", new ConstantExpression(classifier));
244                }
245                if (type != null) {
246                        annotationNode.addMember("type", new ConstantExpression(type));
247                }
248                annotationNode.addMember("transitive", new ConstantExpression(transitive));
249                annotationNode.addMember("initClass", new ConstantExpression(false));
250                return annotationNode;
251        }
252
253        /**
254         * Strategy called to test if dependencies can be added. Subclasses override as
255         * required. Returns {@code true} by default.
256         * @return {@code true} if dependencies can be added, otherwise {@code false}
257         */
258        protected boolean canAdd() {
259                return true;
260        }
261
262        /**
263         * Returns the {@link DependencyResolutionContext}.
264         * @return the dependency resolution context
265         */
266        public DependencyResolutionContext getDependencyResolutionContext() {
267                return this.dependencyResolutionContext;
268        }
269
270}