Sunday, February 14, 2010

ADF UI Shell: Dynamic Tree Menu based on User Roles (ADF Policies)

In this post, I will try to share with you how to create a dynamic tree menu based on the authorization defined in ADF Policies (jazn.xml) in an application built with ADF UI Shell. Menu Items will be added to the tree menu, if the user has a role that was granted view authorization on a certain task-flow or page.


To accomplish the creation of a dynamic menu based on user roles, we need to do at least the following:
  1. Create and populate a Menu Table in the database;
  2. Generate a Menu entity thru the "Entities from Table" wizard in JDeveloper;
  3. Create a session bean to act as facade of our entity;
  4. Create a managed bean to provide tree model of authorized menus in appropriate hierarchy;
  5. Create a .jspx page based on the Oracle Dynamic Tabs Shell Template.
  6. Create a Launcher backing bean that will support our page and provide method to launch new tabs.
  7. Grant view authorization to our taskflows defined in jazn.xml to certain roles.

1) Create and populate a Menu Table in the database;

To support a dynamic menu based on roles we only need to have a table of menu items. A table of roles is not necessary. The key to the solution is the java version of the "#{securityContext.taskFlowViewable['taskFlowId']}" EL expression. Below is the diagram of our table:
ColumnComments
MENU_ID Primary key
DESCRIPTIONThe label that will be displayed on the tree.
DEFINITIONThis will hold taskFlowIds like for example "/WEB-INF/taskflows/gl/natural-acct-list-task-flow.xml#natural-acct-list-task-flow".
PARENT_MENU_IDThe id of the containing menu. This table is recursive.
TYPEThis field will enable us to show different icons per type.
DISPLAY_SEQThe sorting or display sequence of the menus.
IS_MULTIPLE_INSTANCERepresents a boolean value to determine if multiple instance of this taskflow will be allowed on UI Shell tabs.

2) Generate a Menu entity thru the "Entities from Table" wizard in JDeveloper;

Below is a sample generated Menu Entity class (please note the annotations are on the properties instead of the fields - I encountered strange error in related to jpa sessions ("this session is not of the current object but of the parent blah blah...")when I attempted to put the annotations in the fields ).
package blogspot.soadev.model;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

@Entity
@NamedQueries({
  @NamedQuery(name = "findAllMenus", query = "select o from Menu o"),
  @NamedQuery(name = "findRootMenus", query = "select o from Menu o where o.parentMenu IS NULL"),
  @NamedQuery(name = "findTargetRootMenu", query = "select o from Menu o where o.id = :menuId")
})
public class Menu implements Comparable<Menu>, Serializable {   
    private Long id;
    private String description;  
    private String definition;   
    private String type;   
    private Menu parentMenu;   
    private Long displaySeq;  
    private boolean multipleInstance;   
    private List<Menu> childrenMenuList;  
 
    @Column(name="DISPLAY_SEQ", nullable = false)
    public Long getDisplaySeq() {
        return displaySeq;
    }

    public void setDisplaySeq(Long displaySeq) {
        this.displaySeq = displaySeq;
    }
    @Id
    @Column(name="MENU_ID", nullable = false)
    public Long getId() {
        return id;
    }

    public void setId(Long menuId) {
        this.id = menuId;
    }
    @Column(length = 120)
    public String getDescription() {
        return description;
    }

    public void setDescription(String name) {
        this.description = name;
    }
    @Column(length = 120)
    public String getDefinition() {
        return definition;
    }

    public void setDefinition(String taskFlowId) {
        this.definition = taskFlowId;
    }
    @Column(length = 1)
    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
    @ManyToOne
    @JoinColumn(name = "PARENT_MENU_ID")
    public Menu getParentMenu() {
        return parentMenu;
    }

    public void setParentMenu(Menu menu) {
        this.parentMenu = menu;
    }
    
    public void setMultipleInstance(boolean multipleInstance) {
        this.multipleInstance = multipleInstance;
    }
    @Column(name ="IS_MULTIPLE_INSTANCE", length = 1)
    public boolean isMultipleInstance() {
        return multipleInstance;
    }
    @OneToMany(mappedBy = "parentMenu")
    public List<Menu> getChildrenMenuList() {
        Collections.sort(childrenMenuList);
        return childrenMenuList;
    }

    public void setChildrenMenuList(List<Menu> menuList) {
        this.childrenMenuList = menuList;
    }

    public Menu addMenu(Menu menu) {
        getChildrenMenuList().add(menu);
        menu.setParentMenu(this);
        return menu;
    }

    public Menu removeMenu(Menu menu) {
        getChildrenMenuList().remove(menu);
        menu.setParentMenu(null);
        return menu;
    }

    public int compareTo(Menu m) {
        if (m == null){
            return -1;
        }
        return this.displaySeq.compareTo(m.displaySeq);
    }
}
You could note above that I added two named queries: findRootMenus and findTargetMenu. I also implemented a Comparable interface to support sorting of menus based on the desired display sequence.

3) Create a session bean to act as facade of our entity;

Below is my generated session bean:
package blogspot.soadev.service;

