Sunday, June 4, 2017

ICN Known issues-2

Repository.retrieveItem(itemPath, callback) not working


noticed that the following function is not working as the documentation says in the ICN JavaScript model. Actually it can not be used with a document path.
1
retrieveItem(itemIdOrPath, itemRetrievedCallback, templateID, version, vsId, objectStoreId, action)
Here is the Knowledge center documentation:
Retrieves the specified ecm.model.ContentItem from the repository.
  • itemIdOrPath: A String value holding the item identifier or item path.
  • itemRetrievedCallback: A callback function called after the item has been retrieved. Will pass a ecm.model.ContentItem object as a parameter.
  • templateID: A string value holding the template Id.
  • version: A string holding the version requested; ie. “current” or “released” (optional).
  • vsId: A string value holding the version series identifier (optional, FileNet P8 only).
  • objectStoreId: A string value holding the object store identifier (FileNet P8 only).
  • action: A string value to pass up to the mid-tier specifying the action being performed (optional).
You can pass a folder or document ID, but you can only pass a folder path, not a document path. This is because in the implementation (I just checked), if the itemIdOrPath start with a/, they do a Factory.Folder.fetchInstance, so there is no way to fetch a document by path with this function. I actually looked in all sources and I didn’t find any services fetching a document by path. (I might be wrong on that though)
Now as a reminder: the path of a document might be confusing anyway since it is not using the DocumentTitle name but the Containment Relationship name, which will be unique. That makes sense since the DocumentTitle doesn’t have to be unique within a folder.

Work around

You have two ways to work around that.
  1. Create your own service calling Factory,Document,fetchInstance which can use a path (it will use the Containment name relationship). This is the most efficient solution since it will do only one call to the server but it requires some extra work since you’ll have to implement a new service.
  2. Use the JavaScript method anyway. Here is what I came up with, but it has to call the server twice, so it will be slower. But you don’t have any extra work to do except using this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getItemByPath: function (itemPath, repository) {
    var def = new Deferred();
    var parentPath = itemPath.substring(0, itemPath.lastIndexOf("/"));
    var itemName = itemPath.substring(itemPath.lastIndexOf("/") + 1, itemPath.length);
    repository.retrieveItem(parentPath, function (item) {
        item.retrieveFolderContents(false, function (resultSet) {
            if (!resultSet.items || resultSet.items.length == 0) {
                def.resolve(null);
            } else {
                def.resolve(resultSet.items[0]);
            }
        }, null, null, true, null, "", item, {type: "AND", conditions: [
            {name: "{NAME}", condition: "endWith", value: "a.dita"},
            {name: "{NAME}", condition: "startWith", value: "a.dita"}
        ]});
    }, 'Folder');
    return def.promise;
}
However it will fail and display an error to the user if the parent folder does not exist. You might want to combine this with the getFolderSmart I gave you below:


Working with folders


Add a new folder

