Last updated Oct 27, 2023

Interpret the License API output

As of Feb 15, 2024, Atlassian Marketplace will no longer offer sales and support for server apps. We are in the process of updating the server related information in DAC to reflect this change and some of the existing information may not align with the current experience. If you're considering cloud or have migrating customers, here's the step-by-step guide to help moving your app and customers to cloud.

The Adding licensing support to server apps tutorial contains a step where users use the atlas-create-jira-plugin-module command to put License API support into the app. This command generates a number of changes in a project. This page describes the output from this generator.

LicenseHelloWorldServlet class

The LicenseHelloWorldServlet class contains example of simple app functionality. The servlet displays a different message depending on whether the app has a license or not. It is best practice to execute a license validity check at every app entry point (e.g., servlet, REST resource, and webwork action). This ensures that users cannot use your app without a valid license.

Generating this example servlet is optional.

LicenseServlet class

The generator creates the LicenseServlet class. The LicenseServlet class acts as a license administration page. This page is necessary to support input of a license key in host applications where UPM 2.0 is not installed. The servlet includes the following key methods:

MethodDetails

doGet()

Renders the page or redirects to the login if the user is not logged in.

doPost()

Handles POST operations on the page.

addEligibleMarketplaceButtons()

A private method that determines which Marketplace buttons are possible within the page's current state.

initVelocityContext()

A private method that builds the page.

hasAdminPermission()

A private method that determines if the current user is authorized to enter a license.

The license administration screen displays buttons for each Marketplace action (Buy, Try, Renew, and Upgrade) that the app is eligible for. For example, if the app is unlicensed, Buy and Try buttons display. If the app has a maintenance-expired license, a Renew button displays. Clicking any of these buttons fires a POST request to My Atlassian.

Following the acquisition of the new license, My Atlassian installs the license into the user's Atlassian product and redirects back to the source (in this example, the license administration page).

When a licensing-aware UPM is detected your app no longer uses the servlet to update the license. Attempts to update through the servlet are not allowed. If a licensing-aware UPM is present, it disables the mechanism to update your app license. Instead, it displays a message to the user that updating their license should occur in UPM and provide a URI to redirect to.

The generator adds two files to src/main/resources — the /license-admin.vm file and the atlassian-plugin.properties file. These files support internationalization of the administration page defined by LicenseServlet.

Changes to the pom.xml file

Both servlets have dependencies on the License API. This section details the dependency changes made by the generator.

Import licensing packages

The generator ensures that Maven imports the appropriate packages by adding the following <dependency> entries to <dependencies> element:

1
2
<dependency>
    <groupId>com.atlassian.upm</groupId>
    <artifactId>plugin-license-storage-lib</artifactId>
    <version>${upm.license.compatibility.version}</version>
    <scope>compile</scope> <!-- intentionally compile scoped -->
