View Javadoc
1   /*
2    * Copyright (c) 2012-2022 Yegor Bugayenko
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.maven.plugin;
31  
32  import com.jcabi.log.Logger;
33  import java.io.File;
34  import java.io.IOException;
35  import java.nio.file.Files;
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.LinkedList;
39  import java.util.List;
40  import java.util.concurrent.CopyOnWriteArrayList;
41  import org.apache.commons.io.FileUtils;
42  import org.apache.commons.io.filefilter.FileFilterUtils;
43  import org.apache.commons.io.filefilter.IOFileFilter;
44  import org.apache.commons.io.filefilter.TrueFileFilter;
45  import org.apache.commons.lang3.StringUtils;
46  import org.apache.maven.artifact.Artifact;
47  import org.apache.maven.artifact.handler.ArtifactHandler;
48  import org.apache.maven.execution.MavenSession;
49  import org.apache.maven.plugin.AbstractMojo;
50  import org.apache.maven.plugin.MojoExecution;
51  import org.apache.maven.plugin.MojoFailureException;
52  import org.apache.maven.plugins.annotations.LifecyclePhase;
53  import org.apache.maven.plugins.annotations.Mojo;
54  import org.apache.maven.plugins.annotations.Parameter;
55  import org.apache.maven.plugins.annotations.ResolutionScope;
56  import org.apache.maven.project.DefaultProjectBuildingRequest;
57  import org.apache.maven.project.MavenProject;
58  import org.apache.maven.project.ProjectBuildingRequest;
59  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
60  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
61  import org.apache.maven.shared.dependency.graph.DependencyNode;
62  import org.aspectj.bridge.IMessage;
63  import org.aspectj.bridge.IMessageHolder;
64  import org.aspectj.tools.ajc.Main;
65  import org.codehaus.plexus.PlexusConstants;
66  import org.codehaus.plexus.PlexusContainer;
67  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
68  import org.codehaus.plexus.context.Context;
69  import org.codehaus.plexus.context.ContextException;
70  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
71  import org.eclipse.aether.RepositorySystemSession;
72  import org.eclipse.aether.util.artifact.JavaScopes;
73  import org.slf4j.impl.StaticLoggerBinder;
74  
75  /**
76   * AspectJ compile CLASS files.
77   *
78   * @since 0.7.16
79   * @see <a href="http://www.eclipse.org/aspectj/doc/next/devguide/ajc-ref.html">AJC compiler manual</a>
80   */
81  @Mojo(
82      name = "ajc",
83      defaultPhase = LifecyclePhase.PROCESS_CLASSES,
84      threadSafe = true,
85      requiresDependencyResolution = ResolutionScope.COMPILE
86  )
87  @SuppressWarnings({ "PMD.TooManyMethods", "PMD.ExcessiveImports", "PMD.GodClass" })
88  public final class AjcMojo extends AbstractMojo implements Contextualizable {
89  
90      /**
91       * Classpath separator.
92       */
93      private static final String SEP = System.getProperty("path.separator");
94  
95      /**
96       * Maven project.
97       */
98      @Parameter(defaultValue = "${project}", readonly = true)
99      private transient MavenProject project;
100 
101     /**
102      * Maven execution.
103      */
104     @Parameter(defaultValue = "${mojoExecution}", readonly = true)
105     private transient MojoExecution execution;
106 
107     /**
108      * Maven session.
109      */
110     @Parameter(defaultValue = "${session}", readonly = true)
111     private transient MavenSession session;
112 
113     /**
114      * Rep session.
115      */
116     @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
117     private transient RepositorySystemSession rsession;
118 
119     /**
120      * Compiled directory.
121      * @checkstyle MemberNameCheck (7 lines)
122      */
123     @Parameter(defaultValue = "${project.build.outputDirectory}")
124     private transient File classesDirectory;
125 
126     /**
127      * Directory in which uwoven classes are copied.
128      * @checkstyle MemberNameCheck (7 lines)
129      */
130     @Parameter(defaultValue = "${project.build.directory}/unwoven")
131     private transient File unwovenClassesDir;
132 
133     /**
134      * Disables the copy of unwoven files to unwovenClassesDir.
135      * @checkstyle MemberNameCheck (7 lines)
136      */
137     @Parameter(defaultValue = "false")
138     private transient boolean disableCopy;
139 
140     /**
141      * Directories with aspects.
142      * @checkstyle MemberNameCheck (6 lines)
143      */
144     @Parameter
145     private transient File[] aspectDirectories;
146 
147     /**
148      * Temporary directory.
149      * @checkstyle MemberNameCheck (7 lines)
150      */
151     @Parameter(defaultValue = "${project.build.directory}/jcabi-ajc")
152     private transient File tempDirectory;
153 
154     /**
155      * Scopes to take into account.
156      */
157     @Parameter
158     private transient String[] scopes;
159 
160     /**
161      * Container.
162      */
163     private transient PlexusContainer container;
164 
165     /**
166      * Java source version.
167      */
168     @Parameter(property = "source", defaultValue = "1.8")
169     private transient String source;
170 
171     /**
172      * Java target version.
173      */
174     @Parameter(property = "target", defaultValue = "1.8")
175     private transient String target;
176 
177     /**
178      * Project classpath.
179      * @checkstyle MemberNameCheck (7 lines)
180      */
181     @Parameter(
182         defaultValue = "${project.compileClasspathElements}",
183         required = true,
184         readonly = true
185     )
186     private transient List<String> classpathElements;
187 
188     /**
189      * Ajc compiler message log.
190      */
191     @Parameter(
192         property = "log",
193         defaultValue = "${project.build.directory}/jcabi-ajc.log"
194     )
195     private transient String log;
196 
197     @Override
198     public void contextualize(final Context context) throws ContextException {
199         this.container = (PlexusContainer) context
200             .get(PlexusConstants.PLEXUS_KEY);
201     }
202 
203     @Override
204     public void execute() throws MojoFailureException {
205         StaticLoggerBinder.getSingleton().setMavenLog(this.getLog());
206         final ArtifactHandler handler = this.project.getArtifact()
207             .getArtifactHandler();
208         if (!"java".equalsIgnoreCase(handler.getLanguage())) {
209             Logger.warn(
210                 this,
211                 // @checkstyle LineLength (1 line)
212                 "Not executing AJC as the project is not a Java classpath-capable package"
213             );
214             return;
215         }
216         if (this.classesDirectory.mkdirs()) {
217             Logger.info(this, "Created classes dir %s", this.classesDirectory);
218         }
219         if (!this.disableCopy
220             && !this.unwovenClassesDir.equals(this.classesDirectory)) {
221             this.copyUnwovenClasses();
222         }
223         if (this.hasClasses() || this.hasSourceroots()) {
224             try {
225                 this.executeAjc();
226             } catch (final IOException ex) {
227                 throw new IllegalStateException(ex);
228             }
229         } else {
230             Logger.warn(
231                 this,
232                 // @checkstyle LineLength (1 line)
233                 "Not executing AJC as there is no .class file or source roots file."
234             );
235         }
236     }
237 
238     /**
239      * Process classes and source roots files with AJC.
240      *
241      * @throws MojoFailureException If AJC failed to process files
242      * @throws IOException If fails
243      */
244     private void executeAjc() throws MojoFailureException, IOException {
245         if (this.tempDirectory.mkdirs()) {
246             Logger.info(this, "Created temp dir %s", this.tempDirectory);
247         }
248         final Main main = new Main();
249         final IMessageHolder mholder = new AjcMojo.MsgHolder();
250         main.run(
251             new String[] {
252                 "-Xset:avoidFinal=true",
253                 "-Xlint:warning",
254                 "-inpath",
255                 this.classesDirectory.getAbsolutePath(),
256                 "-sourceroots",
257                 this.sourceroots(),
258                 "-d",
259                 this.tempDirectory.getAbsolutePath(),
260                 "-classpath",
261                 StringUtils.join(this.classpath(), AjcMojo.SEP),
262                 "-aspectpath",
263                 this.aspectpath(),
264                 "-source",
265                 this.source,
266                 "-target",
267                 this.target,
268                 "-g:none",
269                 "-encoding",
270                 "UTF-8",
271                 "-time",
272                 "-log",
273                 this.log,
274                 "-showWeaveInfo",
275                 "-warn:constructorName",
276                 "-warn:packageDefaultMethod",
277                 "-warn:deprecation",
278                 "-warn:maskedCatchBlocks",
279                 "-warn:unusedLocals",
280                 "-warn:unusedArguments",
281                 "-warn:unusedImports",
282                 "-warn:syntheticAccess",
283                 "-warn:assertIdentifier",
284             },
285             mholder
286         );
287         try {
288             FileUtils.copyDirectory(this.tempDirectory, this.classesDirectory);
289             FileUtils.cleanDirectory(this.tempDirectory);
290         } catch (final IOException ex) {
291             throw new MojoFailureException(
292                 "failed to copy files and clean temp",
293                 ex
294             );
295         }
296         Logger.info(
297             this,
298             // @checkstyle LineLength (1 line)
299             "ajc result: %d file(s) processed, %d pointcut(s) woven, %d error(s), %d warning(s)",
300             AjcMojo.files(this.classesDirectory).size(),
301             mholder.numMessages(IMessage.WEAVEINFO, false),
302             mholder.numMessages(IMessage.ERROR, true),
303             mholder.numMessages(IMessage.WARNING, false)
304         );
305         if (mholder.hasAnyMessage(IMessage.ERROR, true)) {
306             throw new MojoFailureException("AJC failed, see log above");
307         }
308     }
309 
310     /**
311      * Get classpath for AJC.
312      * @return Classpath
313      */
314     private Collection<String> classpath() {
315         final Collection<String> scps;
316         if (this.scopes == null) {
317             scps = AjcMojo.scope();
318         } else {
319             scps = Arrays.asList(this.scopes);
320         }
321         final Collection<String> elements = new LinkedList<>();
322         try {
323             final DependencyGraphBuilder builder =
324                 DependencyGraphBuilder.class.cast(
325                     this.container.lookup(
326                         DependencyGraphBuilder.class.getCanonicalName(),
327                         "default"
328                     )
329                 );
330             final ProjectBuildingRequest request =
331                 new DefaultProjectBuildingRequest();
332             request.setProject(this.project);
333             request.setRepositorySession(this.rsession);
334             final DependencyNode node = builder.buildDependencyGraph(
335                 request,
336                 artifact -> scps.contains(artifact.getScope())
337             );
338             elements.addAll(this.dependencies(node, scps));
339         } catch (final DependencyGraphBuilderException
340             | ComponentLookupException ex) {
341             throw new IllegalStateException(ex);
342         }
343         elements.addAll(this.classpathElements);
344         return elements;
345     }
346 
347     /**
348      * Retrieve dependencies for from given node and scope.
349      * @param node Node to traverse.
350      * @param scps Scopes to use.
351      * @return Collection of dependency files.
352      */
353     private Collection<String> dependencies(final DependencyNode node,
354         final Collection<String> scps) {
355         final Artifact artifact = node.getArtifact();
356         final Collection<String> files = new LinkedList<>();
357         if (artifact.getScope() == null
358             || scps.contains(artifact.getScope())) {
359             if (artifact.getScope() == null) {
360                 files.add(artifact.getFile().toString());
361             } else {
362                 files.add(
363                     this.session.getLocalRepository().find(artifact).getFile()
364                         .toString()
365                 );
366             }
367             for (final DependencyNode child : node.getChildren()) {
368                 if (child.getArtifact().compareTo(node.getArtifact()) != 0) {
369                     files.addAll(this.dependencies(child, scps));
370                 }
371             }
372         }
373         return files;
374     }
375 
376     /**
377      * Default scopes.
378      * @return List of scopes.
379      */
380     private static Collection<String> scope() {
381         final List<String> scps;
382         if (AjcMojo.eclipseAether()) {
383             scps = Arrays.asList(
384                 JavaScopes.COMPILE,
385                 JavaScopes.PROVIDED,
386                 JavaScopes.RUNTIME,
387                 JavaScopes.SYSTEM
388             );
389         } else {
390             scps = Arrays.asList(
391                 JavaScopes.COMPILE,
392                 JavaScopes.RUNTIME,
393                 JavaScopes.PROVIDED,
394                 JavaScopes.SYSTEM
395             );
396         }
397         return scps;
398     }
399 
400     /**
401      * If environment is inside Eclipse Aether.
402      * @return True if Eclipse Aether.
403      */
404     private static boolean eclipseAether() {
405         boolean found = false;
406         try {
407             Thread.currentThread().getContextClassLoader()
408                 .loadClass("org.sonatype.aether.graph.DependencyFilter");
409         } catch (final ClassNotFoundException ex) {
410             found = true;
411         }
412         return found;
413     }
414 
415     /**
416      * Get locations of all aspect libraries for AJC.
417      * @return Classpath
418      */
419     private String aspectpath() {
420         return new StringBuilder(0)
421             .append(StringUtils.join(this.classpath(), AjcMojo.SEP))
422             .append(AjcMojo.SEP)
423             .append(System.getProperty("java.class.path"))
424             .toString();
425     }
426 
427     /**
428      * Get locations of all source roots (with aspects in source form).
429      * @return Directories separated
430      * @throws IOException If fails
431      */
432     private String sourceroots() throws IOException {
433         final String path;
434         if (this.aspectDirectories == null
435             || this.aspectDirectories.length == 0) {
436             path = Files.createTempDirectory("temp")
437                 .toAbsolutePath().toString();
438         } else {
439             for (final File dir : this.aspectDirectories) {
440                 if (!dir.exists()) {
441                     throw new IllegalStateException(
442                         String.format("source directory %s is absent", dir)
443                     );
444                 }
445             }
446             path = StringUtils.join(this.aspectDirectories, AjcMojo.SEP);
447         }
448         return path;
449     }
450 
451     /**
452      * Check if the project contains .class files.
453      * @return True if .class files found
454      */
455     private boolean hasClasses() {
456         return this.listClasses().size() > 0;
457     }
458 
459     /**
460      * List of all .class files from <b>classesDirectory</b>.
461      * @return A Collection of .class files
462      */
463     private Collection<File> listClasses() {
464         final IOFileFilter filter = FileFilterUtils
465             .suffixFileFilter(".class");
466         return FileUtils.listFiles(
467             this.classesDirectory, filter, FileFilterUtils
468                 .directoryFileFilter()
469         );
470     }
471 
472     /**
473      * Check if the project contains source roots files.
474      * @return True if {@linkplain #aspectDirectories} contain files
475      */
476     private boolean hasSourceroots() {
477         return this.aspectDirectories != null
478             && this.aspectDirectories.length > 0;
479     }
480 
481     /**
482      * Find all files in the directory.
483      * @param dir The directory
484      * @return List of them
485      */
486     private static Collection<File> files(final File dir) {
487         final Collection<File> files = new LinkedList<>();
488         final Collection<File> all = FileUtils.listFiles(
489             dir, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE
490         );
491         for (final File file : all) {
492             if (file.isFile()) {
493                 files.add(file);
494             }
495         }
496         return files;
497     }
498 
499     /**
500      * Copy the unwoven classes from <b>classesDirectory</b> to
501      * <b>unwovenClassesDir</b>.
502      * @throws MojoFailureException If something goes wrong
503      */
504     private void copyUnwovenClasses()
505         throws MojoFailureException {
506         if (this.hasClasses()) {
507             new UnwovenClasses(
508                 this.unwovenClassesDir,
509                 this.classesDirectory,
510                 this.execution.getLifecyclePhase()
511             ).copy();
512         } else {
513             Logger.warn(
514                 this,
515                 "No classes found at %s. Nothing will be copied to %s",
516                 this.classesDirectory,
517                 this.unwovenClassesDir
518             );
519         }
520     }
521 
522     /**
523      * Message holder.
524      *
525      * @since 0.1
526      */
527     private static final class MsgHolder implements IMessageHolder {
528         /**
529          * All messages seen so far.
530          */
531         private final transient Collection<IMessage> messages =
532             new CopyOnWriteArrayList<>();
533 
534         @Override
535         public boolean hasAnyMessage(final IMessage.Kind kind,
536             final boolean greater) {
537             boolean has = false;
538             for (final IMessage msg : this.messages) {
539                 has = msg.getKind().equals(kind) || greater
540                     && IMessage.Kind.COMPARATOR
541                     .compare(msg.getKind(), kind) > 0;
542                 if (has) {
543                     break;
544                 }
545             }
546             return has;
547         }
548 
549         @Override
550         public int numMessages(final IMessage.Kind kind,
551             final boolean greater) {
552             int num = 0;
553             for (final IMessage msg : this.messages) {
554                 final boolean has = msg.getKind().equals(kind) || greater
555                     && IMessage.Kind.COMPARATOR
556                     .compare(msg.getKind(), kind) > 0;
557                 if (has) {
558                     ++num;
559                 }
560             }
561             return num;
562         }
563 
564         @Override
565         public IMessage[] getMessages(final IMessage.Kind kind,
566             final boolean greater) {
567             throw new UnsupportedOperationException();
568         }
569 
570         @Override
571         public List<IMessage> getUnmodifiableListView() {
572             throw new UnsupportedOperationException();
573         }
574 
575         @Override
576         public void clearMessages() {
577             throw new UnsupportedOperationException();
578         }
579 
580         @Override
581         public boolean handleMessage(final IMessage msg) {
582             if (msg.getKind().equals(IMessage.ERROR)
583                 || msg.getKind().equals(IMessage.FAIL)
584                 || msg.getKind().equals(IMessage.ABORT)) {
585                 Logger.error(AjcMojo.class, msg.getMessage());
586             } else if (msg.getKind().equals(IMessage.WARNING)) {
587                 Logger.warn(AjcMojo.class, msg.getMessage());
588             } else {
589                 Logger.debug(AjcMojo.class, msg.getMessage());
590             }
591             this.messages.add(msg);
592             return true;
593         }
594 
595         @Override
596         public boolean isIgnoring(final IMessage.Kind kind) {
597             return false;
598         }
599 
600         @Override
601         public void dontIgnore(final IMessage.Kind kind) {
602             assert kind != null;
603         }
604 
605         @Override
606         public void ignore(final IMessage.Kind kind) {
607             assert kind != null;
608         }
609     }
610 
611 }