I really wanted to write this because I had some hard time figuring out how this works. The documentation is just wrong and I had to debug quite deeply ICN to understand what the problem was. The function to add a folder is:
1
ecm.model.Repository.addFolderItem(parentFolder, objectStore, templateName, criterias, childComponentValues, permissions, securityPolicyId, teamspaceId, callback)
JSDoc, but since it’s wrong let’s comment it:
addFolderItem(parentFolder, objectStore, templateName, criterias, childComponentValues, permissions, securityPolicyId, teamspaceId, callback)
Adds a folder to the repository. Parameters:
  • parentFolder: ecm.model.ContentItem object for the parent folder.
  • objectStore: A object store object holding identifier information (FileNet P8 only).
  • templateName: A string value holding the content class name.
    This is actually the classId and not an entry template ID as you could thing.
  • criterias: An object holding the criteria information.
    This one is all wrong, that’s not the criterias but an object very special actually containing the criterias. If you don’t have it right, the CE will throw an exception. Here is what it should be and I’ll detail after the screenshot.
    addFolderCriterias
    • childComponentValues: you can use an empty array for p8 []
    • criterias, this is where you actually give your criterias as for an add, and not straigt as the documentation let you think. It looks like (This comes from an add document screenshot, you will have to set the FolderName property instead ofDocumentTitle):
      icn_debug_criteria
    • docid: This is the id of the parent, which you also give in the parentFolder parameter. I have no clue why it’s needed twice…
    • documentType: don;t try to understand, you use a addFolder function but you still need to repeat that’s a folder ðŸ™‚
    • folderName: the name of your folder, you have to add it also as criterias
  • childComponentValues: An object holding the child component values (Content Manager only).
    Again, use an [] for p8.
  • permissions: An object holding the permission information.
    You can leave this empty if you are using the default settings. If you want to set property, this is an array of Permissions object, see my post Working with documents.
  • securityPolicyId: A string value holding the security policy id (FileNet P8 only).
  • teamspaceId: A string value holding the teamspace id.
  • callback: A callback function called after the item has been added. Will pass aecm.model.ContentItem object as a parameter.
And here is an example of how to use this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Build this strange object the doc does'nt mention and you can't guess
var folderProperties = {
     childComponentValues: [], // Use [] for p8
     criterias: [{dataType: 'xs:string', name: 'FolderName', value: itemName}], // Your criterias, here I have only the name
     documentType: 'Folder', // The type is always Folder for a folder, even if you are using a child class
     folderName: itemName, // Again, the name of the folder, needs to be here and in the criterias
     docid: parentContentItem.id // the parent folder id
};
repository.addFolderItem(
     parentContentItem, // the parent folder ad a ecm.model.ContentItem
     parentContentItem.objectStore, // the objectstore, you can use the parent to get this
     "Folder", // The class id, use Folder for a classic folder, or the id of your class inherited of Folder for instance
     folderProperties, // This is the strange object you can not guess
     [], // The childComponentValues again, use [] for p8
     null, // the permission, you can use null to use the default permissions
     null, // the security policity id, use null if none
     null, // the teamspace if, use null if none
     lang.hitch(this, function (newItem) {
         // What do you want to do after the add
         // A parentContentItem.refresh(); could be nice for the user 🙂" src="https://s.w.org/images/core/emoji/2/svg/1f642.svg">
     })
);

Test the existence of a folder

