Sending service actions with PostFileWithRequest

The PostFileWithRequest operates differently to the normal Post request in that instead of sending a simple JSON object it sends a multi-part form request with the object (e.g. a Record) serialized not to JSON but as an HTML form.  This means that some complex constructs, such as the Record.AccessControlList, do not translate well.

 

Service Actions, sent in ActionsToCall, nearly work except for the code that specifies the type of Service Action.  The good news is that this can be tweaked on the client side by overriding the code to emit the type name.  In the sample below the line JsConfig.TypeWriter... called after TrimClient is instantiated, should allow any Service Action to be de-serialized by the ServiceAPI.

TrimClient trimClient = new TrimClient("http://MyServer/ServiceAPI");
trimClient.AlwaysSendBasicAuthHeader = true;
trimClient.UserName = "USERNAME";
trimClient.Password = "PASSWORD";

JsConfig.TypeWriter = (tt) => {
    return string.Format("{0}.{1}", tt.Namespace, tt.Name);
};
            
Record rec = new Record();
rec.RecordType = new RecordTypeRef() { FindBy = "Document" };
rec.Title = "test";
rec.ActionsToCall = new List<IMainObjectActionRequest>();

rec.ActionsToCall.Add(new AttachContact()
{
    AsContactType = ContactType.Addressee,
    ContactLocation = new LocationRef() { FindBy = "Me" }
});

rec.ActionsToCall.Add(new AttachContact()
{
    AsContactType = ContactType.Other,
    ContactLocation = new LocationRef() { Uri = 9000000072 }
});

FileInfo fi = new FileInfo("c:\\junk\\Test.txt");

var response = trimClient.PostFileWithRequest<RecordsResponse>(fi, rec);

Attaching contacts to a Record using the ServiceAPI

A further update

Was not thinking as clearly as I could have been last time I posted.  The best way to get the contacts is via the ChildLocations property as this will tell you the relationship type, for example:

Records request = new Records();
request.q = "REC_349";
request.Properties = new PropertyList("ChildLocations");

RecordsResponse response = trimClient.Get<RecordsResponse>(request);

foreach (var rec in response.Results)
{
    foreach (var childLoc in rec.ChildLocations)
    {
        Console.WriteLine("{0} - {1}", childLoc.Name, childLoc.TypeOfContact);
    }
}

Update

As noted by Michelle below you cannot add a contact that already exists.  The way to find who is already a contact is to do a Location search, for example:

Locations request = new Locations();
request.q = "contactOf:REC_1";
LocationsResponse response = trimClient.Get<LocationsResponse>(request);
foreach (Location loc in response.Results)
{
    Console.WriteLine(loc.Uri);
}

 

The problem

Contacts have a slightly more complex relationship to Records than is ideal, for example they have a one to many relationship but are not represented by a child list.  You can set the 'Other' contact using Record.OtherContact but then how do you more contacts?

When using the ServiceAPI look in these four places when trying to get or set data:

  1. the stock properties (e.g. Record.Title, Record.OtherContact),
  2. child lists (e.g. Record.ChildHolds)
  3. custom properties (e.g. Record.Html - check the documentation for which of these are read only), and
  4. service actions.

Service Action to set multiple contacts

Service actions are only for writing, not reading.  Reading is done via properties.  You may call the same service action multiple times in a single request and the ServiceAPI help contains a page to demonstrate the JSON for every action.  Below is the C# code to add multiple contacts to a Record.

Record rec = new Record();
rec.Uri = 9000000001;
rec.ActionsToCall = new List<IMainObjectActionRequest>();

rec.ActionsToCall.Add( new AttachContact()
{
    AsContactType = ContactType.Addressee,
    ContactLocation = new LocationRef() { FindBy = "Me" }
});

rec.ActionsToCall.Add(new AttachContact()
{
    AsContactType = ContactType.Other,
    ContactLocation = new LocationRef() { Uri = 9000000072 }
});

trimClient.Post<RecordsResponse>(rec);

ServiceAPI - page through the entire result set

There are a few parameters that assist you in paging through an entire pageSet.  If you have a good connection to the workgroup server (e.g. server to server) then make the pageSize larger rather than smaller.  The response property HasMoreItems will return false when you are finished.  The sample below assumes basic authentication, check out the ServiceAPI documentation for other authentication methods.

Sample 

TrimClient trimClient = new TrimClient("http://localhost/ServiceAPI");
trimClient.AlwaysSendBasicAuthHeader = true;
trimClient.UserName = "MY_USER_NAME";
trimClient.Password = "MY_PASSWORD";

