OSLC Rational Team Concert/Change Management (Workitem) Perl API
OSLC Open Services for Lifecycle Collaboration (OSLC) is a technology that allows software lifecyle tools to share data. See Consuming Rational Team Concert’s OSLC Change Management V2 Services for more details describes what the server provides for an API.
Lyo, Eclise Lyo, is an Eclipse project that focuses on providing an SDK to help adopt OSLC (Open Services for Lifecycle Collaboration) specifications and build OSLC-compliant tools.
Lyo includes a project that provides a perl API to interact with RTC. The primary function of the API is to query and update workitems in RTC. You can also goto Perl API Web Conference to view a web conference that describes this API. Max Vohlken contributed the perl API to Lyo.
Getting Started
The first step in using the API is to download the modules from Lyo. The modules are in a GIT repository, so you’ll need to download a GIT client.
GIT is an open source, source code control system. You can find out more about it at git – everything is local. There are also Eclipse plugins that integrate GIT into Eclipse Git With Eclipse .
The Lyo product is stored in a GIT repository at Perl API Repository You will also need to install perl Perl to run the tool.
Once you download the files onto your system you will need to get the Rest::Client and XML::Simple from CPAN: CPAN
Organization
The files are organized by product and then by domain. OSLC.pm is the base class common to all products and domains. Below that RTC.pm handles functions specific to RTC. And finally, below that CM.pm handles functions in the change management domain. The RDF.pm module handles management of RDF documents.
Lyooslcoslc.pm
Lyooslcrtc.pm
Lyooslcrdf.pm
Lyooslcrtccm.pm
Command Line Tool
The command line tool is a client application that invokes the underlying APIs. It’s a good starting point for getting an understanding of the API.
Sample Usage rtcclient -baseuri <RTC jazz url> -user <user> -password <password> <url> rtcclient -baseuri <RTC jazz url> -user <user> -password <password> \ -project <project area> \ -oslcwhere <simple query> \ [-property <property> ...] rtcclient -baseuri <RTC jazz url> -user <user> -password <password> \ -workitem <work item #> \ [-property <property> ...] \ [-update <field>=<value> ...] rtcclient -baseuri <RTC jazz url> -user <user> -password <password> \ -project <project area> \ -enum <enum> rtcclient -baseuri <RTC jazz url> -user <user> -password <password> \ -project <project area> \ -state <state> rtcclient -baseuri <RTC jazz url> -user <user> -password <password> \ -project <project area> \ -resolution <resolution>
Overall Usage
$usage Options: -baseuri <RTC jazz url> - The base URI of the RTC server -user <user> - RTC user id to authenicate with -password <password> - RTC password -format <xml|perl|json> - Specify the format of the result. -project <project area> - The project area to run queries against -oslcwhere <simple query> - Simple query style query string. Reference: http://open-services.net/bin/view/Main/CmQuerySyntaxV1 -property <property> - The property to retrieve using -oslcwhere or -workitem. More than one property can be retrieved by supplying more than one -property option. Reference: http://open-services.net/bin/view/Main/CmSpecificationV2#Resource_ChangeRequest -workitem <work item #> - Retrieve a specific work item -update <field>=<value> - Update field <field> with the new value of <value> in the work item specified with -workitem. More than one field can be update by supplying more than one -update option. <field>=<value> has to be contained in one command line argument so remember to quote appropriately. -rootservices - Retrieve the root services document. -services <project area> - Retrieve the services document for <project area>. -enum <enum type> - Retrieve all of the enumeration records with the internal name <enum type>. -enumItem <name> - Retrieve the enumeration that belongs to the -enum enumeration type with the name <name>. -states <state type> - Retrieve all of the state records with the internal name <state type>. -state <name> - Retrieve the state that belongs to the -states state type with the name <name>. -resolutions <resolution type> - Retrieve all of the resolution records with the internal name <resolution type>. -resolution <name> - Retrieve the resolution that belongs to the -resolutions resolution type with the name <name>. -iterations - Retrieve all of the iteration records. -iteration <name> - Retrieves the iteration with the name <name>.
Examples
API details
Let’s walk through the code for the examples below.
rtcclient -user me -password pw -baseuri https://localhost::9443/jazz \ -project MyProject -oslcwhere dcterms:identifier="99" -prop "dcterms:creator" rtcclient -user me -password pw -baseuri https://localhost::9443/jazz \ -workitem 99 -update 'dcterms:description->{content}=99 beers on the wall'
A note for Windows users: some of the examples in the code use single quotes (‘) around arguments, use double quotes (“) instead. Single quotes are not supported.
You can follow along using your server to trace through the discovery documents. You’ll need to download Poster, into Firefox, and add the header values OSLC-Core-Version=2.0, Accept=application/rdf+xml, Content-Type=application/rdf+xml
Example 1
The rtcclient batch file invokes perl modules, the API. They interact with the server, run queries and update workitems.
Top level API in batch File
The batch file includes perl code that interpret the parameters passed to the batch file, packages them up, and passes them to the underlying my $client = Lyo::OSLC::RTC::CM->new;
object.
my $client = Lyo::OSLC::RTC::CM->new; $client->credentials($opt{'user'}, $opt{'password'}); $client->setBaseURI($opt{'baseuri'}); if($opt{'oslcwhere'}) { my $data = $client->oslcWhere( $opt{'project'}, 'Change request queries', # Hard code the queryCapability name. $opt{'oslcwhere'}, defined($opt{'property'}) ? $opt{'property'} : ['*{*}'] );
Logging in
Notice that in the above code there is no explicit login. The credentials are supplied, but the sub oslcWhere
, is called directly. From the client perspective, logging in requires only that you pass a user name and password into the credentials call for the client.
The underlying code in RTC.pl handles logging in. The perl module passes the server an unauthenticated URL. The server tells the perl module, I’m not going directly to the URL you specified, instead I’m redirecting you to another URL, and here’s an HTML document at the URL. The URL is the URL browsers normally go to to login. To the perl module, the redirection shows up as a callback. The perl module posts the credential to the login URL, and the login completes.
sub redirect_ok { my($self, $prospective_request, $response) = @_; ... # Intercept the redirect to the log on form and POST the user credentials. # The RTC form based login mechanism and the use of x-com-ibm-team-repository-web-auth-msg # is documented at https://jazz.net/wiki/bin/view/Main/JazzFormBasedAuth if(defined($response->header('x-com-ibm-team-repository-web-auth-msg')) and $response->header('x-com-ibm-team-repository-web-auth-msg') eq 'authrequired' ) { my($user, $password) = $self->get_basic_credentials(undef, $prospective_request->uri, undef); ... # POST the user's credentials to the j_security_check URI. my $req = HTTP::Request::Common::POST($j_security_check, [ 'j_username' => $user, 'j_password' => $password, ]); my $res = $self->simple_request($req); if($res->code == 302) { if($res->header('location') =~ m,/authfailed$,) { # Change the response to unauthorized. $response->code(401); $response->message("Log in failed."); .... return 0; } else { return 1; } .... } } .... }
Discovery Documents
Once the user is logged in the next call is to sub oslcWhere
which makes a series of calls to discover what URL to use to run a workitem query. The article Consuming Rational Team Concert’s OSLC Change Management V2 Services goes over OSLC 2.0 discovery in detail.
All discovery starts with the rootservcies document. In this case https://localhost::9443/jazz/rootservices. The perl module stores the rootservices document returned from the server in a hash.
sub serviceProviders { ... my $rootservices = $self->rootservices(); my $projectAreas = $rootservices->{'oslc_cm:cmServiceProviders'}->{'rdf:resource'}; ... $self->GET($projectAreas, {'Accept' => 'application/xml'}); $self->{'serviceProviders'} = XMLin($self->responseContent(), 'ForceArray' => ['oslc:serviceProvider'], ); $self->{'serviceProviders'}; }
Below is a portion of the rootservices document
... <oslc_cm:cmServiceProviders xmlns:oslc_cm="http://open-services.net/xmlns/cm/1.0/" rdf:resource="https://localhost:9443/jazz/oslc/workitems/catalog" > ... />
The code in sub serviceProviders
searches for the URL https://localhost:9443/jazz/oslc/workitems/catalog. in the rootservices, and then gets the document.
Service Provider Document
Below is the service provider document for oslc_cm:cmServicesProviders. The document that is retrieved from https://localhost:9443/jazz/oslc/workitems/catalog.
<oslc:ServiceProvider rdf:about="https://localhost:9443/jazz/oslc/contexts/_R1JdsB4LEeKIfKbjXxQkLg/workitems/services.xml"> <dcterms:title rdf:parseType="Literal">MyProject</dcterms:title> <oslc:details rdf:resource="https://localhost:9443/jazz/process/project-areas/_R1JdsB4LEeKIfKbjXxQkLg"/> <jfs_proc:consumerRegistry rdf:resource="https://localhost:9443/jazz/process/project-areas/_R1JdsB4LEeKIfKbjXxQkLg/links"/> </oslc:ServiceProvider>
The XML for the service providers includes the URL https://localhost:9443/jazz/process/project-areas/_R1JdsB4LEeKIfKbjXxQkLg. The code below in in sub services
finds this URL and gets the QueryCapabilities, the URL for running a query against the project area “MyProject.”
sub services { ... my $servicesURL = $self->servicesURL($projectArea); $self->GET($servicesURL, {'Accept' => 'application/rdf+xml'}); .... $self->{'services'}->{$projectArea} = XMLin($self->responseContent(), 'ForceArray' => ['oslc:queryCapability'], ); $self->{'services'}->{$projectArea}; }
The document returned from sub services
includes the fragement below – the QueryCapability. The URL that the API needs is in the QueryCapability; it’s https://localhost:9443/jazz/oslc/contexts/_R1JdsB4LEeKIfKbjXxQkLg/workitems Now we can use this URL to find workitems.
... <oslc:QueryCapability> <dcterms:title rdf:parseType="Literal">Change request queries</dcterms:title> <oslc:usage rdf:resource="http://open-services.net/ns/core#default"/> <oslc:resourceType rdf:resource="http://open-services.net/ns/cm#ChangeRequest"/> <oslc:resourceShape rdf:resource="https://localhost:9443/jazz/oslc/context/_R1JdsB4LEeKIfKbjXxQkLg/shapes/workitems/query"/> <oslc:queryBase rdf:resource="https://localhost:9443/jazz/oslc/contexts/_R1JdsB4LEeKIfKbjXxQkLg/workitems"/> </oslc:QueryCapability> ...
The OSLC parameters, dcterms:identifier=”99″ -prop “dcterms:creator”, in the example above are converted to OSLC format, and appended to the base URL. The URL that’s sent to he server is https://localhost:9443/jazz/oslc/contexts/_R1JdsB4LEeKIfKbjXxQkLg/workitems?oslc.select=dcterms=identifier,dcterms=creator&oslc.where=dcterms=identifier=99 The query results are then returned in the form of a RDF document. The code in sub oslcWhere
drives the process.
sub oslcWhere { my($self, $projectArea, $queryCapability, $where, $propertiesAR) = @_; my $queryCapabilityURL = $self->queryCapabilityURL($projectArea, $queryCapability); my $uri = URI->new($queryCapabilityURL); my %form; $form{'oslc.where'} = $where; # Always select the identifier. my @props = ('dcterms:identifier'); if(defined($propertiesAR) && @{$propertiesAR}) { push(@props, @{$propertiesAR}); } $form{'oslc.select'} = join(',', @props); $uri->query_form(%form); ... my $data = $self->getAllPages($uri, undef, {'DontFollowMembers' => $DontFollowMembers}); ... $data; }
Example 2
Let’s go through on more example.
rtcclient -user me -password pw -baseuri https://rtc.mydomain.com/rtc \ -workitem 99 -update 'dcterms:description->{content}=99 beers on the wall'
In this case the idea is to update the description of workitem 99 with the text 99 beers on the wall. The OSLC document doesn’t include the URL for specifying a workitem, so rather than discover it, the URL is hardcoded. A query is appended to the URL as before.
First, we get the workitem 99 and all of its fields, then build an oslc.properties URL query (i.e. ?oslc.properties) containing only the field that we want to update, in this case dcterms:description.
sub workitemUpdate { my($self, $workItemNumber, $newValuesAR, $verbose) = @_; # Using a hardcoded URI here because this URL doesn't exist in the # discovery mechanism. my $uri = URI->new("$WorkItemURI/$workItemNumber"); my @updated_fields; foreach my $new_valueAR (@$newValuesAR) { my($field, $val) = @$new_valueAR; my $append = ''; if($field =~ s/,(append(_if_not_empty)?)$//) { $append = $1; } my $sub; ($field, $sub) = split(/->/, $field, 2); push(@updated_fields, $field); } if(@updated_fields) { $uri->query_form('oslc.properties' => join(',', @updated_fields)); } $self->GET($uri); my $response = $self->responseContent(); ...
Make sure that when we update the record that it’s the latest version by capturing the Etag field of the GET response, and supplying the ETag when we’re updating the field.
.... my $response = $self->responseContent(); my $ETag = $self->responseHeader('Etag'); my $xml = OLSC::RDF::XML::Simple->new(); my $perl = $xml->XMLin($response, 'KeyAttr' => [], 'ForceArray' => ['rdf:Description', 'rdfs:member'], ); ....
Update the field in memory, and then put the result back with the updated text.
# Find the change request record and make sure the content key exists # for string values. my $cr; foreach my $record (@{$perl->{'rdf:Description'}}) { Lyo::OSLC::RDF::_makeSureContentKeyExists($record); if( exists($record->{'rdf:type'}) and exists($record->{'rdf:type'}->{'rdf:resource'}) and $record->{'rdf:type'}->{'rdf:resource'} eq 'http://open-services.net/ns/cm#ChangeRequest' ) { $cr = $record; } } foreach my $new_valueAR (@$newValuesAR) { my($field, $val) = @$new_valueAR; my $append = ''; if($field =~ s/,(append(_if_not_empty)?)$//) { $append = $1; } my $sub; ($field, $sub) = split(/->/, $field, 2); Lyo::OSLC::RDF::updateUtil($cr->{$field}, $field, $sub, $val, $append); } my $new = $xml->XMLout($perl, 'RootName' => 'rdf:RDF', 'AttrIndent' => 1 ); $uri->query_form('oslc.properties' => join(',', @updated_fields)); $self->PUT($uri, $new, { 'If-Match' => $ETag, 'Content-Type' => 'application/rdf+xml', } ); ... $self; }
Let’s look at the verbose output from the update command. It gives you an idea of how the RDF data is modified. The Data structure before updates. The URL passed into update the document is workitem resource/itemName/com.ibm.team.workitem.WorkItem/2?oslc .properties=dcterms:description. It is a request for an update to update the description field of workitem 2. The RDF version of the XML that’s is POSTed is below. The update is to the content of the dcterms:description.
The workitem is updated. Subsequent queries using the web client will show an updated description field.
Data structure before update $VAR1 = { 'xmlns:oslc_cm' => 'http://open-services.net/ns/cm#', 'xmlns:dcterms' => 'http://purl.org/dc/terms/', 'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdf:Description' => [ { 'rdf:about' => 'https://localhost:9443/jazz/resource/itemName/com.ibm.team .workitem.WorkItem/2?oslc.properties=dcterms%3Adescription', 'rdf:type' => { 'rdf:resource' => 'http://open-services.net/ns/cm#ChangeRequest' }, 'dcterms:description' => { 'rdf:parseType' => 'Literal', 'content' => '100 bottles of beer' } } ] }; Data structure after updates. $VAR1 = { 'xmlns:oslc_cm' => 'http://open-services.net/ns/cm#', 'xmlns:dcterms' => 'http://purl.org/dc/terms/', 'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdf:Description' => [ { 'rdf:about' => 'https://localhost:9443/jazz/resource/itemName/com.ibm.team .workitem.WorkItem/2?oslc.properties=dcterms%3Adescription', 'rdf:type' => { 'rdf:resource' => 'http://open-services.net/ns/cm#ChangeRequest' }, 'dcterms:description' => { 'rdf:parseType' => 'Literal', 'content' => '99 bottles of beer' } } ] };
Issues
RTC 4.0 RDF documents that include embedded HTML, for example, the description field of a workitem, don’t escape the HTML tags. This can cofuse the XML parser. There is a specialized version of GET in CM.pm that escapes some but not all HTML.
For more information
About the author
Glenn Bardwell is a member of the Jazz L3 Dev Team.
Copyright © 2012 IBM Corporation