SLING-10763 : Provide a framework to check artifact versions
diff --git a/docs/api-regions.md b/docs/api-regions.md
index ea23e75..0713720 100644
--- a/docs/api-regions.md
+++ b/docs/api-regions.md
@@ -361,7 +361,7 @@
 
 ## Artifact Rules
 
-The artifact rules extension allows to specify version rules for bundles. For an artifact identity allowed and denied version ranges can be specified. A version range follows the OSGi version range syntax. If no ranges are specified, the artifact is not allowed. An artifact version must match at least one allowed version range and must not match any denied version range (if specified).
+The artifact rules extension allows to specify version rules for bundles and artifacts. For an artifact identity allowed and denied version ranges can be specified. A version range follows the OSGi version range syntax. If no ranges are specified, the artifact is not allowed. An artifact version must match at least one allowed version range and must not match any denied version range (if specified).
 
 ``` json
 "artifact-rules:JSON|optional" : {
@@ -373,8 +373,16 @@
           "allowed-version-ranges":["[2.0.4,3)"],
           "denied-version-ranges":["[2.1.1,2.1.1]]
       }
+  ],
+  "artifact-version-rules":[
+      {
+          "artifact-id" : "g:a:1", # version does not matter
+          "msg":"Use at least version 2.0.4 but avoid 2.1.1",
+          "allowed-version-ranges":["[2.0.4,3)"],
+          "denied-version-ranges":["[2.1.1,2.1.1]]
+      }
   ]
 }
 ```
 
-The mode, either LENIENT or STRICT (default) can be used to decide whether a warning or an error should be emitted.
\ No newline at end of file
+The mode, either LENIENT or STRICT (default) can be used to decide whether a warning or an error should be emitted.
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRules.java b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRules.java
index 0b24240..d35cc25 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRules.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRules.java
@@ -16,12 +16,16 @@
  */
 package org.apache.sling.feature.extension.apiregions.analyser;
 
-import org.apache.sling.feature.Artifact;
+import java.util.List;
+
+import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.analyser.task.AnalyserTask;
 import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.ArtifactRules;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.Mode;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.VersionRule;
