SLING-10471 : Allow giving and evaluating an optional removal date for deprecated package exports
diff --git a/README.md b/README.md
index 144c715..cbfd4ff 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@
 * `region-deprecated-api` : This analyser validates if packages marked as deprecated for a region are used. It has these configuration parameters:
   * `regions` : The regions to check for such usage. This is a comma separate string of region names. It defaults to `global`.
   * `strict` : By default the analyser issues warnings. If this is set to `true` errors are issued instead.
+  * `removal-period` : If deprecated api is used and that api has a `for-removal` information with a date set, then this configuration can be used to issue an error instead of a warning if the removal date is less than the configured number of days away. For example setting this to 28 will result in errors being generated four weeks ahead of the removal date.
 
 ## Extensions
 
diff --git a/docs/api-regions.md b/docs/api-regions.md
index fa164c6..b5a6bc0 100644
--- a/docs/api-regions.md
+++ b/docs/api-regions.md
@@ -139,7 +139,8 @@
                     "name":"org.apache.sling.incubator.api",
                     "deprecated":{
                         "msg":"This is deprecated",
-                        "since":"Since Sling left the incubator"
+                        "since":"Since Sling left the incubator",
+                        "for-removal":"2029-12-31"
                     }
                 }
             ]
@@ -147,6 +148,8 @@
      ]
 ```
 
+The deprecation informnation can just be the message, or it can also include information when the deprecated started (since) and by when the member is expected to be removed (for-removal). The removal information should be either the string `true` or a date in the format `YYYY-MM-DD`.
+
 ## OSGi Configurations
 
 Apart from defining the Java API surface, for some applications it is beneficial to describe the OSGi configuration surface, too. For example, a framework might not allow an application to set some configurations or update existing configurations.
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
index a1cd6a7..da74b2d 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sling.feature.extension.apiregions.analyser;
 
+import java.util.Calendar;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -29,6 +30,7 @@
 import org.apache.sling.feature.extension.apiregions.api.ApiExport;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
+import org.apache.sling.feature.extension.apiregions.api.DeprecationInfo;
 import org.apache.sling.feature.scanner.BundleDescriptor;
 import org.apache.sling.feature.scanner.PackageInfo;
 import org.osgi.framework.Version;
@@ -41,6 +43,8 @@
 
     private static final String CFG_STRICT = "strict";
 
+    private static final String CFG_REMOVAL_PERIOD = "removal-period";
+
     private static final String PROP_VERSION = "version";
 
     
@@ -62,13 +66,14 @@
         } else {
             final Map<BundleDescriptor, Set<String>> bundleRegions = this.calculateBundleRegions(context, regions);
             final boolean strict = Boolean.parseBoolean(context.getConfiguration().getOrDefault(CFG_STRICT, "false"));
+            final Integer removalPeriod = Integer.parseInt(context.getConfiguration().getOrDefault(CFG_REMOVAL_PERIOD, "-1"));
             final String regionNames = context.getConfiguration().getOrDefault(CFG_REGIONS, ApiRegion.GLOBAL);
             for(final String r : regionNames.split(",")) {
                 final ApiRegion region = regions.getRegionByName(r.trim());
                 if (region == null ) {
                     context.reportExtensionError(ApiRegions.EXTENSION_NAME, "Region not found:" + r.trim());
                 } else {
-                    checkBundlesForRegion(context, region, bundleRegions, strict);
+                    checkBundlesForRegion(context, region, bundleRegions, strict, removalPeriod);
                 }
             }
         }
@@ -86,7 +91,18 @@
     private void checkBundlesForRegion(final AnalyserTaskContext context, 
             final ApiRegion region,
             final Map<BundleDescriptor, Set<String>> bundleRegions,
-            final boolean strict) {
+            final boolean strict,
+            final int removalPeriod) {
+        final Calendar checkDate;
+        if ( removalPeriod > 0 ) {
+            checkDate = Calendar.getInstance();
+            checkDate.set(Calendar.HOUR_OF_DAY, 23);
+            checkDate.set(Calendar.MINUTE, 59);
+            checkDate.add(Calendar.DAY_OF_YEAR, removalPeriod);
+        } else {
+            checkDate = null;
+        }
+
         final Set<ApiExport> exports = this.calculateDeprecatedPackages(region, bundleRegions);
 
         final Set<String> allowedNames = getAllowedRegions(region);
@@ -95,19 +111,37 @@
             if ( isInAllowedRegion(bundleRegions.get(bd), region.getName(), allowedNames) ) {
                 for(final PackageInfo pi : bd.getImportedPackages()) {
                     final VersionRange importRange = pi.getPackageVersionRange();
-                    String imports = null;
+                    DeprecationInfo deprecationInfo = null;
                     for(final ApiExport exp : exports) {
                         if ( pi.getName().equals(exp.getName()) ) {
                             String version = exp.getProperties().get(PROP_VERSION);
                             if ( version == null || importRange == null || importRange.includes(new Version(version)) ) {
-                                imports = exp.getDeprecation().getPackageInfo().getMessage();
+                                deprecationInfo = exp.getDeprecation().getPackageInfo();
                                 break;
                             }
                         }
                     }
-                    if ( imports != null ) {
-                        final String msg = "Usage of deprecated package found : ".concat(pi.getName()).concat(" : ").concat(imports);
-                        if ( strict ) {
+                    if ( deprecationInfo != null ) {
+                        String msg = "Usage of deprecated package found : ".concat(pi.getName()).concat(" : ").concat(deprecationInfo.getMessage());
+                        if ( deprecationInfo.getSince() != null ) {
+                            msg = msg.concat(" Deprecated since ").concat(deprecationInfo.getSince());
+                        }
+                        boolean isError = strict;
+                        if ( deprecationInfo.getForRemoval() != null ) {
+                            boolean printRemoval = true;
+                            if ( checkDate != null ) {
+                                final Calendar c = deprecationInfo.getForRemovalBy();
+                                if ( c != null && c.before(checkDate)) {
+                                    isError = true;
+                                    printRemoval = false;
+                                    msg = msg.concat(" The package is scheduled to be removed in less than ").concat(String.valueOf(removalPeriod)).concat(" days by ").concat(deprecationInfo.getForRemoval());
+                                }
+                            }
+                            if ( printRemoval ) {
+                                msg = msg.concat(" For removal : ").concat(deprecationInfo.getForRemoval());
+                            } 
+                        }
+                        if ( isError ) {
                             context.reportArtifactError(bd.getArtifact().getId(), msg);
                         } else {
                             context.reportArtifactWarning(bd.getArtifact().getId(), msg);
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java
index 818fbde..bf91b9e 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java
@@ -43,6 +43,8 @@
 
     private static final String SINCE_KEY = "since";
 
+    private static final String FOR_REMOVAL_KEY = "for-removal";
+
     private static final String MEMBERS_KEY = "members";
 
     private static final String NAME_KEY = "name";
@@ -210,11 +212,15 @@
                 // whole package
                 final DeprecationInfo info = new DeprecationInfo(depObj.getString(MSG_KEY));
                 info.setSince(depObj.getString(SINCE_KEY, null));
+                info.setForRemoval(depObj.getString(FOR_REMOVAL_KEY, null));
                 this.getDeprecation().setPackageInfo(info);
             } else {
                 if ( depObj.containsKey(SINCE_KEY) ) {
                     throw new IOException("Export " + this.getName() + " has wrong since in " + DEPRECATED_KEY);
                 }
+                if ( depObj.containsKey(FOR_REMOVAL_KEY) ) {
+                    throw new IOException("Export " + this.getName() + " has wrong for-removal in " + DEPRECATED_KEY);
+                }
                 final JsonValue val = depObj.get(MEMBERS_KEY);
                 if ( val.getValueType() != ValueType.OBJECT) {
                     throw new IOException("Export " + this.getName() + " has wrong type for " + MEMBERS_KEY + " : " + val.getValueType().name());
@@ -230,6 +236,7 @@
                         }
                         final DeprecationInfo info = new DeprecationInfo(memberObj.getString(MSG_KEY));
                         info.setSince(memberObj.getString(SINCE_KEY, null));
+                        info.setForRemoval(depObj.getString(FOR_REMOVAL_KEY, null));
                         this.getDeprecation().addMemberInfo(memberProp.getKey(), info);
                     } else {
                         throw new IOException("Export " + this.getName() + " has wrong type for member in " + MEMBERS_KEY + " : " + memberProp.getValue().getValueType().name());
@@ -248,26 +255,34 @@
     JsonValue deprecationToJSON() {
         final Deprecation dep = this.getDeprecation();
         if ( dep.getPackageInfo() != null ) {
-            if ( dep.getPackageInfo().getSince() == null ) {
+            if ( dep.getPackageInfo().getSince() == null && dep.getPackageInfo().getForRemoval() == null ) {
                 return Json.createValue(dep.getPackageInfo().getMessage());
             } else {
                 final JsonObjectBuilder depBuilder = Json.createObjectBuilder();
                 depBuilder.add(MSG_KEY, dep.getPackageInfo().getMessage());
-                depBuilder.add(SINCE_KEY, dep.getPackageInfo().getSince());
-
+                if ( dep.getPackageInfo().getSince() != null ) {
+                    depBuilder.add(SINCE_KEY, dep.getPackageInfo().getSince());
+                }
+                if ( dep.getPackageInfo().getForRemoval() != null ) {
+                    depBuilder.add(FOR_REMOVAL_KEY, dep.getPackageInfo().getForRemoval());
+                }
                 return depBuilder.build();
             }
         } else if ( !dep.getMemberInfos().isEmpty() ) {
             final JsonObjectBuilder depBuilder = Json.createObjectBuilder();
             final JsonObjectBuilder membersBuilder = Json.createObjectBuilder();
             for(final Map.Entry<String, DeprecationInfo> memberEntry : dep.getMemberInfos().entrySet()) {
-                if ( memberEntry.getValue().getSince() == null ) {
+                if ( memberEntry.getValue().getSince() == null && memberEntry.getValue().getForRemoval() == null ) {
                     membersBuilder.add(memberEntry.getKey(), memberEntry.getValue().getMessage());
                 } else {
                     final JsonObjectBuilder mBuilder = Json.createObjectBuilder();
                     mBuilder.add(MSG_KEY, memberEntry.getValue().getMessage());
-                    mBuilder.add(SINCE_KEY, memberEntry.getValue().getSince());
-
+                    if ( memberEntry.getValue().getSince() != null ) {
+                        mBuilder.add(SINCE_KEY, memberEntry.getValue().getSince());
+                    }
+                    if ( memberEntry.getValue().getForRemoval() != null ) {
+                        mBuilder.add(FOR_REMOVAL_KEY, memberEntry.getValue().getForRemoval());
+                    }
                     membersBuilder.add(memberEntry.getKey(), mBuilder);
                 }
             }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfo.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfo.java
index c46b7f9..740109c 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfo.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfo.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sling.feature.extension.apiregions.api;
 
+import java.util.Calendar;
 import java.util.Objects;
 
 /**
@@ -29,6 +30,12 @@
 
     private String since;
 
+    /** 
+     * Optional for removal information.
+     * @since 1.3.0
+     */
+    private String forRemoval;
+
     /**
      * Create a new info
      * @param msg The msg
@@ -65,9 +72,68 @@
         this.since = since;
     }
 
+    /**
+     * Get the optional for removal information. This should either be 'true' or 'false'
+     * or a date in the format 'YYYY-MM-DD'.
+     * @return The for removal information or {@code null}
+     * @since 1.3.0
+     */
+    public String getForRemoval() {
+        return forRemoval;
+    }
+
+    /**
+     * Set the for removal information. This should either be 'true' or 'false'
+     * or a date in the format 'YYYY-MM-DD'.
+     * @param value The new removal info
+     * @since 1.3.0
+     */
+    public void setForRemoval(final String value) {
+        this.forRemoval = value;
+    }
+
+    /**
+     * Is this member intended to be removed?
+     * @return {@code true} if the member will be removed in the future
+     * @since 1.3.0
+     */
+    public boolean isForRemoval() {
+        return this.forRemoval != null && !"false".equalsIgnoreCase(this.forRemoval);
+    }
+
+    /**
+     * Return a date by which this member will be removed
+     * @return A calendar if the value from {@link #getForRemoval()} is formatted as 'YYYY-MM-DD'.
+     * @since 1.3.0
+     */
+    public Calendar getForRemovalBy() {
+        if ( this.forRemoval != null ) {
+           final String[] parts = this.forRemoval.split("-");
+           if ( parts.length == 3 ) {
+               if ( parts[0].length() == 4 && parts[1].length() == 2 && parts[2].length() == 2 ) {
+                   try {
+                       final int year = Integer.parseInt(parts[0]);
+                       final int month = Integer.parseInt(parts[1]);
+                       final int day = Integer.parseInt(parts[2]);
+
+                       final Calendar c = Calendar.getInstance();
+                       c.set(Calendar.YEAR, year);
+                       c.set(Calendar.MONTH, month - 1);
+                       c.set(Calendar.DAY_OF_MONTH, day);
+
+                       return c;
+                   } catch ( final NumberFormatException ignore ) {
+                       // ignore
+                   }
+               }
+           }
+        }
+        return null;
+    }
+    
     @Override
     public int hashCode() {
-        return Objects.hash(message, since);
+        return Objects.hash(message, since, forRemoval);
     }
 
     @Override
@@ -82,6 +148,6 @@
             return false;
         }
         DeprecationInfo other = (DeprecationInfo) obj;
-        return Objects.equals(message, other.message) && Objects.equals(since, other.since);
+        return Objects.equals(message, other.message) && Objects.equals(since, other.since) && Objects.equals(forRemoval, other.forRemoval);
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java
index 95c1c76..4e32aaf 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("1.2.0")
+@org.osgi.annotation.versioning.Version("1.3.0")
 package org.apache.sling.feature.extension.apiregions.api;
 
 
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiExportTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiExportTest.java
index 34958bf..60cc3d9 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiExportTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiExportTest.java
@@ -36,6 +36,8 @@
 
     private static final String PCK = "org.apache.sling";
 
+    private static final String FOR_REMOVAL = "2021-01-01";
+
     @Test(expected = IllegalArgumentException.class)
     public void testNameRequired() throws Exception {
         new ApiExport(PCK);
@@ -71,6 +73,22 @@
 
         assertEquals(MSG, exp.getDeprecation().getPackageInfo().getMessage());
         assertEquals(SINCE, exp.getDeprecation().getPackageInfo().getSince());
+        assertNull(exp.getDeprecation().getPackageInfo().getForRemoval());
+        assertTrue(exp.getDeprecation().getMemberInfos().isEmpty());
+
+        assertEquals(jv, exp.deprecationToJSON());
+    }
+
+    @Test
+    public void testPackageDeprecationMessageAndForRemoval() throws Exception {
+        final JsonValue jv = getJson("{\"msg\":\"" + MSG + "\",\"for-removal\":\"" + FOR_REMOVAL + "\"}");
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertEquals(MSG, exp.getDeprecation().getPackageInfo().getMessage());
+        assertEquals(FOR_REMOVAL, exp.getDeprecation().getPackageInfo().getForRemoval());
+        assertNull(exp.getDeprecation().getPackageInfo().getSince());
         assertTrue(exp.getDeprecation().getMemberInfos().isEmpty());
 
         assertEquals(jv, exp.deprecationToJSON());
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfoTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfoTest.java
index 086b0e1..b6a9f7a 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfoTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfoTest.java
@@ -16,6 +16,14 @@
  */
 package org.apache.sling.feature.extension.apiregions.api;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Calendar;
+
 import org.junit.Test;
 
 public class DeprecationInfoTest {
@@ -25,4 +33,52 @@
         new DeprecationInfo("Message");
         new DeprecationInfo(null);
     }
+
+    @Test
+    public void testForRemovalNull() throws Exception {
+        final DeprecationInfo info = new DeprecationInfo("Message");
+        assertNull(info.getForRemoval());
+        assertFalse(info.isForRemoval());
+        assertNull(info.getForRemovalBy());
+    }
+
+    @Test
+    public void testForRemovalTrue() throws Exception {
+        final DeprecationInfo info = new DeprecationInfo("Message");
+        info.setForRemoval("true");
+        assertEquals("true", info.getForRemoval());
+        assertTrue(info.isForRemoval());
+        assertNull(info.getForRemovalBy());
+    }
+
+    @Test
+    public void testForRemovalFalse() throws Exception {
+        final DeprecationInfo info = new DeprecationInfo("Message");
+        info.setForRemoval("false");
+        assertEquals("false", info.getForRemoval());
+        assertFalse(info.isForRemoval());
+        assertNull(info.getForRemovalBy());
+    }
+
+    @Test
+    public void testForRemovalDate() throws Exception {
+        final DeprecationInfo info = new DeprecationInfo("Message");
+        info.setForRemoval("2021-02-05");
+        assertEquals("2021-02-05", info.getForRemoval());
+        assertTrue(info.isForRemoval());
+        final Calendar c = info.getForRemovalBy();
+        assertNotNull(c);
+        assertEquals(2021, c.get(Calendar.YEAR));
+        assertEquals(1, c.get(Calendar.MONTH));
+        assertEquals(5, c.get(Calendar.DAY_OF_MONTH));
+    }
+
+    @Test
+    public void testForRemovalString() throws Exception {
+        final DeprecationInfo info = new DeprecationInfo("Message");
+        info.setForRemoval("hello");
+        assertEquals("hello", info.getForRemoval());
+        assertTrue(info.isForRemoval());
+        assertNull(info.getForRemovalBy());
+    }
 }