Backwards compatibility support

Apache NetBeans Wiki Index

Note: These pages are being reviewed.

NetBeans contains deprecated obsolete code, which is typically left in place for several releases. In addition to add polution to the API, it also increases the number of dependencies to both ancient modules and Java platform. The deprecated code is a dead weight in the released NB product, as the shipped modules are (or should be) upgraded to work with API modules in their current versions, not using deprecated APIs.

The purpose of this backward compatible support is to preserve binary compatibility for unmaintained modules, or 3rd party modules with a different release cycle while allowing to remove obsolete code from the public APIs.

The following techniques can be used for backward compatibility:

Accessor method

A compatible implementation may need to access the internals possibly from a different module (classloader). @PatchedPublic annotation currently serves this purpose.

The annotated method is patched to be public at runtime, while the class is being loaded. The calling code is typically resides in the same package, although a different module with an implementation dependency. Using @PatchedPublic it can access the method even at runtime, although from a different classloader.

Note that this approach still requires that method signature dependencies affect the API module dependency closure. All types referenced from the signature must be present for the compilation and execution of the API module. If the referenced type contains an illegal platform or library dependency in its API/impl, then the illegal component infects even the API module.

Automatic module dependencies

If some classes are split from module "A" into a different module (say B), source and binary compatibility can be retained if the module "A" declares "B" as an additional implied dependency for clients who depend on an older version of the module. Clients compiled against older version will receive the additional dependency at both run-time and compile-time.

During compilation, a special file in config area will be generated. The generated file will be recognized when dependent module load, and their dependencies will be transformed according to the description.

The automatic dependencies must be stored in the file module-automatic-deps.xml in the module project’s root folder. A typical example of dependencies implied when a module is split to several ones is shown below:

 <!DOCTYPE transformations PUBLIC "-//NetBeans//DTD Module Automatic Dependencies 1.0//EN" "http://www.netbeans.org/dtds/module-auto-deps-1_0.dtd">

 <transformations version="1.0">
     <!-- unimportant content -->
     <transformationgroup>
         <description>Separation of desktop and cleanup</description>
         <transformation>
             <trigger-dependency type="older">
                 <module-dependency codenamebase="org.openide.filesystems" spec="9.0"/>
             </trigger-dependency>
             <implies>
                 <result>
                     <module-dependency codenamebase="org.openide.filesystems.nb"/>
                     <module-dependency codenamebase="org.openide.filesystems.compat8"/>
                 </result>
             </implies>
         </transformation>
     </transformationgroup>

 </transformations>

Module Fragment

If a class in a module A patches a class in module B, the system must esnure proper visibility between A and B classloaders. With the Compatible Superclass approach, the compatibility class in A typically uses types defined by B, but B must see A’s contents at run-time as B class will be made to extend A type (see below). The simplest way is to join contents of A and B in the same classloader.

If a module’s MANIFEST.MF defines OpenIDE-Module-Fragment-Host: header, the module becomes a Module Fragment and its contents is included into the fragment host’s module classloader.

Example

This is an example MANIFEST.MF of openide.filesystems module:

   Manifest-Version: 1.0
   OpenIDE-Module: *org.openide.filesystems*
   OpenIDE-Module-Localizing-Bundle: org/openide/filesystems/Bundle.properties
   OpenIDE-Module-Layer: org/openide/filesystems/resources/layer.xml
   OpenIDE-Module-Specification-Version: 9.0

A compatibility support module, which needs to merge with filesystems API at runtime uses the following MANIFEST:

   Manifest-Version: 1.0
   OpenIDE-Module: org.openide.filesystems.compat8
   OpenIDE-Module-Localizing-Bundle: org/openide/filesystems/compat8/Bundle.properties
   OpenIDE-Module-Specification-Version: 9.0
   *OpenIDE-Module-Fragment-Host: _org.openide.filesystems_ *

There’s no dependency from the real API module to the patch; the patch depends on the API module. The patch module may be eventually not present at all, if compatibility is not needed.

Compatible superclass

