Writing Plugins for Nexus (Part 2)

July 24, 2009 By Tamas Cservenak

15 minute read time

As we said in first part of Nexus Plugins blog, Nexus itself is a Plexus application, we are quickly moving to OSGi runtime platform. The target for this new architecture is most probably Guice + Peaberry, a cool container and it's cool extension.

Okay, but will we need to rewrite every plugin after the migration happen? Well, hopefully no. This is actually the reason, why are we in hurry defining the Plugin API and plugin development environment as much as possible in a way, to make this migration as seamless as possible to plugin developers (core developers are already prepared for this challenge). In short, we are Nexus plugins from having to know anything of Plexus.

In perfect world, a plugin written today using this technique, against today's Nexus (powered by Plexus) should work without any problem after 6 months and the release of Nexus (powered by Guice + Peaberry). All you would have to do, is simply take your plugin bundle (a self-contained and self-sufficient package of the plugin) and move it from one Nexus to another. That's it.

Again, this is our "ultimate goal", but as with all software, we can't predict any tumbler. We are trying hard, at least. The rest of this post provides more guidance on writing Nexus plugins with some hints for how to prepare for the transition to OSGi.


How to create Nexus plugin for impatients (in 5 minutes)

We provided a bunch (and will be more) examples of how to implement some interesting or not so interesting plugins. Some of those are already mentioned in first part of Nexus Plugins blog (the "Nexus Kung Fu Virus Scanner Plugin" for example, but there are some changes, so update please!).

You can freely browse but also checkout the examples.

So, all you need for developing a plugin is (follow the list, the conditional items do only if needed):

* create a Maven module with packaging "nexus-plugin" (you need to add Maven Extension for this to work, look into the nexus-sample-plugins parent POM for this or simply reference the Nexus Plugins parent POM)

      <build>
        <extensions>
          <extension>
            <groupId>org.sonatype.plugins</groupId>
            <artifactId>app-lifecycle-nexus</artifactId>
            <version>1.0-SNAPSHOT</version>
          </extension>
        </extensions>
      </build>

* reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>org.sonatype.nexus</groupId>
        <artifactId>nexus-plugin-api</artifactId>
        <version>1.4.0-SNAPSHOT</version>
        <scope>provided</scope>
        <type>jar</type>
      </dependency>

* if you want plugin UTs, reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>org.sonatype.nexus</groupId>
        <artifactId>nexus-plugin-test-api</artifactId>
        <version>1.4.0-SNAPSHOT</version>
        <scope>test</scope>
        <type>jar</type>
      </dependency>

