You’re using the popular Confluence wiki? You’re using its RPC/SOAP API and missing a function you really need? Just extend the capabilities of the Confluence RPC API by programming a custom web service component – it is really easy and also well documented.
In this tutorial we’re going to take a look on how to quickly implement a SOAP service, securing it and putting its methods in a transactional context.
Prerequisites
-
Maven >=2
-
JDK >= 5
-
Confluence Wiki >= 3.0 (for a quick installation guide take a look at this article)
-
SoapUI for Testing
Creating a simple SOAP service without authorization
To create a simple example first we’re going to develop a simple service that prints a string, fantastic! ;)
-
Create a new Maven project using Atlassian’s archetype or use the Atlassian SDK (its just a wrapped maven instance ..). If you choose the first option, be sure to select confluence-plugin2-archetype
mvn archetype:generate -DarchetypeCatalog=http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog
-
My edited Maven descriptor pom.xml looks like this
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <parent> <groupId>com.atlassian.confluence.plugin.base</groupId> <artifactId>confluence-plugin-base</artifactId> <version>25</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.hascode.confluence.tutorial</groupId> <artifactId>soap-component-example</artifactId> <version>0.0.1-SNAPSHOT</version> <name>hasCode.com Confluence SOAP Component Example</name> <packaging>atlassian-plugin</packaging> <properties> <atlassian.plugin.key>com.hascode.confluence.tutorial.soap-component-example</atlassian.plugin.key> <atlassian.product.version>3.1</atlassian.product.version> <atlassian.product.test-lib.version>1.4.1</atlassian.product.test-lib.version> <atlassian.product.data.version>3.1</atlassian.product.data.version> <confluence.version>3.1.1</confluence.version> <confluence.data.version>3.1</confluence.data.version> </properties> <repositories> <repository> <id>atlassian-public</id> <url>https://maven.atlassian.com/repository/public</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> <repository> <id>atlassian-m1-repository</id> <url>https://maven.atlassian.com/maven1</url> <layout>legacy</layout> </repository> <repository> <id>atlassian-unknown</id> <name>atlassian-unknown</name> <url>http://repository.atlassian.com/</url> </repository> <repository> <id>maven2-repository.dev.java.net</id> <name>Java.net Repository for Maven</name> <url>http://download.java.net/maven/2/</url> <layout>default</layout> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>atlassian-public</id> <url>https://maven.atlassian.com/repository/public</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> </project>
-
The webservice is exposed via interface so we need a service interface and an implementing class..
-
This is our service class ExampleService in the package com.hascode.confluence.tutorial.ws.service
package com.hascode.confluence.tutorial.ws.service; public class ExampleService implements ExampleServicePublic { /* * (non-Javadoc) * * @see com.hascode.confluence.tutorial.ws.service.ExampleServicePublic# * getSpaceInformation() */ public String getSpaceInformation() { return "Isn't that a very useful information?"; } }
-
This is the interface for our webservice named ExampleServicePublic
package com.hascode.confluence.tutorial.ws.service; public interface ExampleServicePublic { public abstract String getSpaceInformation(); }
-
At last we need to declare our SOAP service in the atlassian-plugin.xml
<?xml version="1.0"?> <atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" pluginsVersion="2"> <plugin-info> <description>${project.name}</description> <version>${project.version}</version> <vendor name="hasCode.com" url="https://www.hascode.com"/> </plugin-info> <rpc-soap key="sample-component-soap" name="SOAP Example Service" class="com.hascode.confluence.tutorial.ws.service.ExampleService"> <description>Example SOAP Service</description> <service-name>ExampleServicePublic</service-name> <service-path>soapexample</service-path> <published-interface>com.hascode.confluence.tutorial.ws.service.ExampleServicePublic</published-interface> <authenticate>false</authenticate> </rpc-soap> </atlassian-plugin>
-
Now build the plugin using mvn package and deploy it
-
Finally you just need to activate “Remote API (XML-RPC & SOAP)“___in your Confluence via _Administration Console >> General Configuration
Testing
We want to see if the created SOAP service is correctly exposed and responsive and explorable via WSDL.
-
If you open the address http://<yourhost>:<yourport>/rpc/soap-axis/soapexample?WSDL in your browser you should be able to see a WSDL declaration like this one
<?xml version="1.0" encoding="UTF-8"?> <wsdl:definitions xmlns:apachesoap="http://xml.apache.org/xml-soap" xmlns:impl="http://localhost:8080/rpc/soap-axis/soapexample" xmlns:intf="http://localhost:8080/rpc/soap-axis/soapexample" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://localhost:8080/rpc/soap-axis/soapexample"> <!--WSDL created by Apache Axis version: 1.2.1 Built on Jun 14, 2005 (09:15:57 EDT) --> <wsdl:message name="getSpaceInformationResponse"> <wsdl:part name="getSpaceInformationReturn" type="xsd:string"/> </wsdl:message> <wsdl:message name="getSpaceInformationRequest"> </wsdl:message> <wsdl:portType name="ExampleServicePublic"> <wsdl:operation name="getSpaceInformation"> <wsdl:input message="impl:getSpaceInformationRequest" name="getSpaceInformationRequest"/> <wsdl:output message="impl:getSpaceInformationResponse" name="getSpaceInformationResponse"/> </wsdl:operation> </wsdl:portType> <wsdl:binding name="soapexampleSoapBinding" type="impl:ExampleServicePublic"> <wsdlsoap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="getSpaceInformation"> <wsdlsoap:operation soapAction=""/> <wsdl:input name="getSpaceInformationRequest"> <wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://service.ws.tutorial.confluence.hascode.com" use="encoded"/> </wsdl:input> <wsdl:output name="getSpaceInformationResponse"> <wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://localhost:8080/rpc/soap-axis/soapexample" use="encoded"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="ExampleServicePublicService"> <wsdl:port binding="impl:soapexampleSoapBinding" name="soapexample"> <wsdlsoap:address location="http://localhost:8080/rpc/soap-axis/soapexample"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
-
Start SoapUI and create a new project with the WSDL URL from above as in this screenshot
-
Click on the method getSpaceInformation and run the request – you should see the string “Isn’t that a very useful information?”
Adding Transaction Context to the Service
In the next step we’re going to pull some information from Confluence and wrap it in a transactional context.
-
We’re going to need Atlassian’s Shared Access Layer(SAL) for this so add this dependency and property to your pom.xml
<properties> [..] <sal.version>2.0.16</sal.version> </properties> <dependencies> <dependency> <groupId>com.atlassian.sal</groupId> <artifactId>sal-api</artifactId> <version>${sal.version}</version> <scope>provided</scope> </dependency> </dependencies>
-
Now import the TransactionTemplate by defining a component-import in the atlassian-plugin.xml
<component-import key="transactionTemplate"> <description>Import the com.atlassian.sal.api.transaction.TransactionTemplate</description> <interface>com.atlassian.sal.api.transaction.TransactionTemplate</interface> </component-import>
-
Now inject the TransactionTemplate into the service class by setter injection and put some logic into the service method
package com.hascode.confluence.tutorial.ws.service; import com.atlassian.confluence.spaces.Space; import com.atlassian.confluence.spaces.SpaceManager; import com.atlassian.sal.api.transaction.TransactionCallback; import com.atlassian.sal.api.transaction.TransactionTemplate; public class ExampleService implements ExampleServicePublic { private TransactionTemplate transactionTemplate; private SpaceManager spaceManager; /* * (non-Javadoc) * * @see com.hascode.confluence.tutorial.ws.service.ExampleServicePublic# * getSpaceInformation() */ public String getSpaceInformation() { return (String) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction() { String spaceInformation = "Our spaces are: "; for (Space space : spaceManager.getAllSpaces()) { spaceInformation += " " + space.getName(); } return spaceInformation; } }); } public void setTransactionTemplate(TransactionTemplate transactionTemplate) { this.transactionTemplate = transactionTemplate; } public void setSpaceManager(SpaceManager spaceManager) { this.spaceManager = spaceManager; } }
-
Build and deploy the changed plugin. I have noted that in some Confluence versions it is neccessary to restart Confluence – otherwise you receive an exception “java.lang.IllegalArgumentException: object is not an instance of declaring class“
-
Run the request again using SoapUI – you should receive a string with all your space’s names listed
<ns1:getSpaceInformationResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://service.ws.tutorial.confluence.hascode.com"> <getSpaceInformationReturn xsi:type="xsd:string">Our spaces are: Demonstration Space</getSpaceInformationReturn> </ns1:getSpaceInformationResponse>
Adding Authorization
The information about the Confluence spaces is of course highly confidential so we’re going to need some authentication for our webservice. Atlassian has made our life very easy here so we just need some steps to enable this on our service:
-
Change the service declaration in the atlassian-plugin.xml set authenticate to true
<authenticate>true</authenticate>
-
The Service must implement the login and logout methods from com.atlassian.confluence.rpc.SecureRpc but we don’t have to write any implementation code here .. those methods are intercepted :D
-
Out Service methods must accept a String as first argument (the token from the login method call) – again we don’t have to handle this parameter .. interception ftw
-
The service interface now looks like this
package com.hascode.confluence.tutorial.ws.service; import com.atlassian.confluence.rpc.SecureRpc; public interface ExampleServicePublic extends SecureRpc { public abstract String getSpaceInformation(String token); }
-
Out service implementation has changed to this
package com.hascode.confluence.tutorial.ws.service; import com.atlassian.confluence.rpc.AuthenticationFailedException; import com.atlassian.confluence.rpc.RemoteException; import com.atlassian.confluence.spaces.Space; import com.atlassian.confluence.spaces.SpaceManager; import com.atlassian.sal.api.transaction.TransactionCallback; import com.atlassian.sal.api.transaction.TransactionTemplate; public class ExampleService implements ExampleServicePublic { private TransactionTemplate transactionTemplate; private SpaceManager spaceManager; /* * (non-Javadoc) * * @see com.hascode.confluence.tutorial.ws.service.ExampleServicePublic# * getSpaceInformation() */ public String getSpaceInformation(String token) { return (String) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction() { String spaceInformation = "Our spaces are: "; for (Space space : spaceManager.getAllSpaces()) { spaceInformation += " " + space.getName(); } return spaceInformation; } }); } public void setTransactionTemplate(TransactionTemplate transactionTemplate) { this.transactionTemplate = transactionTemplate; } public void setSpaceManager(SpaceManager spaceManager) { this.spaceManager = spaceManager; } /* * (non-Javadoc) * * @see com.atlassian.confluence.rpc.SecureRpc#login(java.lang.String, * java.lang.String) */ public String login(String user, String password) throws AuthenticationFailedException, RemoteException { return null; } /* * (non-Javadoc) * * @see com.atlassian.confluence.rpc.SecureRpc#logout(java.lang.String) */ public boolean logout(String token) throws RemoteException { return false; } }
-
Build and deploy (and restart) . The WSDL has changed so be sure to remove the obsolete soap binding in SoapUI and add the new WSDL .. you should see three methods listed now: getSpaceInformation, login and logoutThe WSDL has changed so be sure to remove the obsolete soap binding in SoapUI and add the new WSDL .. you should see three methods listed now: getSpaceInformation, login and logout
-
Try to run the getSpaceInformation method – you should receive an UserException
-
Now run first a request for the method login – don’t forget to modify the request to send your Confluence username and password. In the response you will find the token needed for the next operation.
-
Run the method getSpaceInformation again but with the token from the login request provided – you should receive the spaces list without an error now.
Creating a SOAP client
I have written two articles that might help here ..
The fastest way is by using Axis as described in the first article because you don’t need to adjust some binding that JAX-WS criticises.