3 ene 2014

Synchronizing users with ACL metadata boxes

Using WebCenter Content with Access Control List (ACL) enabled. Allow users to setup their custom security based on users and groups that they know.

This is how ACL's looks like under the new WCC UI.


The box of users have an "auto-complete" behavior that helps the search of the user that you want to share your content.

The main problem is that WCC only shows users that already have sign-in or "logged" under the product. This means, that only "active" users will be shown.

To solve this, we can perform a "preload" of all users from our LDAP to the internal DB of WCC. After this loading the auto-complete box will show all the available users of our company.

We need to perform this steps:

Configuring the jps-config.xml

This file has the information relative to our LDAP. In my case Im using ActiveDirectory from Microsoft, by default jps file is configured for Oracle LDAP (OID), that means that will search for "uid" attribute in the person objetc. As most of you know, ActiveDirectory uses the attribute "sAMAccountName" as user id.

You need to modify your WCC domain jps-config.xml file, in my case was under this path:
/opt/oracle/domains/domwc/config/fmwconfig/jps-config.xml

<serviceInstance name="idstore.ldap" provider="idstore.ldap.provider">
    <description>LDAP Identity Store Service Instance</description>
    <property name="idstore.config.provider" value="oracle.security.jps.wls.internal.idstore.WlsLdapIdStoreConfigProvider"/>
    <property name="CONNECTION_POOL_CLASS" value="oracle.security.idm.providers.stdldap.JNDIPool"/>
    <property name="username.attr" value="sAMAccountName"/>
    <property name="user.login.attr" value="sAMAccountName"/>
</serviceInstance>

I've included two properties username.attr and user.login.attr, this properties should be added only if your LDAP does not use the "uid" attribute.

Sync users from LDAP with WCC

For sync the users, I've created a small Java program that connects with LDAP to read all available users, and later performs a call to the service "CHECK_USER_CREDENTIALS" via RIDC. That service will force a connect with the LDAP within UCM, and all the information will be added to the local database of the content server.

The Java process needs the following parameters:
  • LDAP IP or Host
  • LDAP Admin user DN
  • LDAP Admin user password
  • Base DN for user search
The sample JDev project has a RIDC connection setup that points to the RIDC ip & port of your content server, be sure to setup this connection along with the parameters needed of the LDAP server.

After the execution of the program, you can search all your users under ACL boxes ;-)


Documentation

Downloads

10 oct 2013

View SiteStudio Dynamic List on WebCenter Portal application

Last week my coworker Daniel wanted to view the results of a Dynamic List on a WebCenter Portal Content Presenter Template. After a few test we saw that this feature is not suppported.

Under a Content Presenter Template (CPT) you can show any field of the DataFile, simple text, WYSWYG, static list of elements, but seems that the Dynamic List was missing by WebCenter developers ;-)

To solve this issue we created a custom TaskFlow that is capable of listing the elements of a Dynamic List (Element Definition).

First I've created a new Element Definition (ED) of type "Dynamic List", this ED will perform this query:

dExtension <matches> `docx`

This will retrieve all Word documents of the WebCenter Content (WCC).

Next, I've created a Region Definition (RD) with two fields, one of type "Single Text" and another with the DynamicList (ED) just created.


After that, I've created a DataFile based on that Region Definition, this is the resulting XML content:


<wcm:root version="8.0.0.0" xmlns:wcm="http://www.stellent.com/wcm-data/ns/8.0.0">
  <wcm:element name="text">This is a simple text</wcm:element>
</wcm:root>

As you see, there is not track about the ED with the DynamicList, but if you edit that DataFile on WCC interface this is how it looks


So, where is the information of the "Documents" section stored? In the Element Definition called "ED-DYNLIST".

This is the content of the ED file:


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<elementDefinition xmlns="http://www.oracle.com/sitestudio/Element/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.oracle.com/sitestudio/Element/ http://www.oracle.com/sitestudio/ss_element_definition.xsd">
    <property value="3" name="type"/>
    <property value="" name="description"/>
    <complexProperty name="flags">
  ...
  ...
    </complexProperty>
    <property value="" name="height"/>
    <complexProperty name="dynlistaddregioncontent">
  ...
  ...
    </complexProperty>
    <dataProperty name="querytext">dExtension <matches> `docx`</dataProperty>
    <property value="dDocTitle" name="sortfield"/>
    <property value="asc" name="sortorder"/>
    <property value="20" name="resultcount"/>
    <property value="false" name="limitscope"/>
    <property value="" name="targetnodeid"/>