This is one is also a bit tricky, because their is no API allowing us to do that. and there is a problem with the following code:
1
2
3
repository.retrieveItem('/MyFolder/Guillaume', function (item) {
    // Here the item exist and you fetched it
};
But the problem with that is, if the item does not exist, the retrieveItem function add an error message to the desktop, which will be displayed ad an error dialog for the user. There is no way to specify an error callback, nor to aspect a return function (believe me I tried but the Request class directly adding the error to the desktop via a private function, that would be really ugly to aspect and we might catch return from other Request), which makes this function pointless for what we want to do.
We have to find a way test the existence and get an error callback if it does not exist. Here is what I’m using:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
doesFolderExist: function (repository, folderPath) {
    var res = new Deferred();
    var requestParams = {
        repositoryId: repository.id,
        objectStoreId: repository.objectStore.id || "",
        docid: folderPath,
        template_name: "Folder",
        version: ""
        };
    Request.invokeService("getContentItems", repository.type, requestParams, lang.hitch(this, function (response) {
        res.resolve(true);
    }), false, false, lang.hitch(this, function () {
        res.resolve(false);
    }));
    return res.promise;
}
That way you can handle the case where the item does not exist.

Fetch a folder without error if the folder does not exist.

Now you can use what’s above to retrieve a folder without failing if the folder does not exist.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
getFolderSmart: function (repository, folderPath, parent) {
     var res = new Deferred();
    var requestParams = {
        repositoryId: repository.id,
        objectStoreId: repository.objectStore.id || "",
        docid: folderPath,
        template_name: "Folder",
        version: ""
    };
    Request.invokeService("getContentItems", repository.type, requestParams, lang.hitch(this, function (response) {
        // Build the ContentItem from the response
        var item = ContentItem.createFromJSON(response.rows[0], repository, null);
        // When you build a ContentItem from JSON, of course it has no idea of its context,
        // Which means he has no parent and the getPath() will return /nameOfTheFolder
        // We have to give it a context by settings the parent
        item.parent = parent;
        res.resolve(item);
    }), false, false, lang.hitch(this, function () {
        res.resolve(null);
    }));
    return res.promise;
}

Fetch the content of a folder

The method is the following:
1
retrieveFolderContents(folderOnly, callback, orderBy, descending, noCache, teamspaceId, filterType, parent, criteria)
However it’s worth documenting it better because the documentation just doesn’t say anything… especially about what we can and can’t do with the criterias, filter and so on.
There is a lot of limitations on this method because it is intended for UI only and not complex search, I’ll talk about them after explaining some simple use cases:
Get only the sub-folders (for a folder tree for instance):
1
2
3
folder.retrieveFolderContents(true, function (resultSet) {
    // You will have your items in resultSet.items, which is an ContentItem[]
}, null, null, null, null, null, folder, null);
Get sub-folders and document (for a content pane for instance):
1
2
3
folder.retrieveFolderContents(false, function (resultSet) {
    // You will have your items in resultSet.items, which is an ContentItem[]
}, null, null, null, null, '', folder, null);
Apparently, from the code, you can not fetch document in the Root directory, which makes sense since it is not allowed in P8 anyway, I could’t tell for other repository types.
Now let’s talk complicated stuff, how to filter this :). That where it starts hurting the brain because there is no documentation, and even debugging the client doesn’t really help since there just don’t use it in the existing ICN, and almost everything is done on the server side.
Let’s take all parameters one by one
  • folderOnly: this one’s easy, true you get only sub-folders, false you get also documents. Just know that if this is true, then the filterType doesn;t matter anymore and will be set to searchFolder anyway
  • callback: what to do with the result, take an ecm.model.ResultSet as parameter
  • orderBy: This one can give you an headache, the only possible values are:
    • !MimeTypeIcon (don’t ask my why the ! ðŸ™‚ ), but good thing is that it will work for Folder and Document
    • !Name: Also work for both
    • Id
    • LastModifier
    • DateLastModified
    • ContainerType
    • All others would simply be ignored
  • descending: true or false, used only if orderBy is used
  • noCache: if set to false or undefined, you will get the same result as the previous request if the filterType is the same. If set to true, it will ask the server any times. Set it to true if you are filtering or you will have the same answer, except if you name your fileType depending of what you filter, so if you ask the same thing you can benefit of the cache system. How smart ?!! ðŸ™‚
  • teamspaceId: can be null
  • filterType: Can be
    • searchFolder which is equivalent to foldersOnly to true (foldersOnly overwrite this value with searchFolder anyway of set to true)
    • searchAndFolderSearch
    • anything else which means “I don’t care”, but can be used to cache result of your filtered request if you reuse the same filterType (anything you want since ICN doesn’t care), if null will be set to ‘folderSearch’ or ” depending of foldersOnly
  • parent: The parent where to look, this is a ContentItem, not an ID
  • criterias: Here things become interesting. Crtierias would be used to build the WHERE clause, but for both the Sub-Folders and the Documents. That’s usually not a problem but just know it because it can be. Criterias is an object with a type member, and an array of conditions item, looking like this.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
        type: "OR", // OR or AND
        conditions: [
            {
                name: "{NAME}", // Can only be {NAME} or {ALL}, which to a OR on all searchable fields
                condition: "contain", // can only be contain, startWith or endWidth, can NOT be =
                value: "myText", // Can be anything
            }
        ]
    }

2 comments: