ArtifactSpec supports tagging. Common classifiers and tags defined.
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 
      */