View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.archetype.ui.generation;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.StringReader;
28  import java.io.StringWriter;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Comparator;
32  import java.util.HashMap;
33  import java.util.LinkedHashSet;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Properties;
37  import java.util.Set;
38  
39  import org.apache.maven.archetype.ArchetypeGenerationRequest;
40  import org.apache.maven.archetype.common.ArchetypeArtifactManager;
41  import org.apache.maven.archetype.common.Constants;
42  import org.apache.maven.archetype.exception.ArchetypeGenerationConfigurationFailure;
43  import org.apache.maven.archetype.exception.ArchetypeNotConfigured;
44  import org.apache.maven.archetype.exception.ArchetypeNotDefined;
45  import org.apache.maven.archetype.exception.UnknownArchetype;
46  import org.apache.maven.archetype.ui.ArchetypeConfiguration;
47  import org.apache.maven.archetype.ui.ArchetypeDefinition;
48  import org.apache.maven.archetype.ui.ArchetypeFactory;
49  import org.apache.velocity.Template;
50  import org.apache.velocity.VelocityContext;
51  import org.apache.velocity.context.Context;
52  import org.apache.velocity.context.InternalContextAdapterImpl;
53  import org.apache.velocity.runtime.RuntimeInstance;
54  import org.apache.velocity.runtime.parser.ParseException;
55  import org.apache.velocity.runtime.parser.node.ASTNegateNode;
56  import org.apache.velocity.runtime.parser.node.ASTReference;
57  import org.apache.velocity.runtime.parser.node.SimpleNode;
58  import org.apache.velocity.runtime.visitor.BaseVisitor;
59  import org.codehaus.plexus.components.interactivity.PrompterException;
60  import org.codehaus.plexus.util.StringUtils;
61  import org.codehaus.plexus.velocity.VelocityComponent;
62  import org.eclipse.aether.RepositorySystem;
63  import org.eclipse.aether.RepositorySystemSession;
64  import org.eclipse.aether.repository.RemoteRepository;
65  import org.eclipse.aether.repository.RepositoryPolicy;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  // TODO: this seems to have more responsibilities than just a configurator
70  @Named("default")
71  @Singleton
72  public class DefaultArchetypeGenerationConfigurator implements ArchetypeGenerationConfigurator {
73      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultArchetypeGenerationConfigurator.class);
74  
75      private ArchetypeArtifactManager archetypeArtifactManager;
76  
77      private ArchetypeFactory archetypeFactory;
78  
79      private ArchetypeGenerationQueryer archetypeGenerationQueryer;
80  
81      private VelocityComponent velocity;
82  
83      private RepositorySystem repositorySystem;
84  
85      @Inject
86      public DefaultArchetypeGenerationConfigurator(
87              ArchetypeArtifactManager archetypeArtifactManager,
88              ArchetypeFactory archetypeFactory,
89              ArchetypeGenerationQueryer archetypeGenerationQueryer,
90              VelocityComponent velocity,
91              RepositorySystem repositorySystem) {
92          this.archetypeArtifactManager = archetypeArtifactManager;
93          this.archetypeFactory = archetypeFactory;
94          this.archetypeGenerationQueryer = archetypeGenerationQueryer;
95          this.velocity = velocity;
96          this.repositorySystem = repositorySystem;
97      }
98  
99      public void setArchetypeArtifactManager(ArchetypeArtifactManager archetypeArtifactManager) {
100         this.archetypeArtifactManager = archetypeArtifactManager;
101     }
102 
103     @Override
104     @SuppressWarnings("checkstyle:MethodLength")
105     public void configureArchetype(
106             ArchetypeGenerationRequest request, Boolean interactiveMode, Properties executionProperties)
107             throws ArchetypeNotDefined, UnknownArchetype, ArchetypeNotConfigured, PrompterException,
108                     ArchetypeGenerationConfigurationFailure {
109 
110         List<RemoteRepository> repositories = new ArrayList<>();
111 
112         Properties properties = new Properties(executionProperties);
113 
114         ArchetypeDefinition ad = new ArchetypeDefinition(request);
115 
116         if (!ad.isDefined()) {
117             if (!interactiveMode) {
118                 throw new ArchetypeNotDefined("No archetype was chosen");
119             } else {
120                 throw new ArchetypeNotDefined("The archetype is not defined");
121             }
122         }
123         if (request.getArchetypeRepository() != null) {
124             RepositorySystemSession repositorySession = request.getRepositorySession();
125             RemoteRepository archetypeRepository =
126                     createRepository(repositorySession, request.getArchetypeRepository(), ad.getArtifactId() + "-repo");
127             repositories.add(archetypeRepository);
128         }
129         if (request.getRemoteRepositories() != null) {
130             repositories.addAll(request.getRemoteRepositories());
131         }
132 
133         if (!archetypeArtifactManager.exists(
134                 ad.getGroupId(), ad.getArtifactId(), ad.getVersion(), repositories, request.getRepositorySession())) {
135             throw new UnknownArchetype("The desired archetype does not exist (" + ad.getGroupId() + ":"
136                     + ad.getArtifactId() + ":" + ad.getVersion() + ")");
137         }
138 
139         request.setArchetypeVersion(ad.getVersion());
140 
141         ArchetypeConfiguration archetypeConfiguration;
142 
143         File archetypeFile = archetypeArtifactManager.getArchetypeFile(
144                 ad.getGroupId(), ad.getArtifactId(), ad.getVersion(), repositories, request.getRepositorySession());
145         if (archetypeArtifactManager.isFileSetArchetype(archetypeFile)) {
146             org.apache.maven.archetype.metadata.ArchetypeDescriptor archetypeDescriptor =
147                     archetypeArtifactManager.getFileSetArchetypeDescriptor(archetypeFile);
148 
149             archetypeConfiguration = archetypeFactory.createArchetypeConfiguration(archetypeDescriptor, properties);
150         } else if (archetypeArtifactManager.isOldArchetype(archetypeFile)) {
151             org.apache.maven.archetype.old.descriptor.ArchetypeDescriptor archetypeDescriptor =
152                     archetypeArtifactManager.getOldArchetypeDescriptor(archetypeFile);
153 
154             archetypeConfiguration = archetypeFactory.createArchetypeConfiguration(archetypeDescriptor, properties);
155         } else {
156             throw new ArchetypeGenerationConfigurationFailure(
157                     "The defined artifact is not an archetype: " + archetypeFile);
158         }
159 
160         List<String> propertiesRequired = archetypeConfiguration.getRequiredProperties();
161         Collections.sort(propertiesRequired, new RequiredPropertyComparator(archetypeConfiguration));
162 
163         Context context = new VelocityContext();
164         if (interactiveMode.booleanValue()) {
165             boolean confirmed = false;
166             context.put(Constants.GROUP_ID, ad.getGroupId());
167             context.put(Constants.ARTIFACT_ID, ad.getArtifactId());
168             context.put(Constants.VERSION, ad.getVersion());
169             while (!confirmed) {
170                 if (archetypeConfiguration.isConfigured()) {
171                     for (String requiredProperty : propertiesRequired) {
172                         LOGGER.info("Using property: " + requiredProperty + " = "
173                                 + archetypeConfiguration.getProperty(requiredProperty));
174                     }
175                 } else {
176                     for (String requiredProperty : propertiesRequired) {
177                         String value;
178 
179                         if (archetypeConfiguration.isConfigured(requiredProperty)
180                                 && !request.isAskForDefaultPropertyValues()) {
181                             LOGGER.info("Using property: " + requiredProperty + " = "
182                                     + archetypeConfiguration.getProperty(requiredProperty));
183 
184                             value = archetypeConfiguration.getProperty(requiredProperty);
185                         } else {
186                             String defaultValue = archetypeConfiguration.getDefaultValue(requiredProperty);
187 
188                             if (Constants.PACKAGE.equals(requiredProperty)
189                                     && (defaultValue == null || defaultValue.isEmpty())) {
190                                 defaultValue = archetypeConfiguration.getProperty(Constants.GROUP_ID);
191                             }
192                             value = archetypeGenerationQueryer.getPropertyValue(
193                                     requiredProperty,
194                                     expandEmbeddedTemplateExpressions(defaultValue, requiredProperty, context),
195                                     archetypeConfiguration.getPropertyValidationRegex(requiredProperty));
196                         }
197 
198                         archetypeConfiguration.setProperty(requiredProperty, value);
199 
200                         context.put(requiredProperty, value);
201                     }
202                 }
203 
204                 if (!archetypeConfiguration.isConfigured()) {
205                     LOGGER.warn("Archetype is not fully configured");
206                 } else if (!archetypeGenerationQueryer.confirmConfiguration(archetypeConfiguration)) {
207                     LOGGER.debug("Archetype generation configuration not confirmed");
208                     archetypeConfiguration.reset();
209                     restoreCommandLineProperties(archetypeConfiguration, executionProperties);
210                 } else {
211                     LOGGER.debug("Archetype generation configuration confirmed");
212 
213                     confirmed = true;
214                 }
215             }
216         } else {
217             if (!archetypeConfiguration.isConfigured()) {
218                 for (String requiredProperty : propertiesRequired) {
219                     if (archetypeConfiguration.isConfigured(requiredProperty)) {
220                         context.put(requiredProperty, archetypeConfiguration.getProperty(requiredProperty));
221                     } else {
222                         String defaultValue = archetypeConfiguration.getDefaultValue(requiredProperty);
223 
224                         if (defaultValue != null) {
225                             String value = expandEmbeddedTemplateExpressions(defaultValue, requiredProperty, context);
226                             archetypeConfiguration.setProperty(requiredProperty, value);
227                             context.put(requiredProperty, value);
228                         }
229                     }
230                 }
231 
232                 // in batch mode, we assume the defaults, and if still not configured fail
233                 if (!archetypeConfiguration.isConfigured()) {
234                     StringBuilder exceptionMessage = new StringBuilder();
235                     exceptionMessage.append("Archetype ");
236                     exceptionMessage.append(request.getArchetypeGroupId());
237                     exceptionMessage.append(":");
238                     exceptionMessage.append(request.getArchetypeArtifactId());
239                     exceptionMessage.append(":");
240                     exceptionMessage.append(request.getArchetypeVersion());
241                     exceptionMessage.append(" is not configured");
242 
243                     List<String> missingProperties = new ArrayList<>(0);
244                     for (String requiredProperty : archetypeConfiguration.getRequiredProperties()) {
245                         if (!archetypeConfiguration.isConfigured(requiredProperty)) {
246                             exceptionMessage.append("\n\tProperty ");
247                             exceptionMessage.append(requiredProperty);
248                             missingProperties.add(requiredProperty);
249                             exceptionMessage.append(" is missing.");
250                             LOGGER.warn("Property " + requiredProperty + " is missing. Add -D" + requiredProperty
251                                     + "=someValue");
252                         }
253                     }
254 
255                     throw new ArchetypeNotConfigured(exceptionMessage.toString(), missingProperties);
256                 }
257             }
258         }
259 
260         request.setGroupId(archetypeConfiguration.getProperty(Constants.GROUP_ID));
261 
262         request.setArtifactId(archetypeConfiguration.getProperty(Constants.ARTIFACT_ID));
263 
264         request.setVersion(archetypeConfiguration.getProperty(Constants.VERSION));
265 
266         request.setPackage(archetypeConfiguration.getProperty(Constants.PACKAGE));
267 
268         properties = archetypeConfiguration.getProperties();
269 
270         request.setProperties(properties);
271     }
272 
273     private String expandEmbeddedTemplateExpressions(String originalText, String textDescription, Context context) {
274         if (StringUtils.contains(originalText, "${")) {
275             try (StringWriter target = new StringWriter()) {
276                 velocity.getEngine().evaluate(context, target, textDescription, originalText);
277                 return target.toString();
278             } catch (IOException ex) {
279                 // closing StringWriter shouldn't actually generate any exception
280                 throw new RuntimeException("Exception closing StringWriter", ex);
281             }
282         }
283         return originalText;
284     }
285 
286     private void restoreCommandLineProperties(
287             ArchetypeConfiguration archetypeConfiguration, Properties executionProperties) {
288         LOGGER.debug("Restoring command line properties");
289 
290         for (String property : archetypeConfiguration.getRequiredProperties()) {
291             if (executionProperties.containsKey(property)) {
292                 archetypeConfiguration.setProperty(property, executionProperties.getProperty(property));
293                 LOGGER.debug("Restored " + property + "=" + archetypeConfiguration.getProperty(property));
294             }
295         }
296     }
297 
298     void setArchetypeGenerationQueryer(ArchetypeGenerationQueryer archetypeGenerationQueryer) {
299         this.archetypeGenerationQueryer = archetypeGenerationQueryer;
300     }
301 
302     public static class RequiredPropertyComparator implements Comparator<String> {
303         private final ArchetypeConfiguration archetypeConfiguration;
304 
305         private Map<String, Set<String>> propertyReferenceMap;
306 
307         public RequiredPropertyComparator(ArchetypeConfiguration archetypeConfiguration) {
308             this.archetypeConfiguration = archetypeConfiguration;
309             propertyReferenceMap = computePropertyReferences();
310         }
311 
312         @Override
313         public int compare(String left, String right) {
314             if (references(right, left)) {
315                 return 1;
316             }
317 
318             if (references(left, right)) {
319                 return -1;
320             }
321 
322             return Integer.compare(
323                     propertyReferenceMap.get(left).size(),
324                     propertyReferenceMap.get(right).size());
325         }
326 
327         private Map<String, Set<String>> computePropertyReferences() {
328             Map<String, Set<String>> result = new HashMap<>();
329 
330             List<String> requiredProperties = archetypeConfiguration.getRequiredProperties();
331 
332             final InternalContextAdapterImpl velocityContextAdapter =
333                     new InternalContextAdapterImpl(new VelocityContext());
334 
335             final RuntimeInstance velocityRuntime = new RuntimeInstance();
336             Properties properties = new Properties();
337             properties.put("parser.allow_hyphen_in_identifiers", true);
338             velocityRuntime.setProperties(properties);
339 
340             for (String propertyName : requiredProperties) {
341                 final Set<String> referencedPropertyNames = new LinkedHashSet<>();
342 
343                 String defaultValue = archetypeConfiguration.getDefaultValue(propertyName);
344                 if (StringUtils.contains(defaultValue, "${")) {
345                     try {
346                         Template template = new Template();
347                         template.setName(propertyName + ".default");
348                         SimpleNode node = velocityRuntime.parse(new StringReader(defaultValue), template);
349 
350                         node.init(velocityContextAdapter, velocityRuntime);
351 
352                         node.jjtAccept(
353                                 new BaseVisitor() {
354                                     @Override
355                                     public Object visit(ASTReference node, Object data) {
356                                         referencedPropertyNames.add(node.getRootString());
357                                         return super.visit(node, data);
358                                     }
359 
360                                     @Override
361                                     public Object visit(ASTNegateNode astNegateNode, Object data) {
362                                         return astNegateNode.childrenAccept(this, data);
363                                     }
364                                 },
365                                 velocityRuntime);
366                     } catch (ParseException e) {
367                         throw new IllegalStateException("Unparsable default value for property " + propertyName, e);
368                     }
369                 }
370 
371                 referencedPropertyNames.retainAll(archetypeConfiguration.getRequiredProperties());
372 
373                 // handle the case that a property expression #set()s itself:
374                 referencedPropertyNames.remove(propertyName);
375                 result.put(propertyName, referencedPropertyNames);
376             }
377 
378             return result;
379         }
380 
381         /**
382          * Learn whether one property references another. Semantically, "references
383          * {@code targetProperty}, {@code sourceProperty} (does)."
384          *
385          * @param targetProperty {@link String} denoting property for which the state of
386          *        being-referenced-by-the-property-denoted-by {@code sourceProperty} is desired
387          * @param sourceProperty {@link String} denoting property for which the state of
388          *        references-the-property-denoted-by {@code targetProperty} is desired
389          * @return {@code boolean}
390          */
391         private boolean references(String targetProperty, String sourceProperty) {
392             if (targetProperty.equals(sourceProperty)) {
393                 return false;
394             }
395             synchronized (this) {
396                 if (!propertyReferenceMap.containsKey(sourceProperty))
397                 // something has changed
398                 {
399                     this.propertyReferenceMap = computePropertyReferences();
400                 }
401             }
402             Set<String> referencedProperties = propertyReferenceMap.get(sourceProperty);
403             if (referencedProperties.contains(targetProperty)) {
404                 return true;
405             }
406             for (String referencedProperty : referencedProperties) {
407                 if (references(targetProperty, referencedProperty)) {
408                     return true;
409                 }
410             }
411             return false;
412         }
413     }
414 
415     private RemoteRepository createRepository(
416             RepositorySystemSession repositorySession, String url, String repositoryId) {
417 
418         // snapshots vs releases
419         // offline = to turning the update policy off
420 
421         // TODO: we'll need to allow finer grained creation of repositories but this will do for now
422 
423         RepositoryPolicy repositoryPolicy = new RepositoryPolicy(
424                 true, RepositoryPolicy.UPDATE_POLICY_ALWAYS, RepositoryPolicy.CHECKSUM_POLICY_WARN);
425 
426         RemoteRepository remoteRepository = new RemoteRepository.Builder(repositoryId, "default", url)
427                 .setSnapshotPolicy(repositoryPolicy)
428                 .setReleasePolicy(repositoryPolicy)
429                 .build();
430 
431         return repositorySystem
432                 .newResolutionRepositories(repositorySession, Collections.singletonList(remoteRepository))
433                 .get(0);
434     }
435 }