You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2020/09/17 07:07:55 UTC

[sling-org-apache-sling-feature-extension-apiregions] branch master updated: SLING-9738 : Allow to mark api deprecated as part of the api region

This is an automated email from the ASF dual-hosted git repository.

cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-extension-apiregions.git


The following commit(s) were added to refs/heads/master by this push:
     new 4bf6acb  SLING-9738 : Allow to mark api deprecated as part of the api region
4bf6acb is described below

commit 4bf6acbba8c4759406242b75c28940b5ad985731
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu Sep 17 09:07:20 2020 +0200

    SLING-9738 : Allow to mark api deprecated as part of the api region
---
 docs/api-regions.md                                |  37 +++++
 .../extension/apiregions/api/ApiExport.java        | 166 ++++++++++++++++----
 .../extension/apiregions/api/ApiRegion.java        |  30 ++--
 .../extension/apiregions/api/ApiRegions.java       |  81 ++++++++--
 .../extension/apiregions/api/Deprecation.java      | 131 ++++++++++++++++
 .../extension/apiregions/api/DeprecationInfo.java  |  86 +++++++++++
 .../apiregions/api/JDKDeprecationExtension.java    | 167 +++++++++++++++++++++
 .../extension/apiregions/api/package-info.java     |   2 +-
 .../extension/apiregions/api/ApiExportTest.java    | 140 +++++++++++++++++
 .../{TestApiRegions.java => ApiRegionsTest.java}   |  37 ++++-
 .../apiregions/api/DeprecationInfoTest.java        |  28 ++++
 .../extension/apiregions/api/DeprecationTest.java  |  44 ++++++
 .../api/JDKDeprecationExtensionTest.java           | 110 ++++++++++++++
 13 files changed, 987 insertions(+), 72 deletions(-)

diff --git a/docs/api-regions.md b/docs/api-regions.md
index a6ede8f..2c672f4 100644
--- a/docs/api-regions.md
+++ b/docs/api-regions.md
@@ -59,3 +59,40 @@ If a feature exports no packages and only wants to have visibility of packages f
             "exports": []
         }
     ]
+    
+## Deprecation of API
+
+When changing the source of an API is not possible to mark it as deprecated, it can be marked as such through this api region as well. This information can be used by tooling to mark and report usage of such API.
+
+     "api-regions:JSON|false":[
+        {
+            "name":"global",
+            "exports":[
+                # Package exported without deprecation
+                "org.apache.sling.api",
+                # Package exported - full package deprecated
+                {
+                    "name" : "org.apache.sling.api.file",
+                    "deprecated" : "Deprecation message"
+                },
+                # Package exported - single class and single member deprecated
+                {
+                    "name" : "org.apache.sling.api.io",
+                    "deprecated" : {
+                        "FileCache" : "Deprecation message",
+                        "MemoryCache#getFile()" : "Deprecation message"
+                    }
+                },
+                # Instead of just the message, additional information about when the 
+                # deprecation happened can be provided (this works in all of the
+                # above places)
+                {
+                    "name":"org.apache.sling.incubator.api",
+                    "deprecated":{
+                        "msg":"This is deprecated",
+                        "since":"Since Sling left the incubator"
+                    }
+                }
+            ]
+        }
+     ]
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 413e87f..303a5c3 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
@@ -16,8 +16,17 @@
  */
 package org.apache.sling.feature.extension.apiregions.api;
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
 
 import org.apache.sling.feature.ArtifactId;
 