import java.util.List;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import blogspot.soadev.model.Menu;

@Stateless(name = "ApplicationManager", mappedName = "DynamicTreeMenuBasedOnRoles-Model-ApplicationManager")
@Remote
@Local
public class ApplicationManagerBean implements ApplicationManager,
                                               ApplicationManagerLocal {
    @PersistenceContext(unitName="Model")
    private EntityManager em;


    /** <code>select o from Menu o</code> */
    public List<Menu> findAllMenus() {
        return em.createNamedQuery("findAllMenus").getResultList();
    }

    /** <code>select o from Menu o where o.id = menuId</code> */
    //I intend it to return a list instead of just a menu.
    public List<Menu> findTargetRootMenu(Long menuId) {
        return em.createNamedQuery("findTargetRootMenu").setParameter("menuId", menuId).getResultList();
    }

    /** <code>select o from Menu o where o.parentMenu IS NULL</code> */
    public List<Menu> findRootMenus() {
        return em.createNamedQuery("findRootMenus").getResultList();
    }
}

4) Create a managed bean to provide tree model of authorized menus in appropriate hierarchy;

Below is the managed bean that we will declare in adfc-config.xml as view scope. This will supply a tree model to our tree component:
package blogspot.soadev.view.managed;

import blogspot.soadev.model.Menu;
import blogspot.soadev.view.util.JSFUtils;
import java.beans.IntrospectionException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import oracle.adf.controller.security.TaskFlowPermission;
import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;
import oracle.adf.share.security.authorization.RegionPermission;
import oracle.binding.BindingContainer;
import oracle.binding.OperationBinding;
import org.apache.myfaces.trinidad.model.ChildPropertyTreeModel;
import org.apache.myfaces.trinidad.model.TreeModel;

public class TreeMenuNavigator implements Serializable {
    private TreeModel model;
    private List<Menu> menuList;
    private List<Menu> authorizedMenuList = new ArrayList<Menu>();
    private Long rootMenuId = 2L; //the root menu Id of my default page
    private Map<Long, Menu> menuMap;

    public void setModel(TreeModel model) {
        this.model = model;
    }

    public TreeModel getModel() throws IntrospectionException {
        if (model == null) {
            model =
                    new ChildPropertyTreeModel(getMenuList(), "childrenMenuList");
        }
        return model;
    }

    public void setMenuList(List<Menu> menuList) {
        this.menuList = menuList;
    }

    public List<Menu> getMenuList() {
        BindingContainer bindings =
            (BindingContainer)JSFUtils.resolveExpression("#{bindings}");
        //I used the findTargetRootMenu() method instead of the findRootMenus
        //because I am reusing the same model for menu of other global tabs as well.
        //A global tab will have a different menu based on the root menu id 
        //that I set on this class whenever I invoke a global tab.
        //for testing purposes you could use the findRootMenus()
        //Please ensure to add the appropriate methodAction in the page definition of the containing .jspx page
        OperationBinding oper =
            (OperationBinding)bindings.getOperationBinding("findTargetRootMenu");
        List<Menu> rootMenuList = (List<Menu>)oper.execute();
        //initialize attributes
        authorizedMenuList = new ArrayList<Menu>();
        menuMap = new HashMap<Long, Menu>();
        menuList = new ArrayList<Menu>();
        reinitializeAuthorizedMenuList(rootMenuList);
        reconstructHierarchicalMenuList(authorizedMenuList);
        List<Menu> resultList = menuList;
        //release attributes that was used in recursive methods
        authorizedMenuList = null;
        menuMap = null;
        menuList = null;
        if (!resultList.isEmpty()) {
            return resultList.get(0).getChildrenMenuList();// I prefer to return the immediate children 
             //rather than the root. But you could try returning resultList instead
            //return resultList;
        }
        return Collections.emptyList();
    }

    private void reinitializeAuthorizedMenuList(List<Menu> menuList) {
        if (menuList == null) {
            return;
        }
        for (Menu menu : menuList) {
            if (isAccessible(menu.getDefinition())) {
                authorizedMenuList.add(menu);
            } else {
                reinitializeAuthorizedMenuList(menu.getChildrenMenuList());
            }
        }
    }


    public void reconstructHierarchy(Menu menu) {
        if (menu == null) {
            return;
        }
        if (menuMap.containsKey(menu.getId())) { //menu already loaded
            return;
        }
        menuMap.put(menu.getId(), menu);
        Menu m = menu.getParentMenu();
        if (m == null) {
            menuList.add(menu);
            return;
        }
        Menu parentCopy = null;
        if (menuMap.containsKey(m.getId())) {
            parentCopy = menuMap.get(m.getId());
        } else {
            parentCopy = copyAttributes(m);
            reconstructHierarchy(parentCopy);
        }
        parentCopy.addMenu(menu);
    }


    public void reconstructHierarchicalMenuList(List<Menu> authorizedMenuList) {
        for (Menu menu : authorizedMenuList) {
            Menu copy = copyAttributes(menu);
            reconstructHierarchy(copy);
        }
    }