</elementDefinition>


As you see the query is stored on <dataProperty> TAG.

Now that we have all the needed information, we can create the TaskFlow that parses the WCC query stored on the ED file, and show the results of that query under a Content Presenter.

Diagram of our TaskFlow


This is how it looks like at the end



Download Links

Documentation

2 oct 2013

ADF Twitter timeline

Yesterday I was playing with twitter widgets, specifically I was trying to add the timeline to my blog, Is quite simple and these are the steps.

Log-in on your twitter account and go to "setup" menu item.



Now go to "Widgets" option and try to create a new one.


You can create timeline by user or by hashtag, in this example is created with #webcenter hashtag.


After the creation, the page will give you the HTML code to embed the timeline on your website. We have to look the <a> tag and save the "href" and "data-widget-id"

<a class="twitter-timeline" href="https://twitter.com/search?q=%23webcenter" data-widget-id="385306900536885248">Tweets sobre "#webcenter"</a>

Now we have the HTML to add the timeline in any page, but this won't work on an ADF application. Lets move this code to a TaskFlow, the first step is to create a JSF fragment for our taskflow that handles the HTML code provided by twitter. This is my fragment code:


<?xml version='1.0' encoding='UTF-8'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1" xmlns:af="http://xmlns.oracle.com/adf/faces/rich" xmlns:trh="http://myfaces.apache.org/trinidad/html">
  <af:panelGroupLayout id="pgl1" layout="vertical">
    <a class="twitter-timeline" href="${pageFlowScope.href}" data-widget-id="${pageFlowScope.id}">Tweets by @${pageFlowScope.referer}</a>
    <trh:script generatesContent="true" id="jsTwitter">
    !function(d,s,id)
    {
        var js, fjs=d.getElementsByTagName(s)[0], p=/^http:/.test(d.location)?'http':'https';
        if(!d.getElementById(id))
        {
            js=d.createElement(s);
            js.id=id;
            js.src="https://platform.twitter.com/widgets.js";
            fjs.parentNode.insertBefore(js,fjs);
        }
    }
    (document,"script","twitter-wjs");
    </trh:script>
  </af:panelGroupLayout>
</jsp:root>

As you see, href and id are taskflow input parameters, when you add the taskflow on your page you have to add the parameters that were kept from last step.

Create a TaskFlow with the previous view as main view. Also add the both parameters as input for the taskflow.

After all, you only have to drop your TaskFlow as a region on a test page, this is my sample configuration:



Finally this is the result of the ADF TaskFlow after loading the twitter timeline.



Download the sample (link) and export as a ADF TaskFlow for the use on your ADF application.

6 ago 2013

Obtaining user LDAP groups from Weblogic / WebCenter security provider

Sometimes you need to obtain the groups where a user is member. You can check if a user is in a group with a common and simple EL:

#{securityContext.userInRole['Administrators']}

This EL will return true if the user is a member of that group.


In other cases you need the full list of groups where users belongs to, you can achieve this purpose with 2 different solutions.

Weblogic mode


    /**
     * This method returns the default IdentityStore of Weblogic
     * @return
     * @throws JpsException
     */
    private static IdentityStore getIdentityStore() throws JpsException
    {
        JpsContextFactory ctxf = JpsContextFactory.getContextFactory();
        JpsContext ctx = ctxf.getContext();
        IdentityStoreService storeService = ctx.getServiceInstance(IdentityStoreService.class);
        return storeService.getIdmStore();
    }

    /**
     * This method returns a list of the groups where the user belongs to
     * @param userName
     * @return
     */
    public List getRolesFromLDAP()
    {
        List rolesGranted = new ArrayList();
        IdentityStore is = null;
        try
        {
            is = getIdentityStore();
            
            //Getting current user
            ADFContext ctxt = ADFContext.getCurrent();
            SecurityContext sctxt = ctxt.getSecurityContext();

            User userAux = is.searchUser(sctxt.getUserPrincipal());
            RoleManager rm = is.getRoleManager();
            SearchResponse response = rm.getGrantedRoles(userAux.getPrincipal(), false);
            
            while (response.hasNext())
            {
                String name = response.next().getName();
                LOG.info("Añadiendo el rol:" + name);
                rolesGranted.add(name);
            }
            
        }
        catch (Exception e)
        {
            LOG.severe("Error obteniendo los grupos del usuario", e);
        }
        
        return rolesGranted;
    }


WebCenter mode


    public List getRolesWebCenter() throws WCSecurityException
    {
        List roles = new ArrayList();
        ADFContext ctxt = ADFContext.getCurrent();
        SecurityContext sctxt = ctxt.getSecurityContext();
        
        Collection collection = WebCenterSecurityUtils.getEnterpriseRoles(sctxt.getUserName());
        
        for (Principal user : collection)
        {
            String userName = user.getName();
            LOG.info("Role:" + userName);
            roles.add(userName);
        }
        
        return roles;
    }

Here is for download a sample project with this two samples.

And this is a capture of the results


27 may 2013

MouseOut close of ADF Popup with dialog inside.

A couple of months ago I was helping development of ADF 11g application. One of the most important parts of that app was the menu bar.

This menu was designed to support 4 levels, and almost 900 different options items. One of the problems that I faced was that some options of the menu opens a af:popup with an af:dialog inside. That options needs to be opened when user clicks, and self-closed when user moves out of dialog.

In summary, I need the mix of "mouseHover" and "action" types on my <af:showPopupBehavior> tag.

Check the following video to see the menu in action.



To modify the expected behavior of <af:showPopupBehavior> type "action" I've created this simple JavaScript function to overwrite ADF JS library.

AdfShowPopupBehavior.prototype.fire = function (a)
{
    a.cancel();
    var b = AdfPage.PAGE, c = this._type, d = a.getSource(), e = d.getClientId(), f = d.findComponent(this._popupId);
    if (AdfPage.PAGE.isScreenReaderMode() && (c == "mouseHover" || c == "mouseMove" || c == "mouseOver" || c == "mouseOut"))
    AdfLogger.LOGGER.fine("showPopupBehavior trigger type " + c + " suppressed in screen reader mode for launch source id: " + e);
    else if (f)
    {
        var g = f._delayedActivationState;
        if (g)
        if (g.launchSourceId == e)
            return;
        else 
        b.cancelTimer(g.timerId), 
        delete f._delayedActivationState;
        if (f.isPopupVisible())
        {
            var h = this._getPopupWindow(f);
            if (h == null)
            return;
            g = this._isInlinePopupSelector(h);
            h = this._wasOpendedFromSameSource(h, e);
            if (g)
            {
                if (f.hide(), h)
                    return 
            }
            else 
            h || f.hide()
        }
        var k = this._align, h = this._alignId, g = 
        {
        };
        g[AdfRichPopup.HINT_LAUNCH_ID] = e;
        var l = this._type == AdfComponentEvent.CONTEXT_MENU_EVENT_TYPE;
        if (h || k || l)
        {
            k && (g[AdfRichPopup.HINT_ALIGN] = k, h || (h = d.getClientId(), k = h.lastIndexOf(":"), k !=  - 1 && (h = h.substring(k + 1))));
            h && ((k = d.findComponent(h)) ? g[AdfRichPopup.HINT_ALIGN_ID] = k.getClientId() : AdfLogger.LOGGER.warning("Unable to find align component: ", h));
            if (l)
            g[AdfDhtmlPopupWindow.HINT_TYPE] = AdfDhtmlPopupWindow.HINT_TYPE_MENU, g[AdfDhtmlPopupWindow.HINT_AUTODISMISS] = AdfDhtmlPopupWindow.HINT_AUTODISMISS_MENU, h || (h = AdfAgent.AGENT.getMousePosition(a.getNativeEvent()), g[AdfDhtmlPopupWindow.HINT_MOUSEPOSITION] = h);
            if (c == "mouseHover" || c == "action")
            g[AdfDhtmlPopupWindow.HINT_AUTODISMISS] = AdfDhtmlPopupWindow.HINT_AUTODISMISS_MOUSEOUT, g[AdfDhtmlPopupWindow.HINT_AUTODISMISS_MOUSEOUT_ID] = g[AdfRichPopup.HINT_ALIGN_ID] ? g[AdfRichPopup.HINT_ALIGN_ID] : g[AdfRichPopup.HINT_LAUNCH_ID]
        }
        a.getType() == AdfUIInputEvent.MOUSE_IN_EVENT_TYPE ? (a = b.scheduleTimer(this, this._onMouseOverTimeout, 
        {
            hints : g, popup : f, source : d
        },
500), f._delayedActivationState = 
        {
            timerId : a, launchSourceId : e
        },
        d.addEventListener(AdfUIInputEvent.MOUSE_OUT_EVENT_TYPE, this._fireCancel, this)) : f.show(g)
    }
    else 
    AdfLogger.LOGGER.severe("Could not find popup ", this._popupId, " from component ", d)
};

