Fetch a custom property for all objects or rows

This post explains how to fetch a custom property you’ve added on a class for all documents retrieved by ICN, before all attributes are actually retrieved using the ContentItem.retrieveAttributes function. That means all items when displayed as row in the Content List view will have this property (also named attribute in ICN) already fetched. There are use cases for this, two of them being:

  • Using a custom attribute in the isVisible or isEnable function of an action, to decide if the action should be displayed based on business logic. I wrote about this here
  • Using a custom property to display a new icon along with the lock/compound/readon-only/… icons. I wrote about this here

After spending some time investigating how ICN works to know what custom properties to fetch when retrieving the content of a folder (openFolder action), I came to the conclusion it goes through two steps:

  • First, it fetches some system-owned hard coded properties, which I think are LockToken, LockTimeout, DateLastModified, LockOwner, Id, DocumentTitle, ClassDescription, Creator, DateCreated, MimeType, LastModifier, IsReserved, Reservation, IsCurrentVersion, VersionSeries, MajorVersionNumber, MinorVersionNumber, ContentSize, ReplicationGroup, VersionStatus, IsVersioningEnabled, ReservationType, Owner, CompoundDocumentState
  • Then, it looks for custom properties configured for the repository in the ICN admin desktop and fetches them as well, but they will be displayed in columns, which we don’t necessarily need.

And that’s it. It does not look for any other properties from some ICN settings we could configure in the admin desktop, although that’s something it could be worth asking to the ICN team. So from this investigation, I thought to the following ways to fetch custom properties:

Using a hidden column

The idea is simple, we will use the second step of the retrieve process since it’s the only one we can actually do something with. However, we don’t want nor need to display the column. So to do this, we will hide the column every time a folder content is retrieved. This is actually quite easy because the layout is part of the answer, so we just need to remove the extra column. Here is how the response looks like in the _retrieveFolderCompleted method:

Let’s focus on the columns member, which is the layout:

As you can see, the structure is stored in the cells member. If we remove one element of the cells array, it won’t be shown anymore in the Content List. We can then use the following code to remove any column we want, here the property removed is MultiFiledIn (which we will show as an icon in another post):

aspect.before(ContentItem.prototype, "_retrieveFolderCompleted", function (response, filterType, noCache, teamspaceId, parent, callback) {
    // Remove column we don't want to display (we just wanted the value populated in the item cache)
    response.columns.cells[0] = array.filter(response.columns.cells[0], function (elem) {
        return elem.field != "MultiFiledIn";
    });
    return [response, filterType, noCache, teamspaceId, parent, callback];
});

It adds some work on the client side since it needs to filter the structure on each folder content fetch, but it’s quite insignificant and I always prefer adding a little work on each client that a little work for each client on the server, which could end up being a huge amount of extra work.

Using a response filter

Another way to do this is to completely forget about the retrieve process and use a response filter to fetch the properties we want on the server side before returning the response to the client. This is cleaner, but in my opinion slower because it adds one extra step for each request on the server. Plus this extra step involves one round trip to the FileNet server. That’s a lot of work when the first solution is not adding any work to the server at all, since we are using the normal process on the server and the client is doing the filtering.

Here is the code of the filter. Basically we want to retrieve our property for all document in the response. To do this in only one server round trip (to the FileNet server), we will use a retrieve batch, to improve significantly the execution time instead of fetching the property one document at a time.

public class AddPropertiesFilter extends PluginResponseFilter {
    
    private static final PropertyFilter pf = new PropertyFilter();
    static {
        pf.addIncludeProperty(new FilterElement(0, null, null, "MultiFiledIn", null));
    }

    public String[] getFilteredServices() {
        return new String[] { "/p8/openFolder" };
    }

    public void filter(String serverType, PluginServiceCallbacks callbacks,
			HttpServletRequest request, JSONObject jsonResponse) throws Exception {
	    
        String repoId = (String) jsonResponse.get("repositoryId");
	    
        // Get all document in the results and store them with their JSON so we can update them
        List<JSONObject> docs = new ArrayList<JSONObject>();
        JSONArray rows = (JSONArray) jsonResponse.get("rows");
        for (Object row : rows) {
            
            String mimetype = (String) ((JSONObject) row).get("mimetype");
            if (!"folder".equals(mimetype)) {
                docs.add((JSONObject) row);
            }
        }
        
        // If there is docs, fetch them to add the missing property
        if (!docs.isEmpty()) {
            ObjectStore os = callbacks.getP8ObjectStore(repoId);
            Subject s = callbacks.getP8Subject(repoId);
            if (s != null) {
                
                UserContext.get().pushSubject(s);
                try {
                    List<Entry<BatchItemHandle, JSONObject>> entries = new ArrayList<Entry<BatchItemHandle, JSONObject>>();
                    RetrievingBatch rb = RetrievingBatch.createRetrievingBatchInstance(os.get_Domain());
                    for (JSONObject json : docs) {
                        String id = (String) json.get("id");
                        id = id.substring(id.lastIndexOf(",") + 1);
                        Document d = Factory.Document.getInstance(os, ClassNames.DOCUMENT, new Id(id));
                        entries.add(new AbstractMap.SimpleEntry<BatchItemHandle, JSONObject>(rb.add(d, pf), json));
                    }
                    rb.retrieveBatch();
                    
                    // Now we can use the result of the RB to add the property to the JSON object
                    for (Entry<BatchItemHandle, JSONObject> entry : entries) {
                        if (!entry.getKey().hasException() && ((Document) entry.getKey().getObject()).getProperties().isPropertyPresent("MultiFiledIn")) {
                            Boolean value = ((Document) entry.getKey().getObject()).getProperties().getBooleanValue("MultiFiledIn");
                            if (value != null) {
                                entry.getValue().put("MultiFiledIn", value);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    UserContext.get().popSubject();
                }
            }
        }
    }
}

5 thoughts on “Fetch a custom property for all objects or rows

  1. George Bredis

    Thanks a lot for the solution! I had the same requirement and solved it with the response filter, because didn’t know how to do this in the frontend. Client-side solution is preferrable for me. But the problem is that we’re not able to add custom property in the search results by default – only system properties are available there. But our customer is using Searches most of the time, not browsing feature. Do You have any idea on how to add custom property to Search Results without using Response Filters?

    Reply
    1. George Bredis

      Maybe I was unclear. The requirement is to ALWAYS include one custom property in all searches and browse feature.
      My old solution was a response filter, adding this property, but it causes an additional roundtrip to retrieve this custom property value, which was bad in terms of performance.
      Your post forced me to think and now I have a new solution.
      Custom property is added to the browse feature in the repository settings – quite easy.
      For saving Search Templates there’s a request filter, cheking the resultsDisplay.columns and adding custom property in case of absence. So, all new saved search templates have my custom property. For the rest situations there’s still my old response filter which checks the result columns and adds one in case of absence (yes, with the extra property retrieval).
      By the way, I have another problem, related to searches…
      Let’s say, there’re some user settings, depending on which additional criterias have to be added to every search. This could be solved with the request filter on Search action – by modifying jsonRequest object. This works fine except one situation, when we have a saved search template, set to be run automatically. In this case, if the template is selected from the ICN Search feature – no problems, everything’s fine, since the search template opens in UI. But if the search template was saved in folder and selected in ICN Browse feature – then jsonRequest is absent, as well as other parameters. Only search template ID is sent to server. As IBM-ers say, this is for performance. But this means I’m not able to modify criterias for this scenario… Maybe Yoy have some ideas on how to solve this?

      Meanwhile, many thanks to You for sharing Your knowledge and experience!

      Reply

Leave a Reply