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}