@@ -26,6 +35,14 @@ import org.apache.sling.feature.ArtifactId;
  */
 public class ApiExport implements Comparable<ApiExport> {
 
+    private static final String DEPRECATED_KEY = "deprecated";
+
+    private static final String MSG_KEY = "msg";
+
+    private static final String SINCE_KEY = "since";
+
+    private static final String MEMBERS_KEY = "members";
+
     private final String name;
 
     private volatile String toggle;
@@ -34,12 +51,17 @@ public class ApiExport implements Comparable<ApiExport> {
 
     private final Map<String, String> properties = new HashMap<>();
 
+    private final Deprecation deprecation = new Deprecation();
+
     /**
      * Create a new export
      *
      * @param name Package name for the export
      */
     public ApiExport(final String name) {
+        if ( name == null ) {
+            throw new IllegalArgumentException();
+        }
         this.name = name;
     }
 
@@ -97,6 +119,109 @@ public class ApiExport implements Comparable<ApiExport> {
         return this.properties;
     }
 
+    /**
+     * Get the deprecation info
+     *
+     * @return The info
+     */
+    public Deprecation getDeprecation() {
+        return this.deprecation;
+    }
+
+    /**
+     * Internal method to parse the extension JSON
+     * @param dValue The JSON value
+     * @throws IOException If the format is not correct
+     */
+    void parseDeprecation(final JsonValue dValue) throws IOException {
+        if ( dValue.getValueType() == ValueType.STRING ) {
+
+            // value is deprecation message for the whole package
+            final DeprecationInfo info = new DeprecationInfo(((JsonString)dValue).getString());
+            this.getDeprecation().setPackageInfo(info);
+
+        } else if ( dValue.getValueType() == ValueType.OBJECT ) {
+
+            // value is an object with properties
+            final JsonObject depObj = dValue.asJsonObject();
+            if ( depObj.containsKey(MSG_KEY) && depObj.containsKey(MEMBERS_KEY) ) {
+                throw new IOException("Export " + this.getName() + " has wrong info in " + DEPRECATED_KEY);
+            }
+            if ( !depObj.containsKey(MSG_KEY) && !depObj.containsKey(MEMBERS_KEY)) {
+                throw new IOException("Export " + this.getName() + " has missing info in " + DEPRECATED_KEY);
+            }
+            if ( depObj.containsKey(MSG_KEY) ) {
+                // whole package
+                final DeprecationInfo info = new DeprecationInfo(depObj.getString(MSG_KEY));
+                info.setSince(depObj.getString(SINCE_KEY, null));
+                this.getDeprecation().setPackageInfo(info);
+            } else {
+                if ( depObj.containsKey(SINCE_KEY) ) {
+                    throw new IOException("Export " + this.getName() + " has wrong since 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());
+                }
+                for (final Map.Entry<String, JsonValue> memberProp : val.asJsonObject().entrySet()) {
+                    if ( memberProp.getValue().getValueType() == ValueType.STRING ) {
+                        final DeprecationInfo info = new DeprecationInfo(((JsonString)memberProp.getValue()).getString());
+                        this.getDeprecation().addMemberInfo(memberProp.getKey(), info);
+                    } else if ( memberProp.getValue().getValueType() == ValueType.OBJECT ) {
+                        final JsonObject memberObj = memberProp.getValue().asJsonObject();
+                        if ( !memberObj.containsKey(MSG_KEY) ) {
+                            throw new IOException("Export " + this.getName() + " has wrong type for member in " + MEMBERS_KEY + " : " + memberProp.getValue().getValueType().name());
+                        }
+                        final DeprecationInfo info = new DeprecationInfo(memberObj.getString(MSG_KEY));
+                        info.setSince(memberObj.getString(SINCE_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());
+                    }
+                }
+            }
+        } else {
+            throw new IOException("Export " + this.getName() + " has wrong type for " + DEPRECATED_KEY + " : " + dValue.getValueType().name());
+        }
+    }
+
+    /**
+     * Internal method to create the JSON if deprecation is set
+     * @return The JSON value or {@code null}
+     */
+    JsonValue deprecationToJSON() {
+        final Deprecation dep = this.getDeprecation();
+        if ( dep.getPackageInfo() != null ) {
+            if ( dep.getPackageInfo().getSince() == 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());
+
+                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 ) {
+                    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());
+
+                    membersBuilder.add(memberEntry.getKey(), mBuilder);
+                }
+            }
+
+            depBuilder.add(MEMBERS_KEY, membersBuilder);
+            return depBuilder.build();
+        }
+        return null;
+    }
+
     @Override
     public int compareTo(final ApiExport o) {
         return this.name.compareTo(o.name);
@@ -110,44 +235,23 @@ public class ApiExport implements Comparable<ApiExport> {
 
     @Override
     public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((name == null) ? 0 : name.hashCode());
-        result = prime * result + ((previous == null) ? 0 : previous.hashCode());
-        result = prime * result + ((properties == null) ? 0 : properties.hashCode());
-        result = prime * result + ((toggle == null) ? 0 : toggle.hashCode());
-        return result;
+        return Objects.hash(deprecation, name, previous, properties, toggle);
     }
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (getClass() != obj.getClass())
+        }
+        if (getClass() != obj.getClass()) {
             return false;
+        }
         ApiExport other = (ApiExport) obj;
-        if (name == null) {
-            if (other.name != null)
-                return false;
-        } else if (!name.equals(other.name))
-            return false;
-        if (previous == null) {
-            if (other.previous != null)
-                return false;
-        } else if (!previous.equals(other.previous))
-            return false;
-        if (properties == null) {
-            if (other.properties != null)
-                return false;
-        } else if (!properties.equals(other.properties))
-            return false;
-        if (toggle == null) {
-            if (other.toggle != null)
-                return false;
-        } else if (!toggle.equals(other.toggle))
-            return false;
-        return true;
+        return Objects.equals(deprecation, other.deprecation) && Objects.equals(name, other.name)
+                && Objects.equals(previous, other.previous) && Objects.equals(properties, other.properties)
+                && Objects.equals(toggle, other.toggle);
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java
index dd9f4b2..2fd5898 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java
@@ -17,7 +17,6 @@
 package org.apache.sling.feature.extension.apiregions.api;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -199,30 +198,23 @@ public class ApiRegion {
 
     @Override
     public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((exports == null) ? 0 : exports.hashCode());
-        result = prime * result + ((name == null) ? 0 : name.hashCode());
-        result = prime * result + Arrays.hashCode(getFeatureOrigins());
-        result = prime * result + ((properties == null) ? 0 : properties.hashCode());
-        return result;
+        return Objects.hash(exports, name, origins, parent, properties);
     }
 
     @Override
-    public boolean equals(Object o)
-    {
-        if (this == o)
-        {
+    public boolean equals(Object obj) {
+        if (this == obj) {
             return true;
         }
-        if (o == null || getClass() != o.getClass())
-        {
+        if (obj == null) {
             return false;
         }
-        ApiRegion region = (ApiRegion) o;
-        return exports.equals(region.exports) &&
-            origins.equals(region.origins) &&
-            properties.equals(region.properties) &&
-            Objects.equals(name, region.name);
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ApiRegion other = (ApiRegion) obj;
+        return Objects.equals(exports, other.exports) && Objects.equals(name, other.name)
+                && Objects.equals(origins, other.origins) && Objects.equals(parent, other.parent)
+                && Objects.equals(properties, other.properties);
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java
index 755ca3b..c0f5fc8 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Stream;
 
@@ -42,6 +43,9 @@ import javax.json.JsonWriter;
 
 import org.apache.sling.feature.Artifact;
 import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
 
 /**
  * An api regions configuration
@@ -59,6 +63,8 @@ public class ApiRegions {
 
     private static final String PREVIOUS_KEY = "previous";
 
+    private static final String DEPRECATED_KEY = "deprecated";
+
     private final List<ApiRegion> regions = new ArrayList<>();
 
     /**
@@ -198,6 +204,12 @@ public class ApiRegions {
                         if (exp.getPrevious() != null) {
                             expBuilder.add(PREVIOUS_KEY, exp.getPrevious().toMvnId());
                         }
+
+                        final JsonValue depValue = exp.deprecationToJSON();
+                        if ( depValue != null ) {
+                            expBuilder.add(DEPRECATED_KEY, depValue);
+                        }
+
                         for (final Map.Entry<String, String> entry : exp.getProperties().entrySet()) {
                             expBuilder.add(entry.getKey(), entry.getValue());
                         }
@@ -297,22 +309,35 @@ public class ApiRegions {
                                 for (final String key : expObj.keySet()) {
                                     if (NAME_KEY.equals(key)) {
                                         continue; // already set
+
                                     } else if (TOGGLE_KEY.equals(key)) {
                                         export.setToggle(expObj.getString(key));
+
                                     } else if (PREVIOUS_KEY.equals(key)) {
                                         export.setPrevious(ArtifactId.parse(expObj.getString(key)));
+
+
+                                    } else if ( DEPRECATED_KEY.equals(key)) {
+                                        final JsonValue dValue = expObj.get(DEPRECATED_KEY);
+                                        export.parseDeprecation(dValue);
+
+                                        // everything else is stored as a string property
                                     } else {
                                         export.getProperties().put(key, expObj.getString(key));
                                     }
                                 }
+                            } else {
+                                throw new IOException("Region " + region.getName() + " has wrong type for " + EXPORTS_KEY + " : " + e.getValueType().name());
                             }
                         }
                     } else if (entry.getKey().equals(Artifact.KEY_FEATURE_ORIGINS)) {
-                        Set<ArtifactId> origins = new LinkedHashSet<>();
+                        final Set<ArtifactId> origins = new LinkedHashSet<>();
                         for (final JsonValue origin : (JsonArray) entry.getValue()) {
                             origins.add(ArtifactId.fromMvnId(((JsonString) origin).getString()));
                         }
                         region.setFeatureOrigins(origins.toArray(new ArtifactId[0]));
+
+                        // everything else is stored as a string property
                     } else {
                         region.getProperties().put(entry.getKey(), ((JsonString) entry.getValue()).getString());
                     }
@@ -327,6 +352,39 @@ public class ApiRegions {
         }
     }
 
+    /**
+     * Get the api regions from the feature - if it exists.
+     * @param feature The feature
+     * @return The api regions or {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     * @since 1.1
+     */
+    public static ApiRegions getApiRegions(final Feature feature) {
+        final Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME);
+        return getApiRegions(ext);
+    }
+
+    /**
+     * Get the api regions from the extension.
+     * @param ext The extension
+     * @return The api regions or {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     * @since 1.1
+     */
+    public static ApiRegions getApiRegions(final Extension ext) {
+        if ( ext == null ) {
+            return null;
+        }
+        if ( ext.getType() != ExtensionType.JSON ) {
+            throw new IllegalArgumentException("Extension " + ext.getName() + " must have JSON type");
+        }
+        try {
+            return parse(ext.getJSONStructure().asJsonArray());
+        } catch ( final IOException ioe) {
+            throw new IllegalArgumentException(ioe.getMessage(), ioe);
+        }
+    }
+
     @Override
     public String toString() {
         return "ApiRegions [regions=" + regions + "]";
@@ -334,26 +392,21 @@ public class ApiRegions {
 
     @Override
     public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((regions == null) ? 0 : regions.hashCode());
-        return result;
+        return Objects.hash(regions);
     }
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (getClass() != obj.getClass())
+        }
+        if (getClass() != obj.getClass()) {
             return false;
+        }
         ApiRegions other = (ApiRegions) obj;
-        if (regions == null) {
-            if (other.regions != null)
-                return false;
-        } else if (!regions.equals(other.regions))
-            return false;
-        return true;
+        return Objects.equals(regions, other.regions);
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/Deprecation.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/Deprecation.java
new file mode 100644
index 0000000..7e4348c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/Deprecation.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Deprecation state for a package
+ *
+ * Either the whole package is deprecated or some members.
+ * @since 1.1
+ */
+public class Deprecation {
+
+    private volatile DeprecationInfo packageInfo;
+
+    private final Map<String, DeprecationInfo> memberInfos = new LinkedHashMap<>();
+
+    /**
+     * Get the optional package info if the package is deprecated
+     * @return The info or {@code null}
+     */
+    public DeprecationInfo getPackageInfo() {
+        return this.packageInfo;
+    }
+
+    /**
+     * Set the deprecation info for the whole package
+     * @param i The info
+     * @throws IllegalStateException If a member is already deprecated
+     */
+    public void setPackageInfo(final DeprecationInfo i) {
+        if ( !this.memberInfos.isEmpty()) {
+            throw new IllegalStateException("Member is already deprecated");
+        }
+        this.packageInfo = i;
+    }
+
+    /**
+     * Add deprecation info for a member
+     * @param member The member
+     * @param i The info
+     * @throws IllegalStateException if the package is already deprecated
+     */
+    public void addMemberInfo(final String member, final DeprecationInfo i) {
+        if ( this.packageInfo != null ) {
+            throw new IllegalStateException("Package is already deprecated");
+        }
+        this.memberInfos.put(member, i);
+    }
+
+    /**
+     * Remove deprecation info for a member
+     * @param member The member
+     */
+    public void removeMemberInfo(final String member) {
+        this.memberInfos.remove(member);
+    }
+
+    /**
+     * Get all deprecation member infos
+     * @return The infos
+     */
+    public Map<String, DeprecationInfo> getMemberInfos() {
+        return this.memberInfos;
+    }
+
+    /**
+     * Returns the class name part of a member string.
+     * This is the part before the first hash (or the full string if no hash)
+     * @param member The member
+     * @return The class name
+     */
+    public static final String getClassName(final String member) {
+        final int pos = member.indexOf("#");
+        if ( pos == -1 ) {
+            return member;
+        }
+        return member.substring(0, pos);
+    }
+
+    /**
+     * Returns the class member name part of a member string.
+     * This is the part after the first hash (or {@code null} if no hash
+     * @param member The member
+     * @return The class member name or {@code null}
+     */
+    public static final String getClassMemberName(final String member) {
+        final int pos = member.indexOf("#");
+        if ( pos == -1 ) {
+            return null;
+        }
+        return member.substring(pos + 1);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(memberInfos, packageInfo);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Deprecation other = (Deprecation) obj;
+        return Objects.equals(memberInfos, other.memberInfos) && Objects.equals(packageInfo, other.packageInfo);
+    }
+}
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
new file mode 100644
index 0000000..db844d0
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfo.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import java.util.Objects;
+
+/**
+ * Deprecation info for a package or member.
+ * @since 1.1
+ */
+public class DeprecationInfo {
+
+    private final String message;
+
+    private volatile String since;
+
+    /**
+     * Create a new info
+     * @param msg The msg
+     * @throws IllegalArgumentException if msg is {@code null}
+     */
+    public DeprecationInfo(final String msg) {
+        if ( msg == null ) {
+            throw new IllegalArgumentException();
+        }
+        this.message = msg;
+    }
+
+    /**
+     * Get the message
+     * @return The message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * Get the optional since information
+     * @return The since information or {@code null}
+     */
+    public String getSince() {
+        return since;
+    }
+
+    /**
+     * Set the since information
+     * @param since The new info
+     */
+    public void setSince(final String since) {
+        this.since = since;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(message, since);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DeprecationInfo other = (DeprecationInfo) obj;
+        return Objects.equals(message, other.message) && Objects.equals(since, other.since);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtension.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtension.java
new file mode 100644
index 0000000..7ee11ed
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtension.java
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonString;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+
+/**
+ * Extension to hold information about deprecated JDK API
+ * @since 1.1.0
+ */
+public class JDKDeprecationExtension {
+
+    private static final String MSG_KEY = "msg";
+
+    private static final String SINCE_KEY = "since";
+
+    /**
+     * Extension name containing the deprecation. The extension
+     * can be used to specify JDK API which is deprecated / should not be used.
+     * This extension is of type {@link ExtensionType#JSON} and is optional.
+     */
+    public static final String EXTENSION_NAME = "jdk-deprecation";
+
+    /**
+     * Get the extension from the feature - if it exists.
+     * @param feature The feature
+     * @return The extension or {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     */
+    public static JDKDeprecationExtension getExtension(final Feature feature) {
+        final Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME);
+        return getExtension(ext);
+    }
+
+    /**
+     * Get the execution environment from the extension.
+     * @param ext The extension
+     * @return The execution environment or {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     */
+    public static JDKDeprecationExtension getExtension(final Extension ext) {
+        if ( ext == null ) {
+            return null;
+        }
+        if ( ext.getType() != ExtensionType.JSON ) {
+            throw new IllegalArgumentException("Extension " + ext.getName() + " must have JSON type");
+        }
+        return new JDKDeprecationExtension(ext.getJSONStructure());
+    }
+
+    private final Map<String, DeprecationInfo> memberInfos = new LinkedHashMap<>();
+
+    private JDKDeprecationExtension(final JsonStructure structure) {
+        for(final Map.Entry<String, JsonValue> prop : structure.asJsonObject().entrySet()) {
+            if ( prop.getValue().getValueType() == ValueType.STRING ) {
+                final DeprecationInfo info = new DeprecationInfo(((JsonString)prop.getValue()).getString());
+                this.addMemberInfo(prop.getKey(), info);
+            } else if ( prop.getValue().getValueType() == ValueType.OBJECT ) {
+                final JsonObject memberObj = prop.getValue().asJsonObject();
+                if ( !memberObj.containsKey(MSG_KEY) ) {
+                    throw new IllegalArgumentException("No msg property found");
+                }
+                final DeprecationInfo info = new DeprecationInfo(memberObj.getString(MSG_KEY));
+                info.setSince(memberObj.getString(SINCE_KEY, null));
+                this.addMemberInfo(prop.getKey(), info);
+            } else {
+                throw new IllegalArgumentException("Wrong value type " + prop.getValue().getValueType().name());
+            }
+
+        }
+    }
+
+    /**
+     * Add deprecation info for a member
+     * @param member The member
+     * @param i The info
+     * @throws IllegalStateException if the package is already deprecated
+     */
+    public void addMemberInfo(final String member, final DeprecationInfo i) {
+        this.memberInfos.put(member, i);
+    }
+
+    /**
+     * Remove deprecation info for a member
+     * @param member The member
+     */
+    public void removeMemberInfo(final String member) {
+        this.memberInfos.remove(member);
+    }
+
+    /**
+     * Get all deprecation member infos
+     * @return The infos
+     */
+    public Map<String, DeprecationInfo> getMemberInfos() {
+        return this.memberInfos;
+    }
+
+    /**
+     * Generate a JSON representation
+     * @return The JSON Object
+     */
+    public JsonObject toJSON() {
+        final JsonObjectBuilder membersBuilder = Json.createObjectBuilder();
+        for(final Map.Entry<String, DeprecationInfo> memberEntry : this.getMemberInfos().entrySet()) {
+            if ( memberEntry.getValue().getSince() == 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());
+
+                membersBuilder.add(memberEntry.getKey(), mBuilder);
+            }
+        }
+
+        return membersBuilder.build();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(memberInfos);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        JDKDeprecationExtension other = (JDKDeprecationExtension) obj;
+        return Objects.equals(memberInfos, other.memberInfos);
+    }
+}
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 72e56f8..5c7d69a 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.0.0")
+@org.osgi.annotation.versioning.Version("1.1.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
new file mode 100644
index 0000000..34958bf
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiExportTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
+
+import org.junit.Test;
+
+public class ApiExportTest {
+
+    private static final String MSG = "deprecated!";
+
+    private static final String SINCE = "now";
+
+    private static final String PCK = "org.apache.sling";
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNameRequired() throws Exception {
+        new ApiExport(PCK);
+        new ApiExport(null);
+    }
+
+    private JsonObject getJson(final String text) {
+        try (final StringReader reader = new StringReader(text)) {
+           return Json.createReader(reader).readObject();
+        }
+   }
+
+    @Test
+    public void testPackageDeprecationSimpleMessage() throws Exception {
+        final JsonValue jv = Json.createValue(MSG);
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertEquals(MSG, exp.getDeprecation().getPackageInfo().getMessage());
+        assertNull(exp.getDeprecation().getPackageInfo().getSince());
+        assertTrue(exp.getDeprecation().getMemberInfos().isEmpty());
+
+        assertEquals(jv, exp.deprecationToJSON());
+    }
+
+    @Test
+    public void testPackageDeprecationMessageAndSince() throws Exception {
+        final JsonValue jv = getJson("{\"msg\":\"" + MSG + "\",\"since\":\"" + SINCE + "\"}");
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertEquals(MSG, exp.getDeprecation().getPackageInfo().getMessage());
+        assertEquals(SINCE, exp.getDeprecation().getPackageInfo().getSince());
+        assertTrue(exp.getDeprecation().getMemberInfos().isEmpty());
+
+        assertEquals(jv, exp.deprecationToJSON());
+    }
+
+    @Test
+    public void testSimpleMembers() throws Exception {
+        final JsonValue jv = getJson("{\"members\":{\"foo\":\"" + MSG + "\",\"bar\":\"" + MSG + MSG + "\"}}");
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertNull(exp.getDeprecation().getPackageInfo());
+        assertEquals(2, exp.getDeprecation().getMemberInfos().size());
+
+        final DeprecationInfo foo = exp.getDeprecation().getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = exp.getDeprecation().getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertNull(bar.getSince());
+
+        assertEquals(jv, exp.deprecationToJSON());
+    }
+
+    @Test
+    public void testComplexMembers() throws Exception {
+        final JsonValue jv = getJson("{\"members\":{\"foo\":{\"msg\":\"" + MSG + "\"},\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}}");
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertNull(exp.getDeprecation().getPackageInfo());
+        assertEquals(2, exp.getDeprecation().getMemberInfos().size());
+
+        final DeprecationInfo foo = exp.getDeprecation().getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = exp.getDeprecation().getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertEquals(SINCE, bar.getSince());
+
+        // the expected JSON is actually a mixed JSON (not the input!)
+        final JsonValue expJV = getJson("{\"members\":{\"foo\":\"" + MSG + "\",\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}}");
+        assertEquals(expJV, exp.deprecationToJSON());
+    }
+
+    @Test
+    public void testMixedMembers() throws Exception {
+        final JsonValue jv = getJson("{\"members\":{\"foo\":\"" + MSG + "\",\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}}");
+
+        final ApiExport exp = new ApiExport(PCK);
+        exp.parseDeprecation(jv);
+
+        assertNull(exp.getDeprecation().getPackageInfo());
+        assertEquals(2, exp.getDeprecation().getMemberInfos().size());
+
+        final DeprecationInfo foo = exp.getDeprecation().getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = exp.getDeprecation().getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertEquals(SINCE, bar.getSince());
+
+        assertEquals(jv, exp.deprecationToJSON());
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiRegionsTest.java
similarity index 83%
rename from src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.java
rename to src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiRegionsTest.java
index 0ad697c..d18a8b6 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/ApiRegionsTest.java
@@ -16,6 +16,12 @@
  */
 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.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -23,18 +29,17 @@ import java.io.StringWriter;
 import java.io.Writer;
 
 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.junit.Test;
-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;
 
-public class TestApiRegions {
+public class ApiRegionsTest {
 
     private String readJSON(final String name) throws IOException {
         try (final Reader reader = new InputStreamReader(
-                TestApiRegions.class.getResourceAsStream("/json/" + name + ".json"),
+                ApiRegionsTest.class.getResourceAsStream("/json/" + name + ".json"),
                 "UTF-8"); final Writer writer = new StringWriter()) {
             int l;
             char[] buf = new char[2048];
@@ -132,4 +137,22 @@ public class TestApiRegions {
         assertEquals(1, three.listExports().size());
         assertTrue(three.listExports().contains(new ApiExport("c")));
     }
+
+    @Test public void testNullFeature() {
+        assertNull(ApiRegions.getApiRegions((Feature)null));
+    }
+
+    @Test public void testNullExtension() {
+        assertNull(ApiRegions.getApiRegions((Extension)null));
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        assertNull(ApiRegions.getApiRegions(f));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testWrongExtensionType() {
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        final Extension e = new Extension(ExtensionType.TEXT, ApiRegions.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        f.getExtensions().add(e);
+        ApiRegions.getApiRegions(f);
+    }
 }
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
new file mode 100644
index 0000000..086b0e1
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationInfoTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import org.junit.Test;
+
+public class DeprecationInfoTest {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testMessageRequired() throws Exception {
+        new DeprecationInfo("Message");
+        new DeprecationInfo(null);
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationTest.java
new file mode 100644
index 0000000..74f6f03
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/DeprecationTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import org.junit.Test;
+
+public class DeprecationTest {
+
+    @Test(expected = IllegalStateException.class)
+    public void testNoMemberIfPackageIsDeprecated() throws Exception {
+        final Deprecation dep = new Deprecation();
+        final DeprecationInfo info = new DeprecationInfo("msg");
+
+        // setting package info is ok
+        dep.setPackageInfo(info);
+        // setting member now is not
+        dep.addMemberInfo("foo", info);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testNoPackageIfMemberIsAvailable() throws Exception {
+        final Deprecation dep = new Deprecation();
+        final DeprecationInfo info = new DeprecationInfo("msg");
+
+        // add member is ok
+        dep.addMemberInfo("foo", info);
+        // setting package info is not ok
+        dep.setPackageInfo(info);
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtensionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtensionTest.java
new file mode 100644
index 0000000..869acc9
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/JDKDeprecationExtensionTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+package org.apache.sling.feature.extension.apiregions.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+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.junit.Test;
+
+public class JDKDeprecationExtensionTest {
+
+    private static final String MSG = "deprecated!";
+
+    private static final String SINCE = "now";
+
+    @Test
+    public void testNullFeature() {
+        assertNull(JDKDeprecationExtension.getExtension((Feature)null));
+    }
+
+    @Test
+    public void testNullExtension() {
+        assertNull(JDKDeprecationExtension.getExtension((Extension)null));
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        assertNull(JDKDeprecationExtension.getExtension(f));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testWrongExtensionType() {
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        final Extension e = new Extension(ExtensionType.TEXT, JDKDeprecationExtension.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        f.getExtensions().add(e);
+        JDKDeprecationExtension.getExtension(f);
+    }
+
+    @Test
+    public void testSimpleMembers() throws Exception {
+        final Extension ext = new Extension(ExtensionType.JSON, JDKDeprecationExtension.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        ext.setJSON("{\"foo\":\"" + MSG + "\",\"bar\":\"" + MSG + MSG + "\"}");
+        final JDKDeprecationExtension jdk = JDKDeprecationExtension.getExtension(ext);
+
+        assertEquals(2, jdk.getMemberInfos().size());
+
+        final DeprecationInfo foo = jdk.getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = jdk.getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertNull(bar.getSince());
+
+        assertEquals(ext.getJSONStructure(), jdk.toJSON());
+    }
+
+    @Test
+    public void testComplexMembers() throws Exception {
+        final Extension ext = new Extension(ExtensionType.JSON, JDKDeprecationExtension.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        ext.setJSON("{\"foo\":{\"msg\":\"" + MSG + "\"},\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}");
+        final JDKDeprecationExtension jdk = JDKDeprecationExtension.getExtension(ext);
+
+        assertEquals(2, jdk.getMemberInfos().size());
+
+        final DeprecationInfo foo = jdk.getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = jdk.getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertEquals(SINCE, bar.getSince());
+
+        // the expected JSON is actually a mixed JSON (not the input!)
+        ext.setJSON("{\"foo\":\"" + MSG + "\",\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}");
+        assertEquals(ext.getJSONStructure(), jdk.toJSON());
+    }
+
+    @Test
+    public void testMixedMembers() throws Exception {
+        final Extension ext = new Extension(ExtensionType.JSON, JDKDeprecationExtension.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        ext.setJSON("{\"foo\":\"" + MSG + "\",\"bar\":{\"msg\":\""+MSG+MSG+"\",\"since\":\""+SINCE+"\"}}");
+        final JDKDeprecationExtension jdk = JDKDeprecationExtension.getExtension(ext);
+
+        assertEquals(2, jdk.getMemberInfos().size());
+
+        final DeprecationInfo foo = jdk.getMemberInfos().get("foo");
+        assertEquals(MSG, foo.getMessage());
+        assertNull(foo.getSince());
+        final DeprecationInfo bar = jdk.getMemberInfos().get("bar");
+        assertEquals(MSG + MSG, bar.getMessage());
+        assertEquals(SINCE, bar.getSince());
+
+        assertEquals(ext.getJSONStructure(), jdk.toJSON());
+    }
+}