</dependency>
<dependency>
    <groupId>com.atlassian.upm</groupId>
    <artifactId>plugin-license-storage-plugin</artifactId>
    <version>${upm.license.compatibility.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.upm</groupId>
    <artifactId>licensing-api</artifactId>
    <version>${upm.license.compatibility.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.upm</groupId>
    <artifactId>upm-api</artifactId>
    <version>${upm.license.compatibility.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.sal</groupId>
    <artifactId>sal-api</artifactId>
    <version>2.4.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework.osgi</groupId>
    <artifactId>spring-osgi-core</artifactId>
    <version>1.1.3</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.4</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.templaterenderer</groupId>
    <artifactId>atlassian-template-renderer-api</artifactId>
    <version>1.0.5</version>
    <scope>provided</scope>
</dependency>

Instructions for the maven-jira-plugin

The generator also edits the maven-jira-plugin artifact's configuration in the <plugins> section and adds some instructions. These instructions allow your app to dynamically detect the Licensing API when it is available.

These instructions are in a <plugin> element within <plugins> and look like the following:

1
2
<plugin>
    <groupId>com.atlassian.maven.plugins</groupId>
    <artifactId>maven-jira-plugin</artifactId>
    <version>${amps.version}</version>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            <Private-Package>
                com.atlassian.upm.license.storage.lib*
            </Private-Package>
            <DynamicImport-Package>
                com.atlassian.upm.api.license;version="2.0.1",
                com.atlassian.upm.api.license.entity;version="2.0.1",
                com.atlassian.upm.api.util;version="2.0.1",
                com.atlassian.upm.license.storage.plugin;version="${upm.license.compatibility.version}"
            </DynamicImport-Package>
        </instructions>
        <productVersion>${jira.version}</productVersion>
        <productDataVersion>${jira.version}</productDataVersion>
    </configuration>
</plugin>

What is the purpose of the OSGi instructions?

You may be wondering why you have a fully functional app even though you've never before specified Import-Package, Private-Package, or other directives. That is because the Atlassian plugins framework does it for you. However, the licensing support code requires these two directives: Private-Package to keep the licensing support library that is included in your app from being visible to other apps, and DynamicImport-Package to allow the License Support API to be detected dynamically at runtime. You do not need to specify an other OSGi instructions if you weren't already doing so.

Additionally, since we are using version 3.9+ of the maven-jira-plugin, our app's manifest will automatically include the Atlassian-Build-Date entry which is used to enforce license maintenance expiration.

maven-dependency-plugin usage

The generator also adds the following <plugin> to your build configuration alongside maven-jira-plugin.

1
2
<plugin>
    <!-- Include the License Storage app artifact such that it can be found on the classpath when we need to install it -->
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-storage-plugin</id>
            <phase>process-resources</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.outputDirectory}</outputDirectory>
                <includeArtifactIds>plugin-license-storage-plugin</includeArtifactIds>
                <stripVersion>true</stripVersion>
            </configuration>
        </execution>
    </executions>
</plugin>

This maven-dependency-plugin configuration copies the plugin-license-storage-plugin JAR artifact onto your app's classpath. At runtime, when your app requires the License Storage app but cannot find a sufficient version installed, your app is able to install it.

bundledArtifact entries

The generator adds the following <bundledArtifact> entries to the maven-jira-plugin configuration.

1
2
<plugin>
    <groupId>com.atlassian.maven.plugins</groupId>
    <artifactId>maven-jira-plugin</artifactId>
    <version>${amps.version}</version>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            ...
        </instructions>
        <productVersion>${jira.version}</productVersion>
        <productDataVersion>${jira.version}</productDataVersion>
        <bundledArtifacts>
            <bundledArtifact>
                <groupId>com.atlassian.upm</groupId>
                <artifactId>atlassian-universal-plugin-manager-plugin</artifactId>
                <version>1.6.3</version>
            </bundledArtifact>
            <bundledArtifact>
                <groupId>com.atlassian.upm</groupId>
                <artifactId>plugin-license-storage-plugin</artifactId>
                <version>${upm.license.compatibility.version}</version>
            </bundledArtifact>
        </bundledArtifacts>
    </configuration>
</plugin>

The first entry bundles UPM 1.6 into the app configuration. UPM 1.6 does not expose the licensing API, however, it is able to self-update to UPM 2.0 which does. So, this app version allows us to easily test both when UPM is, and is not, licensing-aware.

The second entry bundles the License Storage app into your app's configuration. In certain situations, AMPS can have problems enabling this app because of the dynamic nature of how the app is installed. This problem and workaround only affect your app while in its development environment; the License Storage app is installed and enabled just fine in a production environment.

Changes to the atlassian-plugin.xml file

The generator makes a number of changes to the descriptor file.

Enabling licensing

The generator adds the atlassian-licensing-enabled parameter to plugin-info

1
2
<param name="atlassian-licensing-enabled">true</param>

This parameter tells UPM 2.0+ that the app is Marketplace-paid, meaning it has a license that is UPM 2.0+ compatible.

Add component support

The License API has a number of components it depends on. These components include:

Interface

Description

PluginAccessor

Required by the embedded library.

PluginController

Required by the embedded library.

TransactionTemplate

Required by the embedded library.

ApplicationProperties

Required by the embedded library.

TemplateRenderer

Required by the embedded library.

PluginSettingsFactory

Required by the embedded library.

LoginUriProvider

Required by the embedded library.

PluginLicenseStoragePluginInstaller

Determines whether or not the License Storage app is needed. If the storage app is needed, this component installs it. Other than declaring it here, your code does not need to do anything else with it.

ThirdPartyPluginLicenseStorageManagerImpl

Used to access and manage your app license.

AtlassianMarketplaceUriFactoryImpl

Used to create URIs to interact with My Atlassian and generate licenses when appropriate.

The generator adds <component> elements to the atlassian-plugin.xml file for these:

1
2
<component-import key="pluginAccessor" interface="com.atlassian.plugin.PluginAccessor"/>
<component-import key="pluginController" interface="com.atlassian.plugin.PluginController"/>
<component-import key="txTemplate" interface="com.atlassian.sal.api.transaction.TransactionTemplate"/>
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<component-import key="templateRenderer" interface="com.atlassian.templaterenderer.TemplateRenderer"/>
<component-import key="pluginSettingsFactory" interface="com.atlassian.sal.api.pluginsettings.PluginSettingsFactory"/>
<component-import key="loginUriProvider" interface="com.atlassian.sal.api.auth.LoginUriProvider"/>
<component-import key="userManager" interface="com.atlassian.sal.api.user.UserManager"/>
<component-import key="i18nResolver" interface="com.atlassian.sal.api.message.I18nResolver"/>
<component key="pluginLicenseStoragePluginInstaller" class="com.atlassian.upm.license.storage.lib.PluginLicenseStoragePluginInstaller"/>
<component key="thirdPartyPluginLicenseStorageManager" class="com.atlassian.upm.license.storage.lib.ThirdPartyPluginLicenseStorageManagerImpl"/>
<component key="atlassianMarketplaceUriFactory" class="com.atlassian.upm.license.storage.lib.AtlassianMarketplaceUriFactoryImpl"/>

servlet Elements

The descriptor must also reference each servlet in your code. The generator adds these files in support of them:

1
2
<servlet name="License Servlet" i18n-name-key="license-servlet.name" key="license-servlet" class="com.example.plugins.tutorial.servlet.LicenseServlet">
  <description key="license-servlet.description">The License Servlet Plugin</description>
  <url-pattern>/com.example.plugins.tutorial.plugin-license-compatibility-tutorial/license</url-pattern>
</servlet>
<servlet name="License Hello World Servlet" i18n-name-key="license-hello-world-servlet.name" key="license-hello-world-servlet" class="com.example.plugins.tutorial.servlet.LicenseHelloWorldServlet">
  <description key="license-hello-world-servlet.description">The License Hello World Servlet Plugin</description>
  <url-pattern>/com.example.plugins.tutorial.plugin-license-compatibility-tutorial/licensehelloworld</url-pattern>
</servlet>

Rate this page: