How to use the BonitaSoft REST API from Python/Django

1
0
-1

How do you use the BonitaSoft REST API from Python/Django?

Comments

Submitted by 13509055 on Tue, 02/04/2014 - 02:59

It's not a question... It's a tutorial... you should write it in the tutorial section or something

Submitted by ttoine on Tue, 02/04/2014 - 12:26

What I would recommand, is to just write a short question about the topic of this howto, and to write the howto itself in an answer. This is the way it should be done also on Stackoverflow, where this howto has been posted first. As Bonita community manager, I really enjoy to see members of the community sharing their knowledge. Thank you very very much for you time, jfmcgrath.

Submitted by jfmcgrath on Tue, 02/04/2014 - 15:51

I've changed it to a Q&A.

Submitted by ttoine on Tue, 02/04/2014 - 15:53

Yeah !!!! thank you very much !

1 answer

1
0
-1

I couldn't find much information online about how to do this, so I decided to post how I did it. Please feel free to share your suggestions or experiences.

First off, in settings.py, I set a few variables that I could reuse. These are the defaults if you are using the Bonita Studio locally.

BPM_HOST = 'http://localhost:9090/bonita-server-rest/'
BPM_USERNAME = 'restuser'
BPM_PASSWORD = 'restbpm'

In views.py, I set up a function that I could use anytime I needed to do a call. It uses the variables from the settings file and accepts parameters for logged in user, url to call and a dictionary of post_data. It sets the Basic auth and content type headers the way Bonitasoft expects.

from django.conf import settings
import urllib
import urllib2
import base64

def restcall(user,url,post_data={}):
    #create bpm_request
   bpm_request = urllib2.Request(settings.BPM_HOST + url)

    #encode username and password and add to header
   authKey = base64.b64encode(settings.BPM_USERNAME + ':' + settings.BPM_PASSWORD)
    bpm_request.add_header("Authorization","Basic " + authKey)

    #add content type to header
   bpm_request.add_header("Content-Type","application/x-www-form-urlencoded")

    #must send current user in options
   current_user = 'user:' + ‘user'
    post_data['
options'] = current_user

    bpm_request.add_data(urllib.urlencode(post_data))

    response = urllib2.urlopen(bpm_request)

    try:
        return response
    except Exception, exception:
        logging.info(str(exception))
    #endtry
#end restcall

Now say you wanted to build a list of all your Process Instances:

import xml.etree.ElementTree as ET
response = restcall(my_user,'API/queryRuntimeAPI/getLightProcessInstances')
root = ET.parse(response).getroot()
UUIDs=[]
for doc in root.findall('LightProcessInstance'):
    UUIDs.append(doc.find('instanceUUID').find('value').text)
#endfor

Or to build a list of Process Instances that would be in your inbox:

response = restcall(my_user,'API/queryRuntimeAPI/getLightParentProcessInstancesWithActiveUser/' + my_user +'?fromIndex=0&pageSize=200')
root = ET.parse(response).getroot()

UUIDs=[]
for doc in root.findall('LightProcessInstance'):
    UUIDs.append(doc.find('instanceUUID').find('value').text)
#endfor

Posting data is a real pain. First off, I created a function to clean any text you might send:

def super_clean(text):
    """This will make data safe to send by rest.
    Escape for <> (so we don't screw up our XML).
    Quote plus to handle + signs.
    Encode for international chars and smart quoates.
    Strip to take out extra blanks before and after"
""
    return urllib.quote_plus(escape(text.encode('utf-8','replace').strip()))
#end super_clean

And here's how I generated the XML map. In case you missed it, the documentation for the XML map is at in their docs.

variable_string = '<map>' + \
                  '<entry><string>billing_department</string><string>' + super_clean(form.cleaned_data['billing_department']) + '</string></entry>' + \
                  '<entry><string>amount</string><double>' + super_clean(str(form.cleaned_data['amount'])) + '</double></entry>' + \
                  '<entry><string>account</string><string>' + super_clean(str(form.cleaned_data['number_attended'])) + '</long></entry>' + \

variable_string += '<entry><string>participant_name</string><list>'
for tempform in participant_formset.forms:
    if 'participant_name' in tempform.cleaned_data:
        variable_string += '<string>' + super_clean(tempform.cleaned_data['participant_name']) + '</string>'
    #endif
#endfor                  
variable_string += '</list></entry>'

post_data = {}
post_data['variables'] = variable_string

And you update an existing record with:

process_instance_uuid = form.cleaned_data['uuid']
response = restcall(request,'API/runtimeAPI/setProcessInstanceVariables/' + process_instance_uuid,post_data)

Part 2 - Adding Attachments

Attaching a file requires a call to /API/runtimeAPI/addAttachmentOctetStream/{instanceUUID}?name=�&fileName=�

This REST call is a little different than a regular Bonita Rest call, so I wrote the following function to help:

from django.conf import settings
import urllib
import urllib2
import base64

def restAttachment(user,url,file):    
    #create bpm_request
   bpm_request = urllib2.Request(settings.BPM_HOST + url,data=file.read())

    #encode username and password and add to header
   authKey = base64.b64encode(settings.BPM_USERNAME + ':' + settings.BPM_PASSWORD)
    bpm_request.add_header("Authorization","Basic " + authKey)

    #must send current user in options
   current_user = 'user:' + 'user'

    #add headers
   bpm_request.add_header("Content-Type","application/octet-stream")
    bpm_request.add_header('Content-Length', str(file.size))
    bpm_request.add_header('Cache-Control', 'no-cache')
    bpm_request.add_header('options', current_user)
    bpm_request.add_header("Content-Disposition","attachment; filename="+urllib.quote(file.name))

    try:
        response = urllib2.urlopen(bpm_request)
    except Exception, exception:
        logging.info(str(exception))
    #endtry

    return response
#end restAttachment

In forms.py, you need a form like this one:

class AddAttachmentForm(Form):
    process_instance_uuid = CharField(widget=HiddenInput())
    attachment = FileField()
#end AddAttachmentForm

In your template, you need to have the following (note the enctype):

<form method="post" enctype="multipart/form-data" action="add_attachment"> {% csrf_token %}
    {{ add_attachment_form }}
    <input type="submit" value="Upload Attachment" />
</form>

In views.py, when building the above template, you should have something like:

add_attachment_form = AddAttachmentForm(initial={'process_instance_uuid':my_uuid,})

When processing the form, you should have:

if 'attachment' not in request.FILES:
    return render_to_response(...

form = AddAttachmentForm(request.POST, request.FILES) #note the request.FILES
response = restAttachment(user,'API/runtimeAPI/addAttachmentOctetStream/' + form.cleaned_data['process_instance_uuid'] + '?name=bonita_attachment_type_field_name) + '&fileName=' + urllib.quote(request.FILES['attachment'].name),request.FILES['attachment'])

Part 3 - Using the Search Bonitasoft’s search function via REST Call

As far as I can tell, there is little-to-no documentation on this.

Let’s start with the basics. To do a search, you have to post to API/queryRuntimeAPI/searchByMaxResult?firstResult=�&maxResults=�. If you post to API/queryRuntimeAPI/search, you only get the number of records, not the records themselves.

You can use firstResult and maxResults for paging.

Your body should look like:

options=user:my_user&query=<query_xml>

The hard part is figuring out what goes in the query_xml. The examples I found on the Bonitasoft site were full of br tags. For some reason, the Bonitasoft site stripped out their xml tags. The query xml can be created using the Java API. One way is to use the Groovy Editor in the Bonita Studio and run it with the Evaluate button. In the following example, “first_name” is one of the data fields I created.

import org.ow2.bonita.search.SearchQueryBuilder;
import org.ow2.bonita.search.index.ProcessInstanceIndex;
import org.ow2.bonita.search.index.ActivityInstanceIndex;

SearchQueryBuilder query = new SearchQueryBuilder(new ProcessInstanceIndex());
query.criterion(ProcessInstanceIndex.VARIABLE_NAME).equalsTo("first_name").and().criterion(ProcessInstanceIndex.VARIABLE_VALUE).equalsTo('Archibald');
return query;

You would think that this would return any record where the field “first_name” is set to “Archibald”. In fact it returns any record that includes the field “first_name” (which should be all of them) and has any user defined field that has a value of “Archibald”. So if someone’s last name is “Archibald”, they would be returned as well. There doesn’t appear to be a way around this. It’s probably best just to embrace the fact that it’s a full search and simplify it to:

query.criterion(ProcessInstanceIndex.VARIABLE_VALUE).equalsTo('Archibald');

Which generates the following XML:

<SearchQueryBuilder>
  <index class="org.ow2.bonita.search.index.ProcessInstanceIndex"/>
  <query>
    <org.ow2.bonita.search.Criterion>
      <builder reference="../../.."/>
      <fieldName>variable_value</fieldName>
      <value>Archibald</value>
    </org.ow2.bonita.search.Criterion>
  </query>
</SearchQueryBuilder>

Again, this only searches user defined fields. If you want to search other fields, seehttp://documentation.bonitasoft.com/javadoc/bpm_engine/5.9/org/ow2/bonita/search/index/ProcessInstanceIndex.html. I also wanted to search the creator, so my XML became:

<SearchQueryBuilder>
<index class="org.ow2.bonita.search.index.ProcessInstanceIndex"/>
        <query>
          <org.ow2.bonita.search.Criterion>
            <builder reference="../../.."/>
            <fieldName>variable_value</fieldName>
            <value>Archibald</value>
          </org.ow2.bonita.search.Criterion>
          <string> OR </string>
          <org.ow2.bonita.search.Criterion>
            <builder reference="../../.."/>
            <fieldName>startedBy</fieldName>
            <value>Archibald</value>
          </org.ow2.bonita.search.Criterion>
        </query>
</SearchQueryBuilder>

Lastly, I wanted to limit my search to a date range. The format for dates is yyyymmddhhmmssttt. I want to search for a date range, so I’m setting the start time to 0 and the end time to all 9s. Logic dictates that I add parenthesis around the OR clause.

<SearchQueryBuilder>
<index class="org.ow2.bonita.search.index.ProcessInstanceIndex"/>
<query>
        <string>(</string>
        <org.ow2.bonita.search.Criterion>
            <builder reference="../../.."/>
            <fieldName>variable_value</fieldName>
            <value>Archibald</value>
        </org.ow2.bonita.search.Criterion>
        <string> OR </string>
        <org.ow2.bonita.search.Criterion>
            <builder reference="../../.."/>
            <fieldName>startedBy</fieldName>
            <value>Archibald</value>
        </org.ow2.bonita.search.Criterion>
        <string>)</string>
            <string> AND </string>
        <org.ow2.bonita.search.Criterion>
            <builder reference="../../.."/>
            <fieldName>startedDate</fieldName>
            <value>[20130101000000000 TO 20131231999999999]</value>
        </org.ow2.bonita.search.Criterion>
</query>
</SearchQueryBuilder>

So now that I have my XML, I just need to plug it into Python. I created a form:

class SearchForm(Form):
    search_text = CharField(widget=TextInput(attrs={'size':'80'}))
    start_date = DateField(widget=widgets.TextInput(attrs={"class":"calendar"}))
    end_date = DateField(widget=widgets.TextInput(attrs={"class":"calendar"}))

I didn't want to mess with paging, so if there are more than 100 records, I give them a message to narrow their criteria. Notice that in my xml I put quotes around my search text. That makes the search match the whole text. If there is a space in the text and you don't use quotes, the search doesn't try to match all the words. Here's how I process the form and execute the search:

if request.method == 'POST':
    search_form = SearchForm(request.POST)

    if not search_form.is_valid():
        return render_to_response(...

    #format dates in manner expected by API
   start = search_form.cleaned_data['start_date'].strftime('%Y%m%d') + "000000000"
    end = search_form.cleaned_data['end_date'].strftime('%Y%m%d') + "999999999"

    post_data={}
    #the search query must be in this xml format
   post_data['query'] = '''
    <SearchQueryBuilder>
      <index class="org.ow2.bonita.search.index.ProcessInstanceIndex"/>
      <query>
        <string>(</string>
        <org.ow2.bonita.search.Criterion>
          <builder reference="../../.."/>
          <fieldName>variable_value</fieldName>
          <value>"%s"</value>
        </org.ow2.bonita.search.Criterion>
        <string> OR </string>
        <org.ow2.bonita.search.Criterion>
          <builder reference="../../.."/>
          <fieldName>startedBy</fieldName>
          <value>"%s"</value>
        </org.ow2.bonita.search.Criterion>
        <string>)</string>
        <string> AND </string>
        <org.ow2.bonita.search.Criterion>
          <builder reference="../../.."/>
          <fieldName>startedDate</fieldName>
          <value>[%s TO %s]</value>
        </org.ow2.bonita.search.Criterion>
      </query>
    </SearchQueryBuilder>'
'' \
    % (super_clean(search_form.cleaned_data['search_text']),
       super_clean(search_form.cleaned_data['search_text']),
       start,
       end)

    #get number of records
   response = restcall(request,'API/queryRuntimeAPI/search',post_data)
    number_of_records = response.read()
    if is_int(number_of_records):
        number_of_records = int(number_of_records)
    else:
        return render_to_response(...

    if number_of_records > 100:
        return render_to_response(...

    if number_of_records == 0:
        return render_to_response(...

    #now get the records
   response = restcall(request,'API/queryRuntimeAPI/searchByMaxResult?firstResult=0&maxResults=100',post_data)
    root = ET.parse(response).getroot()

    #loop through requests
   for doc in root.findall('LightProcessInstance'):  
        ...

Notifications