+import org.apache.sling.feature.scanner.ArtifactDescriptor;
+import org.apache.sling.feature.scanner.BundleDescriptor;
 
 
 public class CheckArtifactRules implements AnalyserTask{
@@ -42,27 +46,34 @@
         if ( rules == null ) {
             context.reportExtensionWarning(ArtifactRules.EXTENSION_NAME, "Artifact rules are not specified, unable to validate feature");
         } else {
-            for(final Artifact bundle : context.getFeature().getBundles()) {
-                for(final VersionRule rule : rules.getBundleVersionRules()) {
-                    if ( rule.getArtifactId() != null && rule.getArtifactId().isSame(bundle.getId())) {
-                        if ( ! rule.isAllowed(bundle.getId().getOSGiVersion())) {
-                            String msg = rule.getMessage();
-                            if ( msg == null ) {
-                                msg = "Bundle with version " + bundle.getId().getVersion() + " is not allowed.";
-                            }
-                            Mode m = rules.getMode();
-                            if ( rule.getMode() != null ) {
-                                m = rule.getMode();
-                            }
-                            if ( m == Mode.LENIENT ) {
-                                context.reportArtifactWarning(bundle.getId(), msg);
-                            } else {
-                                context.reportArtifactError(bundle.getId(), msg);
-                            }
-                        }
+            for(final BundleDescriptor bundle : context.getFeatureDescriptor().getBundleDescriptors()) {
+                this.checkArtifact(context, rules.getBundleVersionRules(), rules.getMode(), bundle.getArtifact().getId());
+            }
+            for(final ArtifactDescriptor desc : context.getFeatureDescriptor().getArtifactDescriptors()) {
+                this.checkArtifact(context, rules.getArtifactVersionRules(), rules.getMode(), desc.getArtifact().getId());
+            }    
+        }
+	}
+
+    void checkArtifact(final AnalyserTaskContext context, final List<VersionRule> rules, final Mode defaultMode, final ArtifactId id) {
+        for(final VersionRule rule : rules) {
+            if ( rule.getArtifactId() != null && rule.getArtifactId().isSame(id)) {
+                if ( ! rule.isAllowed(id.getOSGiVersion())) {
+                    String msg = rule.getMessage();
+                    if ( msg == null ) {
+                        msg = "Artifact with version " + id.getVersion() + " is not allowed.";
+                    }
+                    Mode m = defaultMode;
+                    if ( rule.getMode() != null ) {
+                        m = rule.getMode();
+                    }
+                    if ( m == Mode.LENIENT ) {
+                        context.reportArtifactWarning(id, msg);
+                    } else {
+                        context.reportArtifactError(id, msg);
                     }
                 }
             }
         }
-	}
+    }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRules.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRules.java
index 945e674..bdea9f2 100755
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRules.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRules.java
@@ -111,9 +111,12 @@
     /** The validation mode */
     private Mode mode;
 
-    /** The version rules */
+    /** The version rules for bundles */
     private final List<VersionRule> bundleVersionRules = new ArrayList<>();
 
+    /** The version rules for artifacts */
+    private final List<VersionRule> artifactVersionRules = new ArrayList<>();
+
     /**
      * Create a new rules object
      */
@@ -134,6 +137,7 @@
     public void clear() {
         super.clear();
         this.getBundleVersionRules().clear();
+        this.getArtifactVersionRules().clear();
     }
 
     /**
@@ -157,6 +161,14 @@
             objBuilder.add(InternalConstants.KEY_BUNDLE_VERSION_RULES, arrayBuilder);
         }
 
+        if ( !this.getArtifactVersionRules().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final VersionRule rule : this.getArtifactVersionRules()) {
+                arrayBuilder.add(rule.createJson());
+            }
+            objBuilder.add(InternalConstants.KEY_ARTIFACT_VERSION_RULES, arrayBuilder);
+        }
+
         return objBuilder;
     }
 
@@ -185,6 +197,14 @@
                 }
             }
 
+            val = this.getAttributes().remove(InternalConstants.KEY_ARTIFACT_VERSION_RULES);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    final VersionRule rule = new VersionRule();
+                    rule.fromJSONObject(innerVal.asJsonObject());
+                    this.getArtifactVersionRules().add(rule);
+                }
+            }
         } catch (final JsonException | IllegalArgumentException e) {
             throw new IOException(e);
         }
@@ -214,4 +234,12 @@
     public List<VersionRule> getBundleVersionRules() {
         return this.bundleVersionRules;
     }
+
+    /**
+     * Return the list of version rules for artifacts. The returned list is mutable.
+     * @return the list of rules, might be empty.
+     */
+    public List<VersionRule> getArtifactVersionRules() {
+        return artifactVersionRules;
+    }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/InternalConstants.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/InternalConstants.java
index b158b4e..eb3ac01 100755
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/InternalConstants.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/artifacts/InternalConstants.java
@@ -32,4 +32,6 @@
     static final String KEY_MESSAGE = "message";
 
     static final String KEY_BUNDLE_VERSION_RULES = "bundle-version-rules";
+
+    static final String KEY_ARTIFACT_VERSION_RULES = "artifact-version-rules";
 }
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRulesTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRulesTest.java
index 15cf8d1..4689d91 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRulesTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckArtifactRulesTest.java
@@ -18,13 +18,25 @@
 
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 import org.apache.sling.feature.Artifact;
 import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.ArtifactRules;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.Mode;
 import org.apache.sling.feature.extension.apiregions.api.artifacts.VersionRule;
+import org.apache.sling.feature.scanner.ArtifactDescriptor;
+import org.apache.sling.feature.scanner.BundleDescriptor;
+import org.apache.sling.feature.scanner.FeatureDescriptor;
+import org.apache.sling.feature.scanner.impl.FeatureDescriptorImpl;
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -36,7 +48,25 @@
         final AnalyserTaskContext context = Mockito.mock(AnalyserTaskContext.class);
 
         when(context.getFeature()).thenReturn(f);
-        
+        final FeatureDescriptor fd = new FeatureDescriptorImpl(f);
+        when(context.getFeatureDescriptor()).thenReturn(fd);
+
+        for(final Artifact b : f.getBundles()) {
+            final BundleDescriptor bd = Mockito.mock(BundleDescriptor.class);
+            when(bd.getArtifact()).thenReturn(b);
+            fd.getBundleDescriptors().add(bd);
+        }
+
+        for(final Extension ext : f.getExtensions()) {
+            if ( ext.getType() == ExtensionType.ARTIFACTS ) {
+                for(final Artifact a : ext.getArtifacts()) {
+                    final ArtifactDescriptor bd = Mockito.mock(ArtifactDescriptor.class);
+                    when(bd.getArtifact()).thenReturn(a);
+                    fd.getArtifactDescriptors().add(bd);        
+                }
+            }
+        }
+
         return context;
     }
 
@@ -55,18 +85,30 @@
         final Artifact bundle = new Artifact(ArtifactId.parse("g:b:1.1"));
         f.getBundles().add(bundle);
 
+        final Extension ext = new Extension(ExtensionType.ARTIFACTS, "artifacts", ExtensionState.OPTIONAL);
+        f.getExtensions().add(ext);
+        final Artifact artifact = new Artifact(ArtifactId.parse("g:c:3.0"));
+        ext.getArtifacts().add(artifact);
+
         final VersionRule r = new VersionRule();
         r.setArtifactId(bundle.getId());
         r.setMode(Mode.STRICT);
         r.setMessage("foo");
 