Because of JVM definition of method resolution, JVM looks not only in the class hosting the target method and specified as part of the Method Reference, but also in superclasses of that class. It’s therefore binary-compatible to move the methods to some superclass.

We must still prevent the superclass from appearing in the extends clause of the source, in order not to retain the dependencies from the superclass' dependency closure (the requirement was to avoid them). At run-time, the API class A which was compiled as extending superclass S, will be patched to extend another superclass, C. Provided that C extends S, type checks in the running JVM should not be affected. The superclass C can then add methods with illegal dependencies in their transitive dependency closure.

The class which delivers the binary-compatible implementation must be annotated using @PatchFor annotation, which also identifies the target class which should be modified at run-time. To preserve inheritance hierarchy properties, there are some rules to be followed. Given API class "A" which extends "X", and binary-compatible implementation class "A"

  • I must also extend X

  • I must define the constructors with the same signature as X

  • A must contain a default constructor, implicit or explicit

In addition, A and I must be loaded by the same classloader. To instruct NetBeans module system to do so, the module that contain I must list the following Manifest entry:

OpenIDE-Module-Fragment-Host: codename

where the codename identifies the original module which contains API class A.

Example

The AbstractFileSystem, in version 8.0 and earlier contains a number of @deprecated or obsolete methods:

 public abstract class FileSystem  {
     public abstract SystemAction[] getActions();
     @Deprecated
     public void prepareEnvironment(FileSystem.Environment env) throws EnvironmentNotSupportedException {
     ...
     }
     ...
 }

The methods are now moved to a class FileSystemCompat, which resides in a different module - openide.filesystems.compat8:

 @PatchFor(FileSystem.class)
 public abstract class FileSystemCompat {
     public abstract SystemAction[] getActions();
     @deprecated
     public void prepareEnvironment(FileSystem$Environment env) throws EnvironmentNotSupportedException {
       ...
     }
     ...
 }

The example also shows, how a static member type may be moved to a deprecated module; JVM signature does not contain information that FileSystem.Environment is a member type. FileSystem$Environment has the same signature.

Constructor delegate

API class A may have a constructor, which is no longer acceptable, because of its signature dependencies. If the constructor was just implemented in an 'unlucky' way, the implementation could be lobotomized, but if the constructor’s signature contain an unwanted dependency, it should be rather removed at all from the class.

To preserve backward compatibility, the constructor has to be added back at run-time. Although JVM linking algorithm would eventually find <init>()V method to call after new, the constructor "inherited" from the superclass would not be able to initialize the API class fields.

The initialization of the original API class is implemented by its default constructor - this means the API class must have default constructor, even though it is private. Delegation to other A constructors is not implemented yet, but is feasible.

Initialization of the superclass, or possibly setup of API (A) fields are delegated to a static "factory" method in the @PatchFor superclass. The initialization method must be annotated with @ConstructorDelegate. It’s first parameter must be of type of the compatible superclass itself and the rest of parameters must be the same as the to-be-generated constructor in the API class. Modifiers and declared exceptions are copied to the generated constructor.

Example

JarFileSystem has a constructor which takes FileSystemCapability. Since the type is long deprecated and we want to remove it, the relevant implementation moves off to the patch superclass:

 @PatchFor(JarFileSystem.class)
 public abstract class JarFileSystemCompat extends AbstractFileSystem {
     public JarFileSystemCompat() {
         super();
     }

     @ConstructorDelegate
     public static void createJarFileSystemCompat(JarFileSystemCompat jfs, FileSystemCapability cap) throws IOException {
         FileSystemCompat.compat(jfs).setCapability(cap);
     }
     ...
 }

The 1st argument of the @ConstructorDelegate method receives the newly created instance to be initialized. Since AbstractFileSystem does not (in sources) derive from FileSystemCompat, some runtime-typing magic must be done.

In effect, the bytecode generator creates a constructor in JarFileSystem:

     public JarFileSystem(FileSystemCapability cap) throws IOException {
         this();
         setCapability(cap);
     }