The line that changes the expected behavior is this one:

if (c == "mouseHover" || c == "action")

Include that JS fragment in a JS file and later import to your pageTemplate or JSPX page:

<af:resource type="javascript" source="../js/popbehavior.js"/>

Also, you can modify the delay time of mouse-out event modifiying the "500ms" value in the JS code.

 Useful links

9 abr 2013

WebCenter Spaces, navigation from one space to another space specifiying the destination path

Recently on a project we have the requirement of jumping from a Space to another one, but not the landing page.

This is the hierarchy and the desired navigation.






By default you can access to SpaceA with this URL: http://server/webcenter/spaces/spaceA

To access to second space the URL is http://server/webcenter/spaces/spaceB

Inside of SpacesB if you try to move to a "detail page" you can use http://server/webcenter/faces/news/detail because the navigation of the Space is the "default navigation" stored in memory.

Now imagine you're on SpaceA "home page", if you try to move to "detail page" of SpaceB you will get an error of 404 page not found.





This is produced because your current navigation is the default navigation defined in SpaceA.

To bypass this problem, we need to read the "default navigation" of SpaceB and create the correct link to the page.

First, we need the path of the "default navigation" of SpaceB, to get this value, we have to log as space administrator and go to the tab called "Resources".

Go to: http://server/webcenter/spaces/spaceB/admin

Select the correct resource and click "Edit Properties".


You will see a popup similar to this one, copy the value of the "Metadata File" clicking on "more" icon.





Now we have all needed information. It's time to create a "<af:goLink>" from one space to another.

Check this code:
<af:golink destination="#{navigationContext.navigationModel['/oracle/webcenter/siteresources/scopedMD/s8195d183_d2b1_4c90_b0fa_b541d2031f79/navigation/gsrbe2d4c14_2b20_4e1b_94aa_cc1930641d4e/default-navigation.xml'].node['news/detail'].goLinkPrettyUrl}" id="gl1">
</af:golink>


This is the syntax of the Expression Language (EL) used:

 #{navigationContext.navigationModel['<path to xml>'].node['<hierarchy>'].goLinkPrettyUrl}

This <af:goLink> will produce a HTML output like this:

http://server/webcenter/faces/news/detail?wc.contextURL=/spaces/spaceA&&wcnav.model=%2Foracle%2Fwebcenter%2Fsiteresources%2FscopedMD%2Fs8195d183_d2b1_4c90_b0fa_b541d2031f79%2Fnavigation%2Fgsrbe2d4c14_2b20_4e1b_94aa_cc1930641d4e%2Fdefault-navigation

 Souces:
  • WebCenter Developer's guide (link)

19 feb 2013

Using CMIS query with boolean metadata

Often we create boolean type metadata on WebCenter Content. This type of metadata in fact is an integer type metadata, but uses a system internal view called "YesNoView".

Check screenshots above to see the default configuration of this kind of metadata.



If we check the standard "Check-in" page, the metadata is represented with a "combo-box" input.