+        final VersionRule r2 = new VersionRule();
+        r2.setArtifactId(artifact.getId());
+        r2.setMode(Mode.STRICT);
+        r2.setMessage("bar");
+
         final ArtifactRules rules = new ArtifactRules();
         rules.getBundleVersionRules().add(r);
+        rules.getArtifactVersionRules().add(r2);
         
         ArtifactRules.setArtifactRules(f, rules);
         final AnalyserTaskContext context = newContext(f);
         analyser.execute(context);
 
         Mockito.verify(context, Mockito.atLeastOnce()).reportArtifactError(Mockito.eq(bundle.getId()), Mockito.eq(r.getMessage()));
+        Mockito.verify(context, Mockito.atLeastOnce()).reportArtifactError(Mockito.eq(artifact.getId()), Mockito.eq(r2.getMessage()));
     }
 }
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRulesTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRulesTest.java
index 293fb29..e6d6e92 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRulesTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/artifacts/ArtifactRulesTest.java
@@ -72,16 +72,20 @@
         entity.getAttributes().put("a", Json.createValue(5));
         entity.setMode(Mode.LENIENT);
         entity.getBundleVersionRules().add(new VersionRule());
+        entity.getArtifactVersionRules().add(new VersionRule());
         entity.clear();
         assertTrue(entity.getAttributes().isEmpty());
         assertTrue(entity.getBundleVersionRules().isEmpty());
+        assertTrue(entity.getArtifactVersionRules().isEmpty());
         assertEquals(Mode.STRICT, entity.getMode());
     }
 
     @Test public void testFromJSONObject() throws IOException {
         final Extension ext = new Extension(ExtensionType.JSON, ArtifactRules.EXTENSION_NAME, ExtensionState.OPTIONAL);
         ext.setJSON("{ \"mode\" : \"LENIENT\", \"bundle-version-rules\":[{"+
-                "\"artifact-id\":\"g:a:1\",\"allowed-version-ranges\":[\"1.0.0\"]}]}");
+                "\"artifact-id\":\"g:a:1\",\"allowed-version-ranges\":[\"1.0.0\"]}]"+
+                ", \"artifact-version-rules\":[{"+
+                "\"artifact-id\":\"g:c:1\",\"allowed-version-ranges\":[\"2.0.0\"]}]}");
 
         final ArtifactRules entity = new ArtifactRules();
         entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
@@ -90,6 +94,10 @@
         assertEquals(ArtifactId.parse("g:a:1"), entity.getBundleVersionRules().get(0).getArtifactId());
         assertEquals(1, entity.getBundleVersionRules().get(0).getAllowedVersionRanges().length);
         assertEquals(new VersionRange("1.0.0"), entity.getBundleVersionRules().get(0).getAllowedVersionRanges()[0]);
+        assertEquals(1, entity.getArtifactVersionRules().size());
+        assertEquals(ArtifactId.parse("g:c:1"), entity.getArtifactVersionRules().get(0).getArtifactId());
+        assertEquals(1, entity.getArtifactVersionRules().get(0).getAllowedVersionRanges().length);
+        assertEquals(new VersionRange("2.0.0"), entity.getArtifactVersionRules().get(0).getAllowedVersionRanges()[0]);
     }
 
     @Test public void testToJSONObject() throws IOException {
@@ -100,9 +108,16 @@
         rule.setAllowedVersionRanges(new VersionRange[] {new VersionRange("1.0.0")});
         entity.getBundleVersionRules().add(rule);
 
+        final VersionRule artifactRule = new VersionRule();
+        artifactRule.setArtifactId(ArtifactId.parse("g:c:1"));
+        artifactRule.setAllowedVersionRanges(new VersionRange[] {new VersionRange("2.0.0")});
+        entity.getArtifactVersionRules().add(artifactRule);
+
         final Extension ext = new Extension(ExtensionType.JSON, ArtifactRules.EXTENSION_NAME, ExtensionState.OPTIONAL);
         ext.setJSON("{ \"mode\" : \"LENIENT\", \"bundle-version-rules\":[{"+
-                "\"artifact-id\":\"g:a:1\",\"allowed-version-ranges\":[\"1.0.0\"]}]}");
+                "\"artifact-id\":\"g:a:1\",\"allowed-version-ranges\":[\"1.0.0\"]}]"+
+                ", \"artifact-version-rules\":[{"+
+                "\"artifact-id\":\"g:c:1\",\"allowed-version-ranges\":[\"2.0.0\"]}]}");
 
         assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
     }