int pageSize = 50;
bool hasMore = true;

Records request = new Records()
{
    q = "all",
    Properties = new PropertyList(PropertyIds.RecordTitle),
    pageSize = pageSize,
    start = 1
};

while (hasMore)
{                          
    RecordsResponse response = trimClient.Get<RecordsResponse>(request);

    hasMore = response.HasMoreItems;
    request.start = request.start += pageSize;

    foreach (Record record in response.Results)
    {
        Console.WriteLine(record.Title);
    }
}

Create a new Record version in the ServiceAPI

There is a sample for creating a new version of a Record in the ServiceAPI help, found here: /examples/RecordNewVersion in your ServiceAPI help. This sample uses a standard form post. The technique using the .Net wrapper classes is very similar.  The process is:

  1. identify the Record for which you want a new version,
  2. create a Record object using the original Record Uri,
  3. set the NewType property,
  4. update any properties you want to be different on the new version (for example: set the Notes to empty, then
  5. post the Record object as per usual.

Example

Record record = new Record();
record.Uri = 9000008003;
record.NewType = NewType.Version;
// this tells the request I want the Record Number property in the response.
record.Properties = new List<string>() { "RecordNumber" };

RecordsResponse response = trimClient.Post<RecordsResponse>(record);

foreach (Record responseRecord in response.Results)
{
     Console.WriteLine(responseRecord.Number);
}

Rationale

The technique of posting the original Record with the NewType flag allows us to create the new version and set properties (and also run ServiceActions) all in one POST from any of JSON/AJAX, HTTP form post, or the .Net wrapper.

ServiceAPI - Attaching keywords

Overview

Keywords (otherwise known as Thesaurus terms) use the child collection pattern to establish a relationship with a record, this is similar to other related objects such as holds and locations.  One difference with keywords is that you cannot use the standard pattern of adding them to the child collection.

A fix in an upcoming release

Unfortunately existing releases (up to and including 8.2) do not throw an exception to tell you that the adding of the keyword to the ChildKeywords collection has not worked, they simply fail.  You should expect that an upcoming release will actually throw an exception and give you a clue how you proceed.

So, how should you proceed?

In 8.2 a keyword is added using the AttachKeyword service action, using the .Net client classes you would do something like this:

TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI82");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

Record request = new Record();
request.Uri = 9000000015;

AttachKeyword attachKeyword = new HP.HPTRIM.ServiceModel.AttachKeyword() { 
    KeywordToAttach = new KeywordRef() { Uri = 9000000005 } 
};

request.AddAction(attachKeyword);
            
try
{
    var response = trimClient.Post<RecordsResponse>(request);               

}
catch (WebServiceException wex)
{
    Console.WriteLine(wex.ErrorMessage ?? wex.Message);
}

The code above posts a JSON object similar to this:

{
    "Uri": 9000000015,
    "ActionsToCall": [
        {
            "__type": "HP.HPTRIM.ServiceModel.AttachKeyword",
            "AttachKeywordKeywordToAttach": 9000000005
        }
    ]
}

What about earlier versions of RM?

Given that the AttachKeyword action was added in 8.2 options are limited for earlier versions, in 8.1 you may choose to write your own plugin, other than that you might have to avoid keywords.

Record creation with ACL in the ServiceAPI

I had a question the other day about creating a new record and setting the ACL at the same time.  This led me to realise that the only sample for this in the ServiceAPI documentation was quite complex (and only for record update not creation).  In an attempt to remedy this I have added a new sample.  To use this copy the file into your  ServiceAPI 'examples' folder and then open the URL: http://<myServer>/HPRMServiceAPI/examples/CreateWithACL_JSON.

Code

The actual code simply composes a JSON object to represent the new record and includes an AccessControlList property containing a valid ACL object.  This object is documented in the ServiceAPI documentation.  The JSON that is posted looks like this:

var acl = {
    "FunctionEnum": "RecordAccess",
    "FunctionProfiles": {
        "DestroyRecord": {
            "Setting": "Private",
            "ReferenceStyle": "NoRefNoCopy",
            "AccessLocations": [
                {
                    "FindBy": "me"
                }
            ]
        }
    }
}             

var formData = {
    'RecordTitle': $("#recordForm :input[name=RecordTitle]").val(),
    'RecordRecordType': $("#recordForm :input[name=RecordRecordType]").val(),
    'AccessControlList': acl                                        
}

The jquery to post the JSON looks like this:

$.ajax({
    url: action,
    type: 'POST',
    dataType: 'json',
    contentType: 'application/json',
    data: JSON.stringify(formData)
})

Using the .Net libraries

Creation of a record while setting the ACL is also available via the .Net client libraries.  The code below constructs an TrimAccessControlList object for the purpose of setting DestroyRecord to private.

TrimClient trimClient = new TrimClient("http://localhost/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

TrimAccessControlList acl = new TrimAccessControlList();
acl.FunctionProfiles = new FunctionProfilesDictionary();
acl.FunctionProfiles.Add("DestroyRecord",
    new TrimAccessControlFunction()
    {
        Setting = AccessControlSettings.Private,
        ReferenceStyle = AccessReferenceStyle.NoRefNoCopy,
        AccessLocations = new List<LocationRef>() { 
            new LocationRef() { Uri = 1 }
        }
    });

Record request = new Record()
{
    Title = "from client with ACL",
    RecordType = new RecordTypeRef() { Uri = 2 },
    AccessControlList = acl
};

RecordsResponse response = trimClient.ServiceClient.Post<RecordsResponse>(request);

ServiceAPI - Attach an action

Background

Today's question of the day from the forums is how to attach an action from the ServiceAPI.  Took me a few minutes to work out exactly what was going on here as the ServiceAPI inherits a little bit of the opacity of the SDK.

The Code

TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

Record request = new Record();
request.Uri = 9000000012;

request.ActionsToCall = new List<IMainObjectActionRequest>() { 
    new AttachAction() { 
        ActionToAttach = new ActionDefRef() { Uri = 9000000008 }, 
        NewAssignee = new LocationRef() { Uri=1 }
    }
};

try
{
    var response = trimClient.Post<RecordsResponse>(request);
    Console.WriteLine(response.Results[0].Uri);
}
catch (WebServiceException wex)
{
    Console.WriteLine(wex.ErrorMessage ?? wex.Message);
}

Some comments

RecordActionUri

There is a property called RecordActionUri.  This is not the Uri of the action to attach but a Record Action before (or after) you which this action should start.  The help from the SDK reads: 'Insert a new action into the list of actions, having it start before or after the nominated attached record action.'

ActionToAttach

When specifying the ActionToAttach you must specify the Uri of the action, it will not be found by name.

How to create a communication

Background

A common activity around a record is to send and receive communications in relation to it, be they physical letters or emails.  If you want an audit trail of communications sent and received you may be interested in the communication object.

Create a communication

The code below uses the ServiceAPI .Net client library to create a new communication. Some points to note are:

  • a communication must be related to a record, the Record property controls this.
  • a communication must have a sender and recipient and these must be HPRM location objects, if you do not have a location for the recipient you must create one.
  • the ChildDetails is a child list as described in this post.
            TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI");
            trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;
            
            Communication communication = new Communication();
            communication.Record = new RecordRef() { Uri = 9000000018 };
            communication.Direction = CommunicationDirection.Outgoing;     

            communication.ChildDetails = new List<CommunicationDetail>() { 
                new CommunicationDetail() { AddressType = SnapAddressType.Email, 
                    Direction = CommunicatorType.Sender,  
                    Location = new LocationRef() { Uri = 1 }
                },
                new CommunicationDetail() { 
                    AddressType = SnapAddressType.Email, 
                    Direction = CommunicatorType.Recipient, 
                    Location = new LocationRef() { Uri = 9000000001}
                }
            };

            try
            {
                trimClient.Post<CommunicationsResponse>(communication);
            }
            catch (WebServiceException ex)
            {
                Console.WriteLine(ex.ErrorMessage);
            }

JSON

The JSON object posted by the code above looks like this.

{
    "ChildDetails": [
        {
            "TrimType": "CommunicationDetail",
            "CommunicationDetailAddressType": {
                "Value": "Email"
            },
            "CommunicationDetailDirection": {
                "Value": "Sender"
            },
            "CommunicationDetailLocation": {
                "TrimType": "Location",
                "Uri": 1
            },
            "Delete": false,
            "Uri": 0
        },
        {
            "TrimType": "CommunicationDetail",
            "CommunicationDetailAddressType": {
                "Value": "Email"
            },
            "CommunicationDetailDirection": {
                "Value": "Recipient"
            },
            "CommunicationDetailLocation": {
                "TrimType": "Location",
                "Uri": 9000000001
            },
            "Delete": false,
            "Uri": 0
        }
    ],
    "TrimType": "Communication",
    "CommunicationDirection": {
        "Value": "Outgoing"
    },
    "CommunicationRecord": {
        "TrimType": "Record",
        "Uri": 9000000018
    },
    "Uri": 0
}

If you are hand-crafting the JSON you may wish to omit all of the unnecesary object notation and send simple values for objects and enums, like this:

{
    "ChildDetails": [
        {
            "CommunicationDetailAddressType": "Email",
            "CommunicationDetailDirection": "Sender",
            "CommunicationDetailLocation": 9000000001
        },
        {
            "CommunicationDetailAddressType": "Email",
            "CommunicationDetailDirection":"Recipient",
            "CommunicationDetailLocation": 1
        }
    ],
    "CommunicationDirection": "Outgoing",
    "CommunicationRecord": 9000000016,
}

The above JSON must be posted to the communication endpoint (e.g. http://MyServer/HPRMServiceAPI/Communication

Searching

Once you have created some communication objects you can of course find records based on their communications.  For example, all records with:

  • communications to or from me: communication:[detail:[location:me]]
  • communications from me: communication:[detail:[location:me and type:sender]]
  • communications to me: communication:[detail:[location:me and type:recipient]]
  • at least one communicationcommunication:[all]
  • communications in a date range: communication:[dated:1/09/2015 to 1/09/2015]

Today's question - creating record relationships from the ServiceAPI

Background

In HPRM records may have a variety of different relationships, from 'Redaction of' to 'Related to'.  These relationships are managed in both the SDK and ServiceAPI via the child collection construct.  To add a relationship you add a new item to the ChildRelationships collection, to remove a relationship you remove an item from this collection.

C#

This code will create a new relationship using the .Net client for the ServiceAPI.

TrimClient trimClient = new TrimClient("http://MyHost]/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

Record record = new Record() { Uri = 9000000001 };
record.ChildRelationships = new List<RecordRelationship>() { 
    new RecordRelationship() { 
        RelatedRecord = new RecordRef() { Uri = 9000000003}, 
        RelationType = RecordRelationshipType.IsRelatedTo
    }};

trimClient.Post<RecordsResponse>(record);

Javascript

Assuming you are using jQuery this code will create a record relationship from Javascript, the complete code can be found here.

$().ready(function () {
    $('#recordRelationship').submit(function () {
        var data = { "Uri": $("input[name=Uri]").val() }

        data["ChildRelationships"] = [{
            "RecordRelationshipRelatedRecord": $("input[name=RelatedUri]").val(),
            "RecordRelationshipRelationType": "IsRelatedTo"                    
        }];

        $.ajax({
            url: "Record",
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(data)

        })
        .done(function (response, statusText) {
            alert("success");
        })
        .fail(function (xhr) {
            var err = eval("(" + xhr.responseText + ")");
            alert(err.ResponseStatus.Message);
        });

        return false;
    });
});

Simple document download

Background

In a previous post I showed how to patch the ServiceAPI TrimClient to fix a problem with downloading child documents (e.g. record revisions).  A customer (Mark from WA) pointed out a way to download a file to a stream without having to write it to a file.

Examples

The following code downloads a document using a stream.  I then save to a file but it could also be passed on as a stream or byte array to another system.

Stream stream = trimClient.Get<Stream>(string.Format("Record/{0}/File/Document", 1843));
using (FileStream fs = File.Create("d:\\junk\\myfile.xml"))
{
    stream.CopyTo(fs);
}

Below is the code to use to get a record revision as a stream.

Stream stream = trimClient.Get<Stream>(string.Format("Record/{0}/RecordRevision/{1}", 1843, 12));

Side benefits

Two side benefits of this approach are:

  1. not having to write a file a file unnecessarily, and
  2. avoiding the non-thread safe approach in the previous post.

Deleting Records in the ServiceAPI from .Net

Background

Unfortunately deleting a Record (or other object type) is slightly different to other ServiceAPI requests.  This is both because most requests assume a response and also because the delete service is not explicitly built into the .Net client.

Two ways to delete

Each HPRM object has a Delete service which can be posted to via a URL following this pattern: /Record/{Id}/Delete.  Given that the .Net client classes do not have a facility to post to a specified URL you would need to write your own code to post to this URL.

The second way to delete is to post the deletion request to one of the built-in service endpoints.  This can be done like this:

trimClient.Post((new DeleteRecord() { Id = 1837.ToString(), DeleteContents = true });

Or this:

trimClient.Post(new DeleteMainObject() { TrimType = BaseObjectTypes.Record, Id = "1838" });

The catch

Not every version of the ServiceAPI enables the built-in service endpoints by default.  They can be switched on by adding PreDefinedRoutes to the serviceFeatures in the hptrim.config file.

<hptrim 
        poolSize="1000"
        serviceFeatures="Json,PreDefinedRoutes,Razor,Html" 
        ...
>