You can use this metatada to perform searchs over UCM and retrive the contents that are checked as "hidden" (in this example).


Since WebCenter PS3 was released, you can show content on WebCenter Portal/Spaces with "Content-Presenter (CP)" taskflow. This component shows contents stored at content server (UCM).

CP allows the user to define a query that searchs on content-server the items that matches your query. This is done over CMIS standard instead UCM query syntax.

Now, we try to add a content-presenter on a page, and query filtering with the new metadata (Hidden content). This is the CP wizard screenshot.


Remember to check that the new metadata is in "Filters" section. And save/close the wizard...

The system will show some error stacktrace, this is a summary

Caused by: oracle.webcenter.content.integration.RepositoryException: 19-feb-2013 17:00:46 oracle.webcenter.content.integration.spi.ucm.search.SearchService search
GRAVE: Se ha producido un error al buscar el repositorio ucm. Se ha recibido el código de estado -1 al llamar al servicio GET_SEARCH_RESULTS, como usuario weblogic y registro de hora 19/02/13 17:00. La búsqueda es Search[repositoryId=ucm, max to return=5, useFullTextSearch=true, useCache=true, sort="toProperty('dDocTitle') ASC", fullText="
 Metadata criteria((cm_contentType equals IDC:GlobalProfile)AND(xHiddenContent [any] equals true))
 isOr=false"] y la asignación de parámetros es {ResultCount=5, FolderPathInSearchResults=1, SortField=dDocTitle, IdcService=GET_SEARCH_RESULTS, SortOrder=ASC, vcrAppendObjectClassInfo=1, StartRow=1, QueryText=(xHiddenContent  `true`), vcrContentType=IDC:GlobalProfile}.


If you have installed WebCenter Spaces, we can try the Web-interface to test the CMIS queries. In my case I tried the following URLs:
  • http://owc:8888/rest/api/cmis/query/q/ucm?q=SELECT * FROM ora:t:IDC:GlobalProfile WHERE ora:p:xTuiHiddenContent = TRUE
  • http://owc:8888/rest/api/cmis/query/q/ucm?q=SELECT * FROM ora:t:IDC:GlobalProfile WHERE ora:p:xTuiHiddenContent = 1

And the following errors were launched

oracle.webcenter.content.integration.cmis.query.ParseException: Feb 19, 2013 5:19:27 PM oracle.webcenter.content.integration.cmis.query.VcrExpressionSearchDelegate fixLiteral
SEVERE: Comparing boolean property [xHiddenContent] to non-Boolean literal [1]


oracle.webcenter.content.integration.RepositoryException: Feb 19, 2013 5:19:33 PM oracle.webcenter.content.integration.spi.ucm.search.SearchService search
SEVERE: An error occurred when searching repository ucm.  When calling service GET_SEARCH_RESULTS, as user anonymous, at timestamp 2/19/13 5:19 PM, received status code -1.   The search was Search[repositoryId=ucm, max to return=26, useFullTextSearch=false, useCache=true, expr="(toProperty('xHiddenContent') == true && toProperty('cm_contentType') == 'IDC:GlobalProfile')"] and the parameter map was {ResultCount=26, FolderPathInSearchResults=1, SortField=dInDate, IdcService=GET_SEARCH_RESULTS, SortOrder=Desc, vcrAppendObjectClassInfo=1, StartRow=1, QueryText=(xHiddenContent  `1`), vcrContentType=IDC:GlobalProfile}.

To solve this, there is a workaround that we can do, without affecting the content already stored at UCM.

First create a table in UCM (using ConfigurationManager Applet), this table should have a structure similar to this one:



After that, we create a view that uses that table, similar to this one:



Next you should add the values for true/false.


And finally, attach the new view created to the metadata, replacing the system "YesNoView" with the "YesNoCustomView" just created.


Now, restart WebCenter Spaces and try with this URL:

  • http://owc:8888/rest/api/cmis/query/q/ucm?q=SELECT * FROM ora:t:IDC:GlobalProfile WHERE ora:p:xHiddenContent = 1
Now the query is correctly performed over CMIS standard. And if you try CP Wizard now also works without problems.

I hope this information helps on your project.

Related Information