Merge pull request #4631 from sdedic/project/artiacts-tags

Artifact tags, support for shaded output and source attachments.
diff --git a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ArtifactSpec.java b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ArtifactSpec.java
index 0bdc541..fb80df8 100644
--- a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ArtifactSpec.java
+++ b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ArtifactSpec.java
@@ -22,7 +22,10 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.netbeans.api.annotations.common.NonNull;
@@ -45,10 +48,58 @@
  * The version specified is further classified by {@link VersionKind}, to 
  * distinguish versions possibly from repositories, development versions and
  * floating versions.
+ * <p>
+ * The ArtifactSpec may provide additional tags, that can further describe the artifact,
+ * but those tags are not part of "identity" of the artifact, for dependencies or build
+ * systems, only 
+ * <ul>
+ * <li>group
+ * <li>artifact
+ * <li>version
+ * <li>classifier
+ * <Li>extension
+ * </ul>
+ * are important.
  * 
  * @author sdedic
  */
 public final class ArtifactSpec<T> {
+    
+    /**
+     * A tag for an artifact with basic output of the project's code/contents.
+     * You almost never want this, usually you want {@code null} classifier to 
+     * identify the <b>default</b> output. But in rare cases you really do want
+     * to avoid post-processing or shading, this (abstract) classifier should
+     * identify an artifact before those steps.
+     * <p>
+     * If used in a query, a non-tagged artifact may be returned if the implementation
+     * does not support the tag.
+     */
+    public static final String TAG_BASE = "<basic>"; // NOI18N
+    
+    /**
+     * Tag for an artifact, that eventually contains dependencies bundled in. If used
+     * in a query, an ordinary (non-tagged) artifact may be returned from the query in case
+     * the implementation does not support the tag. Implementations may use additional, more
+     * specific tags on the returned artifacts.
+     */
+    public static final String TAG_SHADED = "<shaded>";
+
+    /**
+     * Classifier for an artifact that contains sources.
+     */
+    public static final String CLASSIFIER_SOURCES = "sources"; // NOI18N
+
+    /**
+     * Classifier for an artifact that contains test code
+     */
+    public static final String CLASSIFIER_TESTS = "tests"; // NOI18N
+
+    /**
+     * Classifier for an artifact that contains test sources.
+     */
+    public static final String CLASSIFIER_TEST_SOURCES = "test-sources"; // NOI18N
+    
     static final Logger LOG = Logger.getLogger(ProjectDependencies.class.getName());
     
     /**
@@ -74,10 +125,14 @@
     private final String classifier;
     private final boolean optional;
     private final URI location;
+    
+    // note: tags is NOT a part of hascode / equals, as externally only the classifier
+    // is visible, e.g. to the build system.
+    private final Set<String> tags;
     private FileObject localFile;
     final T data;
 
-    ArtifactSpec(VersionKind kind, String groupId, String artifactId, String versionSpec, String type, String classifier, boolean optional, URI location, FileObject localFile, T impl) {
+    ArtifactSpec(VersionKind kind, String groupId, String artifactId, String versionSpec, String type, String classifier, boolean optional, URI location, FileObject localFile, Set<String> tags, T impl) {
         this.kind = kind;
         this.groupId = groupId;
         this.artifactId = artifactId;
@@ -88,6 +143,7 @@
         this.type = type;
         this.location = location;
         this.localFile = localFile;
+        this.tags = tags == null ? Collections.emptySet() : tags;
     }
 
     public T getData() {
@@ -120,6 +176,10 @@
         }
         return f == FileUtil.getConfigRoot() ? null : f;
     }
+    
+    public boolean hasTag(String tag) {
+        return tags.contains(tag);
+    }
 
     public URI getLocation() {
         return location;
@@ -238,23 +298,25 @@
                 // should not happen
             }
         }
-        return new ArtifactSpec<V>(VersionKind.REGULAR, groupId, artifactId, versionSpec, type, classifier, optional, uri, localFile, data);
+        return new ArtifactSpec<V>(VersionKind.REGULAR, groupId, artifactId, versionSpec, type, classifier, optional, uri, localFile, Collections.emptySet(), data);
     }
 
     public static <V> ArtifactSpec<V> createSnapshotSpec(
             @NonNull String groupId, @NonNull String artifactId, 
             @NullAllowed String type, @NullAllowed String classifier, 
             @NonNull String versionSpec, boolean optional, @NullAllowed FileObject localFile, @NonNull V data) {
-        URL u = URLMapper.findURL(localFile, URLMapper.EXTERNAL);
         URI uri = null;
-        if (u != null) {
-            try {
-                uri = u.toURI();
-            } catch (URISyntaxException ex) {
-                // should not happen
+        if (localFile != null) {
+            URL u = URLMapper.findURL(localFile, URLMapper.EXTERNAL);
+            if (u != null) {
+                try {
+                    uri = u.toURI();
+                } catch (URISyntaxException ex) {
+                    // should not happen
+                }
             }
         }
-        return new ArtifactSpec<V>(VersionKind.SNAPSHOT, groupId, artifactId, versionSpec, type, classifier, optional, uri, localFile, data);
+        return new ArtifactSpec<V>(VersionKind.SNAPSHOT, groupId, artifactId, versionSpec, type, classifier, optional, uri, localFile, Collections.emptySet(), data);
     }
     
     public static final <T> Builder<T> builder(String group, String artifact, String version, T projectData) {
@@ -272,6 +334,7 @@
         private boolean optional;
         private FileObject localFile;
         private URI location;
+        private Set<String> tags;
         
         public Builder(String groupId, String artifactId, String versionSpec, T data) {
             this.groupId = groupId;
@@ -299,6 +362,25 @@
             this.localFile = localFile;
             return this;
         }
+        
+        public Builder tag(String tag) {
+            if (tags == null) {
+                tags = new HashSet<>();
+            }
+            tags.add(tag);
+            return this;
+        }
+        
+        public Builder tags(String... tags) {
+            if (tags == null || tags.length == 0) {
+                return this;
+            } else {
+                for (String t : tags) {
+                    tag(t);
+                }
+                return this;
+            }
+        }
 
         /**
          * Forces the local file reference. Unlike {@link #localFile}, if {@code null} is
@@ -319,7 +401,7 @@
         }
         
         public ArtifactSpec build() {
-            return new ArtifactSpec(kind, groupId, artifactId, versionSpec, type, classifier, optional, location, localFile, data);
+            return new ArtifactSpec(kind, groupId, artifactId, versionSpec, type, classifier, optional, location, localFile, tags, data);
         }
     }
 }
diff --git a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectArtifactsQuery.java b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectArtifactsQuery.java
index f92840b..1d8a41b 100644
--- a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectArtifactsQuery.java
+++ b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectArtifactsQuery.java
@@ -19,9 +19,13 @@
 package org.netbeans.modules.project.dependency;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
@@ -43,7 +47,15 @@
  * <p>
  * By default the query will return artifacts produced by project's compilation (incl. packaging, in maven terminology) - 
  * but the exact meaning depends on a build system used, and the project's settings and the active configuration.
- * 
+ * <p>
+ * Different project output are marked by different <b>classifiers</b>. Some special, abstract classifiers may
+ * be defined that should be handled by implementations specific for each build system.
+ * <ul>
+ * <li>{@link Filter#CLASSIFIER_BUNDLED} - describes a product with all dependencies included, such as the output of
+ * <b>shade or shadow plugins</b> in Gradle or Maven. Since the plugins behave differently (maven replaces the original artifact,
+ * while gradle attaches a new one), this meta-classiifer allows to pick the appropriate artifact despite its real classifier depends
+ * on project type. If there are more bundles (shadows), 
+ * </ul>
  * @author sdedic
  */
 public final class ProjectArtifactsQuery {
@@ -140,6 +152,7 @@
         
         List<ArtifactSpec> updateResults() {
             boolean changes = false;
+            // accept only first matching artifact.
             Collection<ArtifactSpec> specs = new LinkedHashSet<>();
             for (E<?> e : delegates) {
                 Collection<ArtifactSpec> ex = e.findExcludedArtifacts();
@@ -213,25 +226,27 @@
      * perhaps determined by the configured packaging with <b>no classifier</b>. It it possible
      * to list artifacts of all types and/or artifacts with any classifier in one query.
      */
-    public static class Filter {
+    public static final class Filter {
         /**
          * Represents all types of artifacts. The query will return all build products
          */
-        public static final String TYPE_ALL = "all"; // NOI18N
+        public static final String TYPE_ALL = "<all>"; // NOI18N
         
         /**
          * Will return artifacts with any classifier.
          */
-        public static final String CLASSIFIER_ANY = "any"; // NOI18N
+        public static final String CLASSIFIER_ANY = "<any>"; // NOI18N
         
+        private final Set<String> tags;
         private final String classifier;
         private final String artifactType;
         private final ProjectActionContext  buildContext;
         
-        Filter(String artifactType, String classifier, ProjectActionContext buildContext) {
+        Filter(String artifactType, String classifier, Set<String> tags, ProjectActionContext buildContext) {
             this.classifier = classifier;
             this.artifactType = artifactType;
             this.buildContext = buildContext;
+            this.tags = tags == null ? Collections.emptySet() : Collections.unmodifiableSet(tags);
         }
 
         /**
@@ -245,6 +260,14 @@
         public String getClassifier() {
             return classifier;
         }
+        
+        public Set<String> getTags() {
+            return tags;
+        }
+        
+        public boolean hasTag(String t) {
+            return tags.contains(t);
+        }
 
         /**
          * The desired artifact type. Only artifacts with tha type will be returned. {@link #TYPE_ALL} means that artifacts
@@ -274,7 +297,7 @@
      */
     @NonNull
     public static Filter newQuery(@NullAllowed String artifactType) {
-        return new Filter(artifactType, null, null);
+        return new Filter(artifactType, null, null, null);
     }
     
     /**
@@ -286,6 +309,20 @@
      */
     @NonNull
     public static Filter newQuery(@NullAllowed String artifactType, @NullAllowed String classifier, @NullAllowed ProjectActionContext buildContext) {
-        return new Filter(artifactType, classifier, buildContext);
+        return new Filter(artifactType, classifier, null, buildContext);
+    }
+
+    /**
+     * Creates a Filter with the specified properties
+     * @param artifactType the desired type; use {@code null} for the default artifact type (i.e. defined by packaging)
+     * @param classifier the desired classifier; use {@code null} for no classifier
+     * @param buildContext the action context
+     * @return Filter instance.
+     */
+    @NonNull
+    public static Filter newQuery(@NullAllowed String artifactType, @NullAllowed String classifier, @NullAllowed ProjectActionContext buildContext, String... tags) {
+        return new Filter(artifactType, classifier, 
+                tags == null || tags.length == 0 ? null : new HashSet<>(Arrays.asList(tags)), 
+                buildContext);
     }
 }
diff --git a/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectArtifactsImplementation.java b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectArtifactsImplementation.java
index 198350a..339cc78 100644
--- a/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectArtifactsImplementation.java
+++ b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectArtifactsImplementation.java
@@ -41,7 +41,8 @@
     public Result evaluate(ProjectArtifactsQuery.Filter query);
 
     /**
-     * Returns evaluation order of this Implementation. If the Implementation needs to post-process
+     * Returns evaluation order of this Implementation. Implementations ordered
+     * later may remove artifacts generated by earlier ones.
      * 
      * @return 
      */
diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectMetadataCommand.java b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectMetadataCommand.java
index 2cc483e..eabfd06 100644
--- a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectMetadataCommand.java
+++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectMetadataCommand.java
@@ -22,6 +22,8 @@
 import com.google.gson.FieldAttributes;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.google.gson.TypeAdapter;
 import com.google.gson.TypeAdapterFactory;
@@ -132,7 +134,7 @@
     public Set<String> getCommands() {
         return COMMANDS;
     }
-
+    
     @Override
     public CompletableFuture<Object> processCommand(NbCodeLanguageClient client, String command, List<Object> arguments) {
         if (arguments.size() < 1) {
@@ -140,29 +142,79 @@
         }
         FileObject f = Utils.extractFileObject(arguments.get(0), gson);
         Project p = FileOwnerQuery.getOwner(f);
-        if (p == null || p.getProjectDirectory() != f) {
+        if (p == null) {
             throw new IllegalArgumentException("Not a project " + f);
         }
         String artifactType = null;
         ProjectActionContext ctx = null;
+        String[] tags = null;
+        String classifier = null;
         
         if (arguments.size() > 1) {
             // 2nd parameter is the project action
             Object o = arguments.get(1);
-            if (!(o instanceof JsonPrimitive)) {
-                throw new IllegalArgumentException("String or null expected as parameter #3, got " + o);
+            if (o instanceof JsonObject) {
+                JsonObject request = (JsonObject)o;
+                if (request.has("action")) {
+                    Object a = request.get("action");
+                    if (a instanceof JsonPrimitive) {
+                        ctx = ProjectActionContext.newBuilder(p).forProjectAction(((JsonPrimitive)a).getAsString()).context();
+                    } else {
+                        throw new IllegalArgumentException("String expected as action, got " + a);
+                    }
+                }
+                if (request.has("type")) {
+                    Object t = request.get("type");
+                    if (t instanceof JsonPrimitive) {
+                        artifactType = ((JsonPrimitive)t).getAsString();
+                    } else {
+                        throw new IllegalArgumentException("String expected as type, got " + t);
+                    }
+                }
+                if (request.has("classifier")) {
+                    Object c = request.get("classifier");
+                    if (c instanceof JsonPrimitive) {
+                        classifier = ((JsonPrimitive)c).getAsString();
+                    } else {
+                        throw new IllegalArgumentException("String expected as classifier, got " + c);
+                    }
+                }
+                if (request.has("tags")) {
+                    Object t = request.get("tags");
+                    if (t instanceof JsonPrimitive) {
+                        tags = new String[] { ((JsonPrimitive)t).getAsString() };
+                    } else if (t instanceof JsonArray) {
+                        JsonArray arr = (JsonArray)t;
+                        tags = new String[arr.size()];
+                        int index = 0;
+                        for (Object item : arr) {
+                            if (item instanceof JsonPrimitive) {
+                                tags[index++] = ((JsonPrimitive)item).getAsString();
+                            } else {
+                                throw new IllegalArgumentException("String expected as tag, got " + item);
+                            }
+                        }
+                    } else {
+                        throw new IllegalArgumentException("String or array expected as tags, got " + t);
+                    }
+                }
+                
+            } else if (o instanceof JsonPrimitive) {
+                ctx = ProjectActionContext.newBuilder(p).forProjectAction(((JsonPrimitive)o).getAsString()).context();
+            } else {
+                throw new IllegalArgumentException("String, structure, or null expected as parameter #2, got " + o);
             }
-            ctx = ProjectActionContext.newBuilder(p).forProjectAction(((JsonPrimitive)o).getAsString()).context();
+            
         }
         if (arguments.size() > 2) {
             // 3rd parameter is the type of artifact
             Object o = arguments.get(2);
             if (!(o instanceof JsonPrimitive)) {
-                throw new IllegalArgumentException("String or null expected as parameter #2, got " + o);
+                throw new IllegalArgumentException("String or null expected as parameter #3, got " + o);
             }
             artifactType = ((JsonPrimitive)o).getAsString();
         }
-        ProjectArtifactsQuery.Filter filter = ProjectArtifactsQuery.newQuery(artifactType, null, ctx);
+        ProjectArtifactsQuery.Filter filter = ProjectArtifactsQuery.newQuery(artifactType, classifier, ctx, tags);
         CompletableFuture result = new CompletableFuture();
         METADATA_PROCESSOR.post(() -> {
             try {
diff --git a/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java b/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java
index 8a6bfd1..b3fc45d 100644
--- a/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java
+++ b/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java
@@ -59,8 +59,9 @@
  */
 @ProjectServiceProvider(service = ProjectArtifactsImplementation.class, projectType = NbMavenProject.TYPE)
 public class MavenArtifactsImplementation implements ProjectArtifactsImplementation<MavenArtifactsImplementation.Res> {
+
     private static final Logger LOG = Logger.getLogger(ProjectArtifactsImplementation.class.getName());
-    
+
     private final Project project;
 
     public MavenArtifactsImplementation(Project project) {
@@ -129,27 +130,293 @@
     public boolean computeSupportsChanges(Res r) {
         return r.supportsChanges();
     }
-    
+
     static class MavenQuery {
+
         final Project project;
         final NbMavenProject nbMavenProject;
         final ProjectArtifactsQuery.Filter filter;
-        
+        MavenProject evalProject;
+
         List<ArtifactSpec> specs;
+        
+        boolean skipDefaultOutput;
 
         public MavenQuery(Project project, NbMavenProject nbMavenProject, ProjectArtifactsQuery.Filter filter) {
             this.project = project;
             this.nbMavenProject = nbMavenProject;
             this.filter = filter;
         }
+
+        void addArtifactSpec(ArtifactSpec spec) {
+            if (specs.contains(spec)) {
+                return;
+            }
+            specs.add(spec);
+        }
+
+        /**
+         * Evaluator that processes one execution of the shade plugin. Note that multiple artifacts may be produced in multiple
+         * executions, with different classifiers.
+         */
+        private class ShadedEvaluator {
+            /**
+             * Default classifier for attached artifacts produced by shade plugin
+             */
+            private static final String DEFAULT_SHADE_CLASSIFIER = "shaded";
+
+            private final MavenProject evalProject;
+            private final PluginExecution exec;
+            private String t;
+            private String c;
+
+            String classifier;
+            String outputDir;
+            String outputFile;
+            String finalName;
+            String artifactId;
+            boolean attached;
+            boolean createSourcesJar;
+            boolean createTestJar;
+            boolean createTestSourcesJar;
+            String outD;
+            String name;
+            Path basePath;
+            Artifact mA;
+            boolean any;
+            boolean tagBase;
+            boolean tagShaded;
+            boolean renamed;
+
+            public ShadedEvaluator(MavenProject evalProject, PluginExecution exec) {
+                this.evalProject = evalProject;
+                this.exec = exec;
+            }
+
+            public void process() {
+                Xpp3Dom dom = evalProject.getGoalConfiguration(
+                        Constants.GROUP_APACHE_PLUGINS, "maven-shade-plugin", exec.getId(), "shade"); // NOI18N
+                mA = evalProject.getArtifact();
+                t = filter.getArtifactType();
+                c = filter.getClassifier();
+                classifier = getValueOrNull(dom, "shadedClassifierName"); // NOI18N
+                outputDir = getValueOrNull(dom, "outputDirectory"); // NOI18N
+                outputFile = getValueOrNull(dom, "outputFile"); // NOI18N
+                finalName = getValueOrNull(dom, "finalName"); // NOI18N
+                artifactId = getValueOrNull(dom, "shadedArtifactId"); // NOI18N
+
+                attached = Boolean.valueOf(getValueOrNull(dom, "shadedArtifactAttached")); // NOI18N
+                createSourcesJar = Boolean.valueOf(getValueOrNull(dom, "createSourcesJar")); // NOI18N
+                createTestJar = Boolean.valueOf(getValueOrNull(dom, "shadeTestJar")); // NOI18N
+                createTestSourcesJar = Boolean.valueOf(getValueOrNull(dom, "createTestSourcesJar")); // NOI18N
+
+                outD = evalProject.getModel().getBuild().getDirectory();
+                name = evalProject.getBuild().getFinalName();
+
+                if (artifactId == null) {
+                    artifactId = mA.getArtifactId();
+                }
+
+                if (outputDir != null) {
+                    outD = outputDir;
+                }
+                if (finalName != null && finalName.length() > 0) {
+                    name = finalName;
+                }
+                basePath = Paths.get(outD, name);
+                any = ProjectArtifactsQuery.Filter.CLASSIFIER_ANY.equals(c);
+
+                tagShaded = filter.hasTag(ArtifactSpec.TAG_SHADED) || filter.hasTag(classifier);
+                tagBase = filter.hasTag(ArtifactSpec.TAG_BASE);
+
+                // either the caller selects the classifier, or no classifier (this produces artifact tagged with 'shaded' or
+                // explicitly tagged with original to get the unshaded version
+                if (attached) {
+                    if (classifier == null) {
+                        classifier = DEFAULT_SHADE_CLASSIFIER;
+                    }
+                }
+                boolean classifierMatch = any || (classifier != null && classifier.equals(c)) || (c == null && !attached) || tagBase;
+                String suffix;
+
+                String gID = mA.getGroupId();
+                if (outputFile != null) {
+                    addExplicitOutputFile();
+                    suffix = "-" + classifier;
+                } else {
+                    if (name != null && !name.equals(evalProject.getBuild().getFinalName())) {
+                        renamed = true;
+                        basePath = basePath.resolveSibling(name);
+                        suffix = "";
+                    } else {
+                        suffix = "";
+                    }
+                    if (attached) {
+                        suffix = "-" + classifier;
+                    }
+                    if (classifierMatch) {
+                        // do not report base unless the base is explicitly requested
+                        if ((any && !tagShaded) || tagBase) {
+                            skipDefaultOutput = !attached;
+                            Path file = basePath.resolveSibling("original-" + basePath.getFileName() + suffix + ".jar");
+                            ArtifactSpec spec = ArtifactSpec.builder(attached ? gID : null, attached ? artifactId : null, mA.getVersion(), evalProject).
+                                    classifier(classifier).
+                                    location(file.toUri()).
+                                    type("jar").
+                                    tag(ArtifactSpec.TAG_BASE).
+                                    build();
+                            addArtifactSpec(spec);
+                        }
+                        boolean reportShaded;
+                        if (any) {
+                            reportShaded = !tagBase || tagShaded;
+                        } else {
+                            // if no tag is present the default is to report the shaded artifact
+                            reportShaded = !tagBase;
+                        }
+                        if (reportShaded) {
+                            Path file = basePath.resolveSibling(basePath.getFileName() + suffix + ".jar");
+                            ArtifactSpec spec = ArtifactSpec.builder(gID, artifactId, mA.getVersion(), evalProject).
+                                    classifier(classifier).
+                                    location(file.toUri()).
+                                    type("jar").
+                                    tag(ArtifactSpec.TAG_SHADED).
+                                    build();
+                            addArtifactSpec(spec);
+                        }
+                    }
+                }
+
+                if (classifierMatch) {
+                    if (createSourcesJar) {
+                        addClassifiedArtifact("sources", suffix, "sources", ArtifactSpec.CLASSIFIER_SOURCES, "java-source");
+                    }
+                    if (createTestJar) {
+                        addClassifiedArtifact("test-jar", suffix, "tests", ArtifactSpec.CLASSIFIER_TESTS, "test");
+                    }
+                    if (createTestSourcesJar) {
+                        addClassifiedArtifact("sources", suffix, "test-sources", ArtifactSpec.CLASSIFIER_TEST_SOURCES, "java-source", "test");
+                    }
+                }
+            }
+
+            private void addClassifiedArtifact(String type, String suffix, String typeSuffix, String defaultClassifier, String... tags) {
+                if (!(ProjectArtifactsQuery.Filter.TYPE_ALL.equals(t) || type.equals(t) || (any && t == null))) {
+                    return;
+                }
+
+                Path file;
+                ArtifactSpec spec;
+                
+                String clas = classifier;
+                if (clas == null) {
+                    clas = defaultClassifier;
+                }
+                
+                if (any || !tagBase || tagShaded) {
+                    file = basePath.resolveSibling(basePath.getFileName() + suffix + "-" + typeSuffix + ".jar");
+                    spec = ArtifactSpec.builder(mA.getGroupId(), artifactId, mA.getVersion(), evalProject).
+                        classifier(clas).
+                        location(file.toUri()).
+                        type(type).
+                        tag(ArtifactSpec.TAG_SHADED).
+                        tags(tags).
+                        build();
+                    addArtifactSpec(spec);
+                }
+
+                if (any || tagBase) {
+                    file = basePath.resolveSibling("original-" + basePath.getFileName() + suffix + "-" + typeSuffix + ".jar");
+                    spec = ArtifactSpec.builder(attached ? mA.getGroupId() : null, attached ? artifactId : null, mA.getVersion(), evalProject).
+                            classifier(clas).
+                            location(file.toUri()).
+                            type(type).
+                            tag(ArtifactSpec.TAG_BASE).
+                            tags(tags).
+                            build();
+                    addArtifactSpec(spec);
+                }
+            }
+
+            /**
+             * Include only if shaded was explicitly requested, or ALL
+             * classifiers. Otherwise the shaded artifact does not replace the
+             * main artifact and is not 'officially' attached, so it should be
+             * probably not reported at all, as it is invisible outside the
+             * project.
+             */
+            void addExplicitOutputFile() {
+                if ((ProjectArtifactsQuery.Filter.TYPE_ALL.equals(t) || t == null || "jar".equals(t))
+                        && (any || (c == null && filter.hasTag(ArtifactSpec.TAG_SHADED)))) {
+                    Path file = null;
+                    try {
+                        file = Paths.get(outputFile);
+                        ArtifactSpec spec = ArtifactSpec.builder(null, null, mA.getVersion(), evalProject).
+                                classifier(classifier).
+                                location(file.toUri()).
+                                type("jar").
+                                tag(ArtifactSpec.TAG_SHADED).
+                                build();
+                        addArtifactSpec(spec);
+                    } catch (InvalidPathException ex) {
+                        // no main artifact produced.
+                    }
+                } else {
+
+                }
+                if ((ProjectArtifactsQuery.Filter.TYPE_ALL.equals(t) || t == null || "jar".equals(t))
+                        && (any || (c == null && (filter.hasTag(ArtifactSpec.TAG_BASE) || filter.hasTag(DEFAULT_SHADE_CLASSIFIER))))) {
+                    // include the original, but tag it with base
+                    Path p = basePath.resolveSibling("original-" + basePath.getFileName() + ".jar"); // NOI18N
+                    ArtifactSpec spec = ArtifactSpec.builder(null, null, mA.getVersion(), evalProject).
+                            classifier(classifier).
+                            location(p.toUri()).
+                            type("jar").
+                            tag(ArtifactSpec.TAG_BASE).
+                            build();
+                    addArtifactSpec(spec);
+                }
+                // the other possible attachments are not afffected by 'outputFile'
+            }
+
+        }
+
+        private void appendShadePluginOutput(MavenProject evalProject) {
+            Plugin plugin = evalProject.getBuild().getPluginsAsMap().get(Constants.GROUP_APACHE_PLUGINS + ":" + "maven-shade-plugin"); // NOI18N
+            if (plugin == null) {
+                return;
+            }
+            for (PluginExecution exec : plugin.getExecutions()) {
+                if (exec.getGoals().contains("shade")) {
+                    ShadedEvaluator shadedEval = new ShadedEvaluator(evalProject, exec);
+                    shadedEval.process();
+                }
+            }
+        }
+
+        private static String getValueOrNull(Xpp3Dom parent, String childName) {
+            return getChildValue(parent, childName, null);
+        }
+
+        private static String getChildValue(Xpp3Dom parent, String childName, String defValue) {
+            Xpp3Dom child = null;
+            if (parent != null) {
+                child = parent.getChild(childName);
+            }
+            return child != null ? child.getValue() : defValue;
+        }
         
         private void appendPluginOutput(MavenProject evalProject, String pluginId, String goal, String packagingAndType) {
+            appendPluginOutput(evalProject, pluginId, goal, packagingAndType, packagingAndType, null);
+        }
+
+        private void appendPluginOutput(MavenProject evalProject, String pluginId, String goal, String type, String packaging, String defaultClassifier, String... tags) {
             if (filter != null) {
                 if (filter.getArtifactType() == null) {
-                    if (!evalProject.getPackaging().equals(packagingAndType)) {
+                    if (!evalProject.getPackaging().equals(packaging)) {
                         return;
                     }
-                } else if (!filter.getArtifactType().equals(packagingAndType)) {
+                } else if (!filter.getArtifactType().equals(type)) {
                     if (!ProjectArtifactsQuery.Filter.TYPE_ALL.equals(filter.getArtifactType())) {
                         return;
                     }
@@ -157,12 +424,12 @@
             }
             Artifact mA = evalProject.getArtifact();
             Model mdl = evalProject.getModel();
-            
+
             Plugin plugin = evalProject.getBuild().getPluginsAsMap().get(Constants.GROUP_APACHE_PLUGINS + ":" + pluginId); // NOI18N
             if (plugin == null) {
                 return;
             }
-            
+
             for (PluginExecution exec : plugin.getExecutions()) {
                 if (exec.getGoals().contains(goal)) {
                     Xpp3Dom dom = evalProject.getGoalConfiguration(
@@ -171,6 +438,9 @@
                     Xpp3Dom domOutputDir = dom == null ? null : dom.getChild("outputDirectory"); // NOI18N
 
                     String classifier = domClassifier == null ? null : domClassifier.getValue();
+                    if (classifier == null) {
+                        classifier = defaultClassifier;
+                    }
 
                     if (filter != null && !ProjectArtifactsQuery.Filter.CLASSIFIER_ANY.equals(filter.getClassifier())) {
                         if (!Objects.equals(classifier, filter.getClassifier())) {
@@ -178,15 +448,16 @@
                         }
                     }
                     StringBuilder finalNameExt = new StringBuilder(mdl.getBuild().getFinalName());
-                    if (domClassifier != null) {
-                        finalNameExt.append("-").append(domClassifier.getValue());
+                    if (classifier != null) {
+                        finalNameExt.append("-").append(classifier);
                     }
-                    finalNameExt.append(".").append(packagingAndType);
+                    finalNameExt.append(".").append(packaging);
 
-                    ArtifactSpec.Builder builder = ArtifactSpec.builder(mA.getGroupId(), mA.getArtifactId(), mA.getVersion(), 
+                    ArtifactSpec.Builder builder = ArtifactSpec.builder(mA.getGroupId(), mA.getArtifactId(), mA.getVersion(),
                             nbMavenProject.getMavenProject().getArtifact())
                             .classifier(classifier)
-                            .type(packagingAndType);
+                            .tags(tags)
+                            .type(type);
                     try {
                         Path dir = Paths.get(mdl.getBuild().getDirectory());
                         if (domOutputDir != null) {
@@ -202,16 +473,16 @@
                     } catch (InvalidPathException ex) {
                         // TODO: log 
                     }
-                    specs.add(builder.build());
+                    addArtifactSpec(builder.build());
                 }
             }
         }
-        
+
         public void run() {
             specs = new ArrayList<>();
             Model mdl;
             ProjectActionContext buildCtx;
-            
+
             if (filter != null && filter.getBuildContext() != null) {
                 if (filter.getBuildContext().getProjectAction() == null) {
                     buildCtx = filter.getBuildContext().newDerivedBuilder().forProjectAction(ActionProvider.COMMAND_BUILD).context();
@@ -223,37 +494,47 @@
             }
             MavenProject mp = nbMavenProject.getEvaluatedProject(buildCtx);
             mdl = mp.getModel();
-            
+            evalProject = mp;
+
             String packaging = mdl.getPackaging();
             if (packaging == null) {
                 packaging = NbMavenProject.TYPE_JAR;
             }
             
-            appendPluginOutput(mp, Constants.PLUGIN_JAR, NbMavenProject.TYPE_JAR, NbMavenProject.TYPE_JAR);
+            appendShadePluginOutput(evalProject);
+
+            if (!skipDefaultOutput) {
+                appendPluginOutput(mp, Constants.PLUGIN_JAR, NbMavenProject.TYPE_JAR, NbMavenProject.TYPE_JAR);
+            }
+            appendPluginOutput(mp, Constants.PLUGIN_JAR, "test-jar", "test-jar", NbMavenProject.TYPE_JAR, ArtifactSpec.CLASSIFIER_TESTS);
+            appendPluginOutput(mp, "maven-source-plugin", "jar", "sources", NbMavenProject.TYPE_JAR, ArtifactSpec.CLASSIFIER_SOURCES, "java-source");
+            appendPluginOutput(mp, "maven-source-plugin", "test-jar", "sources", NbMavenProject.TYPE_JAR, ArtifactSpec.CLASSIFIER_TEST_SOURCES, "java-source", "test");
+            
             appendPluginOutput(mp, Constants.PLUGIN_WAR, NbMavenProject.TYPE_WAR, NbMavenProject.TYPE_WAR);
             appendPluginOutput(mp, Constants.PLUGIN_EAR, NbMavenProject.TYPE_EAR, NbMavenProject.TYPE_EAR);
             appendPluginOutput(mp, Constants.PLUGIN_EJB, NbMavenProject.TYPE_EJB, NbMavenProject.TYPE_EJB);
         }
     }
-    
+
     private static final RequestProcessor MAVEN_ARTIFACTS_RP = new RequestProcessor(MavenArtifactsImplementation.class);
 
     static class Res implements PropertyChangeListener {
+
         private final Project project;
         private final ProjectArtifactsQuery.Filter filter;
-        
+
         // @GuardedBy(this)
         private List<ArtifactSpec> artifacts;
         // @GuardedBy(this)
         private List<ChangeListener> listeners;
-        
+
         private RequestProcessor.Task refreshTask;
-        
+
         public Res(Project project, ProjectArtifactsQuery.Filter filter) {
             this.project = project;
             this.filter = filter;
         }
-        
+
         public Project getProject() {
             return project;
         }
@@ -274,12 +555,12 @@
             }
             return q.specs;
         }
-        
+
         private void update(List<ArtifactSpec> copy, RequestProcessor.Task self) {
             NbMavenProject mvnProject = project.getLookup().lookup(NbMavenProject.class);
             MavenQuery q = new MavenQuery(project, mvnProject, filter);
             q.run();
-            
+
             ChangeListener[] ll;
             synchronized (this) {
                 if (artifacts == null) {
@@ -296,7 +577,7 @@
                 l.stateChanged(e);
             }
         }
-        
+
         public Collection<ArtifactSpec> getExcludedArtifacts() {
             return null;
         }
@@ -327,7 +608,7 @@
             }
             ChangeListener[] ll;
             final List<ArtifactSpec> copy;
-            
+
             synchronized (this) {
                 artifacts = null;
                 if (listeners == null && listeners.isEmpty()) {
@@ -338,9 +619,9 @@
                 }
                 copy = artifacts == null ? Collections.emptyList() : new ArrayList<>(this.artifacts);
                 RequestProcessor.Task[] arr = new RequestProcessor.Task[1];
-                
+
                 arr[0] = refreshTask = MAVEN_ARTIFACTS_RP.create(() -> update(copy, arr[0]));
-                
+
                 ll = listeners.toArray(new ChangeListener[listeners.size()]);
             }
             ChangeEvent e = new ChangeEvent(this);
diff --git a/java/maven/test/unit/data/artifacts/shaded-attached/pom.xml b/java/maven/test/unit/data/artifacts/shaded-attached/pom.xml
new file mode 100644
index 0000000..281dd69
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-attached/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>nbtest.grp</groupId>
+    <artifactId>shaded-attached</artifactId>
+    <version>16</version>
+
+    <name>Shaded Test</name>
+    <description>Tests artifacts from shade plugin</description>
+    
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.36</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>1.7.36</version>
+            <scope>runtime</scope>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.3.0</version>
+                <configuration>
+                    <shadedArtifactAttached>true</shadedArtifactAttached>
+                </configuration>
+                <executions>
+                  <execution>
+                    <id>default-shade</id>
+                    <phase>package</phase>
+                    <goals>
+                      <goal>shade</goal>
+                    </goals>
+                  </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+
diff --git a/java/maven/test/unit/data/artifacts/shaded-attached/src/main/java/SampleApplication.java b/java/maven/test/unit/data/artifacts/shaded-attached/src/main/java/SampleApplication.java
new file mode 100644
index 0000000..0db334b
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-attached/src/main/java/SampleApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ *
+ * @author sdedic
+ */
+public class SampleApplication {
+    
+}
diff --git a/java/maven/test/unit/data/artifacts/shaded-default/pom.xml b/java/maven/test/unit/data/artifacts/shaded-default/pom.xml
new file mode 100644
index 0000000..f836baf
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-default/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>nbtest.grp</groupId>
+    <artifactId>shaded-default</artifactId>
+    <version>16</version>
+
+    <name>Shaded Test</name>
+    <description>Tests artifacts from shade plugin</description>
+    
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.36</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>1.7.36</version>
+            <scope>runtime</scope>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.3.0</version>
+                <executions>
+                  <execution>
+                    <id>default-shade</id>
+                    <phase>package</phase>
+                    <goals>
+                      <goal>shade</goal>
+                    </goals>
+                  </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+
diff --git a/java/maven/test/unit/data/artifacts/shaded-default/src/main/java/SampleApplication.java b/java/maven/test/unit/data/artifacts/shaded-default/src/main/java/SampleApplication.java
new file mode 100644
index 0000000..0db334b
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-default/src/main/java/SampleApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ *
+ * @author sdedic
+ */
+public class SampleApplication {
+    
+}
diff --git a/java/maven/test/unit/data/artifacts/shaded-sources/pom.xml b/java/maven/test/unit/data/artifacts/shaded-sources/pom.xml
new file mode 100644
index 0000000..d0aa1ca
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-sources/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>nbtest.grp</groupId>
+    <artifactId>shaded-sources</artifactId>
+    <version>16</version>
+
+    <name>Shaded Test</name>
+    <description>Tests artifacts from shade plugin</description>
+    
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.36</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>1.7.36</version>
+            <scope>runtime</scope>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.2.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.2.2</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>            
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.3.0</version>
+                <configuration>
+                    <shadeTestJar>true</shadeTestJar>
+                    <createSourcesJar>true</createSourcesJar>
+                    <createTestSourcesJar>true</createTestSourcesJar>
+                </configuration>
+                <executions>
+                  <execution>
+                    <id>default-shade</id>
+                    <phase>package</phase>
+                    <goals>
+                      <goal>shade</goal>
+                    </goals>
+                  </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+
diff --git a/java/maven/test/unit/data/artifacts/shaded-sources/src/main/java/SampleApplication.java b/java/maven/test/unit/data/artifacts/shaded-sources/src/main/java/SampleApplication.java
new file mode 100644
index 0000000..0db334b
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/shaded-sources/src/main/java/SampleApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ *
+ * @author sdedic
+ */
+public class SampleApplication {
+    
+}
diff --git a/java/maven/test/unit/data/artifacts/sources/pom.xml b/java/maven/test/unit/data/artifacts/sources/pom.xml
new file mode 100644
index 0000000..dbf3dd3
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/sources/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>nbtest.grp</groupId>
+    <artifactId>sources</artifactId>
+    <version>16</version>
+
+    <name>Shaded Test</name>
+    <description>Tests artifacts from shade plugin</description>
+    
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.36</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>1.7.36</version>
+            <scope>runtime</scope>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.2.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.2.2</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>            
+            <!--
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.3.0</version>
+                <configuration>
+                    <shadeTestJar>true</shadeTestJar>
+                    <createSourcesJar>true</createSourcesJar>
+                    <createTestSourcesJar>true</createTestSourcesJar>
+                </configuration>
+                <executions>
+                  <execution>
+                    <id>default-shade</id>
+                    <phase>package</phase>
+                    <goals>
+                      <goal>shade</goal>
+                    </goals>
+                  </execution>
+                </executions>
+            </plugin>
+            -->
+        </plugins>
+    </build>
+</project>
+
diff --git a/java/maven/test/unit/data/artifacts/sources/src/main/java/SampleApplication.java b/java/maven/test/unit/data/artifacts/sources/src/main/java/SampleApplication.java
new file mode 100644
index 0000000..0db334b
--- /dev/null
+++ b/java/maven/test/unit/data/artifacts/sources/src/main/java/SampleApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ *
+ * @author sdedic
+ */
+public class SampleApplication {
+    
+}
diff --git a/java/maven/test/unit/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementationTest.java b/java/maven/test/unit/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementationTest.java
index 5837fb0..c7de3ac 100644
--- a/java/maven/test/unit/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementationTest.java
+++ b/java/maven/test/unit/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementationTest.java
@@ -20,6 +20,8 @@
 
 import java.io.File;
 import java.net.URI;
+import java.nio.file.Paths;
+import java.util.List;
 import java.util.concurrent.Exchanger;
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertNull;
@@ -27,7 +29,6 @@
 import org.netbeans.api.project.ProjectActionContext;
 import org.netbeans.api.project.ProjectManager;
 import org.netbeans.junit.NbTestCase;
-import org.netbeans.modules.maven.api.Constants;
 import org.netbeans.modules.maven.api.NbMavenProject;
 import org.netbeans.modules.maven.embedder.EmbedderFactory;
 import org.netbeans.modules.project.dependency.ArtifactSpec;
@@ -291,4 +292,357 @@
         assertEquals(0, ar.getArtifacts().size());
     }
 
+    /**
+     * Checks that the default artifact for shaded plugin does not change, but is annotated
+     * by an appropriate tag.
+     */
+    public void testShadedDefault() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-default/");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-default");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR)
+        );
+        
+        assertNotNull(ar);
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        
+        ArtifactSpec uberJar = specs.get(0);
+        assertReplacementUberjar(uberJar);
+    }
+    
+    /**
+     * Checks that the original jar can be queried, but has no GAV.
+     */
+    public void testShadedDefaultOriginal() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-default/");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-default");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR, null, null, ArtifactSpec.TAG_BASE)
+        );
+        
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec origJar = specs.get(0);
+        assertNotAttachedOriginal(origJar);
+    }
+    
+    private void assertReplacementUberjar(ArtifactSpec uberJar) {
+        assertReplacementUberjar(uberJar, "shaded-default");
+    }
+    
+    private void assertReplacementUberjar(ArtifactSpec uberJar, String prjName) {
+        assertEquals("Uber-jar has a proper artifactId", prjName, uberJar.getArtifactId());
+        assertEquals("Uber-jar has a proper groupId", "nbtest.grp", uberJar.getGroupId());
+        assertNull("Uber-jar should have no classifier", uberJar.getClassifier());
+        assertTrue(uberJar.hasTag("<shaded>"));
+        assertFalse(uberJar.hasTag(ArtifactSpec.TAG_BASE));
+    }
+    
+    void assertNotAttachedOriginal(ArtifactSpec origJar) {
+        assertNull("Orig-jar has no artifactId", origJar.getArtifactId());
+        assertNull("Orig-jar has no groupId", origJar.getGroupId());
+        assertNull("Orig-jar should have no classifier", origJar.getClassifier());
+        assertTrue(origJar.hasTag(ArtifactSpec.TAG_BASE));
+        assertFalse(origJar.hasTag("<shaded>"));
+    }
+    
+    /**
+     * Checks that 'any classifier' produces all jars, original and shaded.
+     */
+    public void testShadedAllCodeJars() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-default/");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-default");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR, ProjectArtifactsQuery.Filter.CLASSIFIER_ANY, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(2, specs.size());
+        
+        boolean shadedFound = false;
+        boolean origFound = false;
+        for (ArtifactSpec spec : specs) {
+            if (spec.hasTag(ArtifactSpec.TAG_BASE)) {
+                assertFalse("Single base artifact expected", origFound);
+                origFound = true;
+                assertNotAttachedOriginal(spec);
+            } else if (spec.hasTag(ArtifactSpec.TAG_SHADED)) {
+                assertFalse("Single shaded artifact expected", shadedFound);
+                shadedFound = true;
+                assertReplacementUberjar(spec);
+            } else {
+                fail("Artifact should be either base or shaded");
+            }
+        }
+    }
+    
+    /**
+     * Checks that for attached shaded artifact, the default output is not affected and has no tag.
+     */
+    public void testShadedAttachedDefaultOutput() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-attached");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-attached");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertDefaultArtifactWithAttached(out);
+    }
+
+    /**
+     * Checks that for attached shaded artifact, the default output is not affected and has no tag.
+     */
+    public void testShadedAttachedClassifiedOutput() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-attached");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-attached");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR, "shaded", null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedUberjar(out, "shaded");
+    }
+    
+    /**
+     * Checks that for attached shaded artifact, the default output is not affected and has no tag.
+     */
+    public void testShadedAttachedClassifiedOriginalOutput() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-attached");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-attached");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR, "shaded", null, ArtifactSpec.TAG_BASE)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedOriginal(out, "shaded");
+    }
+    
+    private void assertDefaultArtifactWithAttached(ArtifactSpec out) {
+        assertEquals("Output has a proper artifactId", "shaded-attached", out.getArtifactId());
+        assertEquals("Output has a proper groupId", "nbtest.grp", out.getGroupId());
+        assertNull("Output has no classifier", out.getClassifier());
+        assertFalse(out.hasTag("<shaded>"));
+        assertFalse(out.hasTag(ArtifactSpec.TAG_BASE));
+    }
+
+    private void assertAttachedUberjar(ArtifactSpec uberJar, String classifier) {
+        assertEquals("Uber-jar has a proper artifactId", "shaded-attached", uberJar.getArtifactId());
+        assertEquals("Uber-jar has a proper groupId", "nbtest.grp", uberJar.getGroupId());
+        assertEquals("Uber-jar has a classifier", classifier, uberJar.getClassifier());
+        assertTrue(uberJar.hasTag("<shaded>"));
+        assertFalse(uberJar.hasTag(ArtifactSpec.TAG_BASE));
+    }
+    
+    void assertAttachedOriginal(ArtifactSpec origJar, String classifier) {
+        assertEquals("Orig-jar has an artifactId", "shaded-attached", origJar.getArtifactId());
+        assertEquals("Orig-jar has a groupId", "nbtest.grp", origJar.getGroupId());
+        assertEquals("Orig-jar has a classifier", classifier, origJar.getClassifier());
+        assertTrue(origJar.hasTag(ArtifactSpec.TAG_BASE));
+        assertFalse(origJar.hasTag(ArtifactSpec.TAG_SHADED));
+    }
+    
+    /**
+     * Checks that for attached shaded artifact, the default output is not affected and has no tag.
+     */
+    public void testShadedAttachedClassifiedAllOutput() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-attached");
+        FileObject prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-attached");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(NbMavenProject.TYPE_JAR, ProjectArtifactsQuery.Filter.CLASSIFIER_ANY, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(3, specs.size());
+        for (ArtifactSpec out : specs) {
+            if (out.hasTag(ArtifactSpec.TAG_BASE)) {
+                assertAttachedOriginal(out, "shaded");
+            } else if (out.hasTag(ArtifactSpec.TAG_SHADED)) {
+                assertAttachedUberjar(out, "shaded");
+            } else {
+                assertDefaultArtifactWithAttached(out);
+            }
+        }
+    }
+    
+    FileObject prjCopy;
+    
+    private void assertAttachedClassifiedArtifact(String prjName, ArtifactSpec out, String classifier, String type) {
+        assertEquals("Output has a proper artifactId", prjName, out.getArtifactId());
+        assertEquals("Output has a proper groupId", "nbtest.grp", out.getGroupId());
+        assertEquals("Output has a classifier", classifier, out.getClassifier());
+        assertEquals("Output has a type", type, out.getType());
+        assertFalse(out.hasTag("<shaded>"));
+        assertFalse(out.hasTag(ArtifactSpec.TAG_BASE));
+        
+        String suffix = classifier == null ? "" : "-" + classifier;
+        
+        URI expected = FileUtil.toFile(prjCopy).toPath().resolve(Paths.get("target", prjName + "-16" + suffix + ".jar")).toUri();
+        assertEquals(expected, out.getLocation());
+    }
+    
+    boolean attached;
+
+    private void assertShadedAttachedClassifiedArtifact(String prjName, ArtifactSpec out, String classifier, String type, Boolean shaded) {
+        if (Boolean.TRUE == shaded) {
+            assertTrue(out.hasTag("<shaded>"));
+            assertFalse(out.hasTag(ArtifactSpec.TAG_BASE));
+            if (attached) {
+                classifier = classifier + "-shaded";
+            }
+        } else if (Boolean.FALSE == shaded) {
+            assertFalse(out.hasTag(ArtifactSpec.TAG_SHADED));
+            assertTrue(out.hasTag(ArtifactSpec.TAG_BASE));
+            prjName = "original-" + prjName;
+        } else {
+            assertFalse(out.hasTag(ArtifactSpec.TAG_SHADED));
+            assertFalse(out.hasTag(ArtifactSpec.TAG_BASE));
+        }
+        if (attached || Boolean.FALSE != shaded) {
+            assertEquals("Output has a proper artifactId", prjName, out.getArtifactId());
+            assertEquals("Output has a proper groupId", "nbtest.grp", out.getGroupId());
+        }
+        assertEquals("Output has a classifier", classifier, out.getClassifier());
+        assertEquals("Output has a type", type, out.getType());
+        
+        URI expected = FileUtil.toFile(prjCopy).toPath().resolve(Paths.get("target", prjName + "-16-" + classifier + ".jar")).toUri();
+        assertEquals(expected, out.getLocation());
+    }
+    
+    public void testSourceAttachment() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ArtifactSpec.CLASSIFIER_SOURCES, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedClassifiedArtifact("sources", out, "sources", "sources");
+    }
+
+    public void testTestsAttachment() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ArtifactSpec.CLASSIFIER_TESTS, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedClassifiedArtifact("sources", out, "tests", "test-jar");
+    }
+
+    public void testTestsSourcesAttachment() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ArtifactSpec.CLASSIFIER_TEST_SOURCES, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedClassifiedArtifact("sources", out, "test-sources", "sources");
+    }
+    
+    public void testDefaultAllAttachments() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ProjectArtifactsQuery.Filter.CLASSIFIER_ANY, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(4, specs.size());
+        for (ArtifactSpec out : specs) {
+            if (ArtifactSpec.CLASSIFIER_SOURCES.equals(out.getClassifier())) {
+                assertAttachedClassifiedArtifact("sources", out, "sources", "sources");
+                assertFalse(out.hasTag("test"));
+            } else if (ArtifactSpec.CLASSIFIER_TESTS.equals(out.getClassifier())) {
+                assertAttachedClassifiedArtifact("sources", out, "tests", "test-jar");
+            } else if (ArtifactSpec.CLASSIFIER_TEST_SOURCES.equals(out.getClassifier())) {
+                assertAttachedClassifiedArtifact("sources", out, "test-sources", "sources");
+                assertTrue(out.hasTag("test"));
+            } else {
+                assertAttachedClassifiedArtifact("sources", out, null, "jar");
+            }
+        }
+    }
+
+    public void testShadedSourceAttachment() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ArtifactSpec.CLASSIFIER_SOURCES, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(1, specs.size());
+        ArtifactSpec out = specs.get(0);
+        assertAttachedClassifiedArtifact("shaded-sources", out, "sources", "sources");
+        assertSame(prjCopy.getFileObject("target/shaded-sources-16-sources.jar"), out.getLocalFile());
+    }
+
+    public void testShadedAllAttachments() throws Exception {
+        FileObject testApp = dataFO.getFileObject("artifacts/shaded-sources");
+        prjCopy = FileUtil.copyFile(testApp, FileUtil.toFileObject(getWorkDir()), "shaded-sources");
+        
+        Project p = ProjectManager.getDefault().findProject(prjCopy);
+        ProjectArtifactsQuery.ArtifactsResult ar = ProjectArtifactsQuery.findArtifacts(p, 
+                ProjectArtifactsQuery.newQuery(null, ProjectArtifactsQuery.Filter.CLASSIFIER_ANY, null)
+        );
+        List<ArtifactSpec> specs = ar.getArtifacts();
+        assertEquals(8, specs.size());
+        for (ArtifactSpec out : specs) {
+            Boolean b;
+            if (out.hasTag(ArtifactSpec.TAG_SHADED)) {
+                b = true;
+            } else if (out.hasTag(ArtifactSpec.TAG_BASE)) {
+                b = false;
+            } else {
+                fail("Only base and tagged artifacts expected");
+                return; // not reached
+            }
+            if (ArtifactSpec.CLASSIFIER_SOURCES.equals(out.getClassifier())) {
+                assertShadedAttachedClassifiedArtifact("shaded-sources", out, "sources", "sources", b);
+                assertFalse(out.hasTag("test"));
+            } else if (ArtifactSpec.CLASSIFIER_TESTS.equals(out.getClassifier())) {
+                assertShadedAttachedClassifiedArtifact("shaded-sources", out, "tests", "test-jar", b);
+                assertTrue(out.hasTag("test"));
+            } else if (ArtifactSpec.CLASSIFIER_TEST_SOURCES.equals(out.getClassifier())) {
+                assertShadedAttachedClassifiedArtifact("shaded-sources", out, "test-sources", "sources", b);
+                assertTrue(out.hasTag("test"));
+            } else {
+                if (Boolean.TRUE == b) {
+                    assertReplacementUberjar(out, "shaded-sources");
+                } else if (Boolean.FALSE == b) {
+                    assertNotAttachedOriginal(out);
+                }
+            }
+        }
+    }
 }