* if you have any "3rd party" dependency, declare them in your POM as usual (NOTE: the artifacts above do pull a lot of other non-nexus dependencies as transitive dependencies. Currently, we have no way to filter out 3rd party dependency that is in Nexus and also needed by you, hence, we are relying to you. Later, it will be solved by "advanced" Nexus Plugin Manager. So, for example, XStream is included in Nexus, you don't need to reference it. Right now, we rely on plugin developers to be aware of this limitation).

The list up to now will give you full access to Nexus Core API (Nexus API + Nexus Configuration), adding static REST resources (images, JS, CSS, etc) and writing JUnit tests for them. Let's go further:

* if contributing new kind of Repository, content type, grouping, or any other repository related component (you can do this using Core API only, but here are the helpful abstract classes!), reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>org.sonatype.nexus</groupId>
        <artifactId>nexus-proxy</artifactId>
        <version>1.4.0-SNAPSHOT</version>
        <scope>provided</scope>
        <type>jar</type>
      </dependency>

* if needs to tamper with Nexus in direct way (you don't!), reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>org.sonatype.nexus</groupId>
        <artifactId>nexus-app</artifactId>
        <version>1.4.0-SNAPSHOT</version>
        <scope>provided</scope>
        <type>jar</type>
      </dependency>

* if contributing new dynamic REST (programmatic) resources, reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>org.sonatype.nexus</groupId>
        <artifactId>nexus-rest-api</artifactId>
        <version>1.4.0-SNAPSHOT</version>
        <scope>provided</scope>
        <type>jar</type>
      </dependency>

* if relying (depending) on another nexus plugin, reference the following artifact in dependencies, scoped as provided:

      <dependency>
        <groupId>group.of.the.dependency.plugin</groupId>
        <artifactId>artifactId-of-the-nexus-plugin</artifactId>
        <version>version</version>
        <scope>provided</scope>
        <type>nexus-plugin</type>
      </dependency>

Don't worry about emphasized "provided" scopes. If you forget those, your build will fail and force you to fix them. The build will force you to use "provided" scope for the following artifacts:

* that are packaged as "nexus-plugin" (type in dependencies)

* that have the artifact groupID of "org.sonatype.nexus"

And now get to the work of writing the plugin:

* create implementation classes for the needed extension points in Nexus Core API

* add own managed components/beans if needed, and other classes as needed

* build, test and deploy to Nexus

* have a beer/Margarita/whatever and relax

That's all folks!

The plugin bundle

The result of the build above will result in plugin bundle (it will have same Maven coordinates as you build, but with added classifier "bundle" and packaged as ZIP file). The plugin bundle is meant as "self sufficient" (within Nexus of course) package of your plugin. You can look at it as WAR packaging is in J2EE Web Container world.

The bundle is plain ZIP file, that contains your plugin JAR and all the 3rd party dependencies of your plugin, and the idea of plugin ZIP is just to upload it to Nexus and should work. Get it from Nexus and upload it to another Nexus, and should work.

The plugins deployed to Nexus (runtime)

The plugins deployed to nexus are held in Plugin Repository. Actually, Nexus supports many of those, but comes with two repositories pre-configured:

* system plugin repository, that is kept in the application bundle, and will be replaced with every upgrade (as all other files coming from a release). It main purpose is to hold plugins coming with Nexus (plugins kept here makes the Nexus as "product" just as any other JAR in lib directory). No user tampering should happen here.

* user plugin repository, that is kept in Nexus work directory. It's content is preserved during upgrades, and mainly serves to keep user custom plugins.

In the case of a clash (GAV), the system repository always "wins". Meaning, you are not able to "override" an artifact just by placing it into user repository. If it is found in system repository, it will be always used. If not found, will fallback to user repository.

With these two repositories, a Nexus is completely extensible, but is also protected from being broken. Plugin Manager will simply not load the broken plugin, thus protecting Nexus instance as whole.

Note: plugin repository layout resembles, but is not Maven2 local repository layout. Also, the layout is still not finalized, and may change later! Please, never rely on physical layout of this repo (wrt tooling or whatever), and always use Nexus API to deploy or delete a plugin from repository.

Get a grip on Nexus

Right now (in this "transition" period), Nexus is extensible by following means:

* "old" way, like the pre-1.4 Nexuses, by throwing "plexus componentized" JARs into Nexus App classpath (lib directory). Yes, this "old" way still works, and this is intentional, but is highly discouraged since it is deprecated way for creating plugins for Nexus. Tomorrow, you will need to rewrite all of that to be able to run in new Nexus (the "old" style plugins were very much aware of and couple to Plexus Container around Nexus).

* "new way", starting from 1.4 Nexus. This is the way to go and is future-proof. You are intentionally coding in IoC container "unaware" way, yet, still having all the bells and whistles to use.

Let's look what changed in newest Nexus architecture: mostly the Plugins, actually the Nexus Plugin Manager (NPM) appeared, along with lot other technical changes irrelevant for this discussion. Also, pre-1.4 Nexus was loaded into one Realm (monolithic), and all Core JARs and Plugin JARs were loaded into that same Realm. That meant a lot of shading and other trickery to avoid runtime dependency clashes.

Since 1.4, each plugin is loaded into it's own Realm, that is child of the Core. This means, that two different plugin may use same dependency even with different version in one Nexus instance. But this still does not solve core-plugin dependency clash: plugin uses version x of dependency found in Nexus with version y (because plugin is in child Realm of core, and the common parent-child class loader processing occurs here).

Nexus Plugins.png

Also, one plugin may reference another Nexus plugin as dependency, hence it will get access to the classes and components defined in that other plugin (again, see the examples!). Right now, our inter-bundle dependencies are limited to "all or nothing" strategy. One important thing: when one plugin references another plugin, it will "import" only the plugin JAR classes (on the figure above only the pluginC.jar), all the private dependencies will remain "hidden"! Again, we implemented a strict subset of OSGi bundle import/export capabilities in Plexus and Classworld Realms, and we call it "all-or-nothing" strategy. A plugin, when referenced by another plugin, always exports all of itself. A plugin, when imports another plugin, always imports all of it. By "all" we mean the plugin JAR, and the dependencies remains hidden.

We added a lot of features but kept them "low-fi" intentionally, because we wanted to move forward without re-implementing too much of it from OSGi runtime stuff (the target we are moving to) in Plexus/Classworlds. So, currently we do have some strict-subset of "OSGi-like" class loading happening in Nexus, but all this is a tiny subset of OSGi capabilities, where, when moved to, we will be able to fully unleash it's power (and also make this transition less painful).

So, there is a question "what do you want to do?". We are still in flux here, but we are roughly getting to the place to be able to give-away real plugin development tooling and environment for Nexus, and be unaware of what and how powers Nexus.

A word for Plexus zealots

The Plugin Manager and the implementation above relies completely on stable 1.0.x line of Plexus and Classworlds. The only changes we did is how child Realms are created, programatically adding the "imports" (inter plugin dependencies) which is easy since we know the plugin Realm constituents, hence by scanning the plugin JAR will get us what referenced plugin "exports", and we simply import those classes into current plugin realm (Classworld Realms supports only "imports", but no "exports" in OSGi way). Hence, with little plugin descriptor trickery and metadata, every plugin prepares it's "exports", which is simply a list of it's content, and in case of being referenced, it simply hands over that list of classes. The importer's Realm is then adjusted with proper changes on his Realm. But what is happening in Plexus and lookups? How does components declared in child components get picked up by core components? The only one thing we had to "customize" in Plexus is the ComponentRepository but even that is done by simply configuring Plexus properly. The default ComponentRepository of Plexus is replaced by customized ComponentRepository from Nexus Plugin Manager. And the new ComponentRepository does the following very simple change (only changed method is getComponentDescriptors(String role)):

* when a lookup occurs in core realm, collect all 1st level children too into realms set to search (hence, component in core will pick up from Plugin Realms the contributed components)

* when a lookup occurs in a plugin realm, fallback to default behavior (add child and scan all parents upwards and add them to set)

* when a lookup occurs in a plugin realm, that imports another plugin, add all the imported plugin realms to the set of realms to search.

That's all. I emphasize, that the changes above are only about finding the components, not about class loading. We will probably refine this, but the current state works just fine. Later, we will probably add some sort of "filtering", or simply do the above steps if a role marked with @ExtensionPoint is looked up, otherwise fall back to "default" behavior. Yes, Plexus, despite being one of the oldest container, is still very powerful.

Extending Nexus Core API

In case of "simplest" (in technical way, but this way is not more "simpler" on less powerful than anything else) plugins, all the developer needs to do is simply create implementation classes for Nexus Core API interfaces. That's all. Yes it is.

Right now, Nexus has about 40 "extension point interfaces". Soon, we will publish their description as part of Nexus Plugin API.

Extending Nexus REST API with static resources

Pretty much covered also by "nexus-plugin" packaging. All you have to do is to pack static resources you want to get published into src/main/resources/static folder and subfolders. They will be published automatically upon plugin deploy. This way, you can add JavaScript, images, CSS and anything static into Nexus.

Example: if you copy an image.png to src/main/resources/static, upon Nexus start and successfully loading the plugin, it will be accessible on http://localhost:8081/nexus/static/image.png. No more fuss doing it by hand.

Extending Nexus REST API with new REST services

This is kinda ugly right now. Our current "plexus-restlet-bridge" component, used to create REST resources for Nexus is too Plexus and Restlet coupled, and we look at this component as "dead end". We will either move to JSR311 and have some swappable implementation (with some radical change in REST resources), or simply reuse some other cool library out there. More about this later.

In short, creating "active" REST resource within Nexus Plugin did not change from "old" style. You will still have to create Plexus components and extend those ugly Plexus and Restlet bounded abstract classes.

Extending Nexus Security

This is more extending Spice Security (and JSecurity, that Spice Security wraps) then Nexus. Usually, as far it concerns Nexus, it is only about adding REST API and UI for newly added Realm, and it's configuration. This is almost always covered by adding some static REST resources to Nexus (JavaScript files). Bug Brian for a blog entry about this.

Plugin, Extension, what else?

In general, to avoid confusion, "old style" plugins that are getting into core Realm (classloader), we will start calling "extension". As I said, Nexus Plugins are loaded into separated Classloader from now on. It will be kinda Maven-like Plugins and Extensions.

Extension is loaded into core realm, and may replace system parts. A Plugin is more isolated and cannot "override" system components. I will not cover extensions here, since they are more or less similar to the "old style" plugins (Plexus componentized JARs). This path is not done yet, but is wide open.

Other technicalities

Other things to consider in this change cover a lot of things. As we are moving away from Plexus, Nexus Plugin developer should avoid using Plexus "native" features and classes, like Plexus Logger, lifecycle interfaces, etc.

Logging in plugin

Nexus generally used Plexus Logging, but under the hud we used SLF4J from start (the SLF4J LoggerManager in Plexus). Simply taken, SLF4J is needed right now, to be able to "focus" all the different library requirements into one sink (Restlet is using JUL, Jetty Log4j, Appache Commons HttpClient 3.x commons-logging, etc.). With SLF4J we are able to direct all these logs into one sink ("backend" in SLF4J terminology), and that is Log4j.

To prevent further "lock ups" as regarding Nexus and logging, we suggests to take the following into consideration when creating a Nexus plugin:

* use the SLF4J API directly for doing logging, but

* we do not believe that "static" logger initialization is a great thing (but we can bear with it). So, use your imagination and have your loggers injected, just as you inject anything else.

* Nexus API provides a SLF4J logger Provider interface out of the box, you should use that.

* do NOT use Log4j directly for logging. Yes, some Nexus parts are bound directly to Log4j, but you should not increase the mess we have.

Component/Bean lifecycles

Plexus has the Startable and Initializable interfaces to offer life-cycle management. Obviously, we will need to have some alternatives here.

For now, we live with Plexus interfaces, if needed.

How deep to go?

Always try to be as "shallow" as possible when doing a Nexus plugin (at least regarding to which modules you want to depend). Naturally, this is not always possible and heavily depends on what are you trying actually to do. For example, the "Kung Fu Viru Scanner" plugin is very "shallow", as it references the nexus-api only to do the job. But if you are more deeply extending Nexus, and are for example adding a new repository type (see "Nexus Simple Repository Plugin"), then you may reference modules that are more "deeply" in Nexus (nexus-proxy in this case). In general, more deeper you go, be more careful.

Not that it is discouraged, it is meant exactly like this. We surely don't expect from you to not reuse all the code in those modules (like AbstractRepository is), and have simplify your work.

Existing plugins and their conversion to "new" way

If you manage some already existing Nexus plugin out there, the transition is easy for you. Steps to take:

* modify packaging to "nexus-plugin" (you need to add Maven Extension for this to work, look into the nexus-sample-plugins parent POM for this or simply reference the Nexus Plugins parent POM)

      <build>
        <extensions>
          <extension>
            <groupId>org.sonatype.plugins</groupId>
            <artifactId>app-lifecycle-nexus</artifactId>
            <version>1.0-SNAPSHOT</version>
          </extension>
        </extensions>
      </build>

Rebuild, and voila, you have "new" plugin. Well, sort of. This will make your JAR new kind of plugin (will have plugin descriptor embedded), and plugin ZIP bundle will be produced, but the contents of your JAR will intact. And this will work, because Nexus will still "honor" the old way of extensions. So, further steps:

* move away from Plexus annotations, and start using the new nexus-plugin-api (this usually means simply remove the plexus-component-metadata artifact, remove the @Component from stuff, since implementing an "extension point interface" from Core is just enough).

For example of "before" and "after", here is the change set of Nexus Maven Archetype plugin, that was initially a plugin for Nexus 1.3.x line, and then upgraded to 1.4:

* Changes needed to upgrade

* for better insight, here is the trunk that is now 1.4 plugin

* for better insight, here is the branch for 1.3.x compatible line

And finally, the bad news

Yes, we have bad news too. As Nexus Core goes generic, -- at least as "repository management" is concerned -- and not being Maven bound, the current nexus-rest-api module and our UI is delayed in doing this. Simply said, even if Nexus Core is extensible with new repository types (like our OBR or P2 is, for example), the UI -- and REST API -- is still unable to "follow" it. For example, UI is unable to manage all "custom" properties out-of-the-box on a new, plugin contributed repository. You may doing it, but not without digging deeply into UI JS code. Definitely not a way to go.

For that reason, in 1.4 version of Nexus we focused on Core and Plugins and extendability of them, but we tried to smack nexus-rest-api and UI into "compatibility" mode and minimize changes there.

This one problem may be easily overcome today: if you create some very special kind of repository, that needs a lot of "extra" configuration, create a new panel or even new page in UI, and do your custom stuff there. In future, I believe we will have as much pluggable UI (written in Java, so I would bet to some sort of GWT) as pluggable as Core is.

Also, as I mentioned, our current REST integration sucks. This, and the UI explained above, is the reason why I tried to shove a lot of dirty "compatibility code" into nexus-rest-api module, and that module will be ditched soon, and replaced with some new code. And that new code will solve the plugins too.

But right now, we have to live with it.

Tags: Nexus Repo Reel

Written by Tamas Cservenak

Tamas is a former Senior Develoepr at Sonatype. He has over 15 years of experience developing software systems in Public Services, Telco and Publishing industries.