    private Menu copyAttributes(Menu menu) {
        if (menu == null) {
            return null;
        }
        Menu copy = new Menu();
        copy.setId(menu.getId());
        copy.setDescription(menu.getDescription());
        copy.setDefinition(menu.getDefinition());
        copy.setParentMenu(menu.getParentMenu());
        copy.setDisplaySeq(menu.getDisplaySeq());
        copy.setType(menu.getType());
        copy.setMultipleInstance(menu.isMultipleInstance());
        return copy;
    }

    //this is the Java version of the "#{securityContext.regionViewable['pageDef']}" EL Expression
    public boolean isRegionViewable(String pageDef) {
        if (pageDef == null) {
            return false;
        }
        RegionPermission permission =
            new RegionPermission(pageDef, RegionPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }
    //this is the Java version of the "#{securityContext.taskFlowViewable['taskFlowId']}" EL Expression
    public boolean isTaskFlowViewable(String taskflowId) {
        if (taskflowId == null) {
            return false;
        }
        TaskFlowPermission permission =
            new TaskFlowPermission(taskflowId, TaskFlowPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }

    public boolean isAccessible(String definition) {
        return (isRegionViewable(definition) ||
                isTaskFlowViewable(definition));
    }

    public void setRootMenuId(Long rootMenuId) {
        this.rootMenuId = rootMenuId;
        model = new ChildPropertyTreeModel(getMenuList(), "childrenMenuList");
    }

    public Long getRootMenuId() {
        return rootMenuId;
    }
}

5) Create a .jspx page based on the Oracle Dynamic Tabs Shell Template.

Below is a snippet of my jspx page based on an extended UI Shell that shows the source of our tree component:
<af:tree var="node" rowSelection="single" id="menuTree"
                         value="#{viewScope.treeMenuNavigator.model}"
                         initiallyExpanded="true" fetchSize="-1"
                         contentDelivery="immediate">
                  <f:facet name="nodeStamp">
                    <af:panelGroupLayout id="pgl10">
                      <af:outputText value="#{node.description}" id="ot2"
                                     rendered="#{node.definition eq null}"
                                     inlineStyle="font-size:larger; font-weight:bold;"/>
                      <af:commandImageLink text="#{node['description']}" id="pt_cil5"
                                           rendered="#{node.definition  ne null}"
                                           icon="#{node.type eq 'L' ? '/images/List16.png': node.type eq 'C' ? '/images/Maintain16.png': node.type eq 'P' ? '/images/Process16.png': node.type eq 'R' ? '/images/Report16.png':  '/images/Transaction16.png'}"
                                           actionListener="#{backingBeanScope.launcher.launchMenu}"
                                           partialSubmit="true"
                                           immediate="true">
                        <f:attribute name="node" value="#{node}"/>
                      </af:commandImageLink>
                    </af:panelGroupLayout>
                  </f:facet>
                </af:tree>

6) Create a Launcher backing bean that will support our page and provide method to launch new tabs.

Below is our Launcher class declared in backingBean scope in adfc-config.xml:
package blogspot.soadev.view.backing;

import blogspot.soadev.model.Menu;
import javax.faces.component.UIComponent;
import javax.faces.event.ActionEvent;
import oracle.ui.pattern.dynamicShell.TabContext;

public class Launcher {
    
    public void launchMenu(ActionEvent event) {
        UIComponent component = (UIComponent)event.getSource();
        Menu menu = (Menu)component.getAttributes().get("node");
        _launchActivity(menu.getDescription(), menu.getDefinition(),
                       menu.isMultipleInstance());
    }
    private void _launchActivity(String title, String taskflowId, boolean newTab)
    {
      try
      {
        if (newTab)
        {
          TabContext.getCurrentInstance().addTab(
            title,
            taskflowId);
        }
        else
        {
          TabContext.getCurrentInstance().addOrSelectTab(
            title,
            taskflowId);
        }
      }
      catch (TabContext.TabOverflowException toe)
      {
        // causes a dialog to be displayed to the user saying that there are
        // too many tabs open - the new tab will not be opened...
        toe.handleDefault(); 
      }
    }
}
Whew! This is rather a long post. To be continued... Continuation...

7) Grant view authorization to our taskflows defined in jazn.xml to certain roles.

Please refer to this post by Chris Muir for detailed info about the minimum setting to run UI Shell with ADF Security. Be sure to check my comments on the bottom :)

Cheers!

3 comments:

  1. Hi
    Very usefull! Thanks
    One question. I want to generate an ADF Lib with this code to import it in a master project.

    What would be the process to generate libs, EJB modules and deploy?
    I tried in diferent ways but get this error
    The module TestDynamicTreeMenu-TestDynamicTreeMenuView-context-root in application TestDynamicTreeMenu_Project1_TestDynamicTreeMenu uses ejb-links but no EJB modules were found for this application

    Thanks in advance

    ReplyDelete
  2. Hi, I see you blog by web search,It's very useful for me .do you have sample and can you send it to me? thanks a lot. my email: ahuuhl@qq.com

    ReplyDelete