Monday, December 15, 2014

OIM Managed Bean Example: Custom Password Reset

Tested On: Oracle Identity Manager 11.1.2.2.0, WebLogic 10.3.6, JDeveloper 11.1.1.7.0
Description: Demonstrates how to setup and deploy a custom managed bean, and apply UI customization that triggers the bean. The example given here is a custom password reset managed bean. You can download the project here. Below are the results of the completing this tutorial:






Referenceshttp://docs.oracle.com/cd/E40329_01/dev.1112/e27150/uicust.htm#OMDEV4804
http://docs.oracle.com/cd/E40329_01/dev.1112/e27150/facesutils.htm#OMDEV5216
http://fusionsecurity.blogspot.com/2013/09/oim-reset-password-customization-example.html

Developing Custom Managed Bean
Refer to here for setting up the application in JDeveloper.

Source Code
CustomPasswordReset class
package oracle.iam.ui.custom;

import java.text.MessageFormat;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Random;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import oracle.adf.view.rich.event.DialogEvent;
import oracle.core.ojdl.logging.ODLLevel;
import oracle.core.ojdl.logging.ODLLogger;
import oracle.iam.identity.exception.NoSuchUserException;
import oracle.iam.identity.exception.UserLookupException;
import oracle.iam.identity.usermgmt.api.UserManager;
import oracle.iam.identity.usermgmt.api.UserManagerConstants;
import oracle.iam.identity.usermgmt.vo.User;
import oracle.iam.passwordmgmt.api.PasswordMgmtService;
import oracle.iam.passwordmgmt.vo.PasswordPolicyInfo;
import oracle.iam.passwordmgmt.vo.ValidationResult;
import oracle.iam.ui.platform.model.common.OIMClientFactory;

/**
 * Custom Password Reset
 * - Generates a password that conforms to password policy
 * - Displays generated password in UI
 */
public class CustomPasswordReset 
{
    // Logger
    private static final ODLLogger logger = ODLLogger.getODLLogger(CustomPasswordReset.class.getName());
    
    private static final String DEFAULT_SYMBOLS = "!@#$%";
    private static final String DEFAULT_POSSIBLE_CHARACTERS="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + DEFAULT_SYMBOLS;
    private static final String USER_LOGIN_ATTRIBUTE = "userLogin";
    
    /**
     * Confimation dialog box 
     * @param evt
     */
    public void confirmReset(DialogEvent evt) 
    {
        logger.log(ODLLevel.NOTIFICATION, "Enter confirmReset with outcome: [{0}]", new Object[]{evt.getOutcome().name()});

        try 
        {
            // User selects "yes" on the dialog box
            if (evt.getOutcome().compareTo(DialogEvent.Outcome.yes) == 0) 
            {
                // Get the target User Login
                String userLogin = oracle.iam.ui.custom.FacesUtils.getAttributeBindingValue(USER_LOGIN_ATTRIBUTE, String.class).trim();
                logger.log(ODLLevel.NOTIFICATION, "Target User Login: [{0}]", new Object[]{userLogin});
                
                // Generate Random Password
                String generatedPassword = generatePassword(userLogin);
                logger.log(ODLLevel.TRACE, "Generated Password: [{0}]", new Object[]{generatedPassword});
                
                // Change Target User's password
                UserManager userManager = OIMClientFactory.getUserManager();
                userManager.changePassword(userLogin, generatedPassword.toCharArray(), true, false);
               
                this.setFacesMessage(MessageFormat.format("Generated password for {0}: {1}", userLogin, generatedPassword));
                logger.log(ODLLevel.NOTIFICATION, "Reset password for user {0}", new Object[]{userLogin});
            }
        } 
        
        catch (Exception e) 
        {
            this.setFacesMessage("An internal error has occourred: " + e.getLocalizedMessage());
            logger.log(ODLLevel.ERROR, "", e);
        }
        
        logger.exiting(this.getClass().getName(), "confirmReset");
    }

    /**
     * Used to display results in the UI 
     * @param msg   Message to display in UI
     */
    private void setFacesMessage(String msg) 
    {
        FacesMessage message = new FacesMessage();
        message.setDetail(msg);
        message.setSummary(msg);
        message.setSeverity(FacesMessage.SEVERITY_INFO);

        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, message);
    }
    
    /**
    * Generates a temporary password that conforms to:  
    *  - OIM password policy that is applicable to target user
    *  - Custom password policy rules
    *      At least one number or symbol
    *      The email address of the user [IGNORE CASE]
    *      The user's display name [IGNORE CASE]; Remove spaces
    * @param userLogin    OIM.User Login (USR_LOGIN)
    * @return Generated password
    */
   private String generatePassword(String userLogin) throws NoSuchUserException, UserLookupException, Exception 
   {
       logger.log(ODLLevel.NOTIFICATION, "Enter generatePassword with parameter: [User Login: {0}]", new Object[]{userLogin});
       String passwordVal = "";
       ValidationResult vr = null;
       boolean matchCustomPasswordPolicy = false;
               
       // Get OIM Services
       PasswordMgmtService pwdMgmtService = OIMClientFactory.getPasswordMgmtService();
       UserManager userManager = OIMClientFactory.getUserManager();
               
       try 
       {
           // Fetch target user attributes
           HashSet<String> retUserAttrs = new HashSet<String>();
           retUserAttrs.add(UserManagerConstants.AttributeName.USER_KEY.getId()); // usr_key
           retUserAttrs.add(UserManagerConstants.AttributeName.FIRSTNAME.getId());
           retUserAttrs.add(UserManagerConstants.AttributeName.LASTNAME.getId());
           retUserAttrs.add(UserManagerConstants.AttributeName.DISPLAYNAME.getId());
           retUserAttrs.add(UserManagerConstants.AttributeName.EMAIL.getId());
           User user = userManager.getDetails(userLogin, retUserAttrs, true);
           logger.log(ODLLevel.NOTIFICATION, "User: {0}", new Object[]{user});
           
           // Make dummy call in order to obtain the password policy applicable to current user
           vr = pwdMgmtService.validatePasswordAgainstPolicy(passwordVal.toCharArray(), user.getEntityId(), Locale.getDefault(), false);
           PasswordPolicyInfo userPwdPolicy = vr.getPasswordPolicyInfo();
           
           // Validation check on password policy
           /*if (userPwdPolicy.getMaxLength() == null) 
           {
               throw new Exception("Password Policy must have a value for max length since the max property is used as the length of the generated password.");
           }
           
           if (userPwdPolicy.getAllowedChars().size() == 0) 
           {
               throw new Exception("Password Policy must have some value for allow character property");
           }*/
           
           // Fetch Password Policy data to use for generateing the password
           int maxPwdLength = (userPwdPolicy.getMaxLength() == null) ? 32 : userPwdPolicy.getMaxLength(); // Use length 32 if password policy does not specify a value for max length
           LinkedHashSet<Character> allowChars = userPwdPolicy.getAllowedChars(); 
           logger.log(ODLLevel.NOTIFICATION, "Allow Characters: [{0}] Size: [{1}] ", new Object[]{allowChars, allowChars.size()});
           logger.log(ODLLevel.NOTIFICATION, "Max Password Length: {0}", new Object[]{maxPwdLength});
           
           // Generator object
           Random rand = new Random();
           
           // Possible characters for generated password
           char[] possibleCharsForPwd = (userPwdPolicy.getAllowedChars().size() == 0) ? DEFAULT_POSSIBLE_CHARACTERS.toCharArray() : new char[allowChars.size()];
           String symbols = "";
           int count = 0;
           
           // Put allow characters into an array
           for (Character c : allowChars) 
           {
               possibleCharsForPwd[count] = c.charValue();
               count++;
               
               // Check if character is a symbol; assume non alpha-numeric characters are symbols
               if (!(String.valueOf(c).matches("^[a-zA-Z0-9]*$")))
               {
                   symbols = symbols + String.valueOf(c); // add character to string
               }
           }
           
           symbols = symbols.equals("") ? DEFAULT_SYMBOLS : symbols; // default symbols
           logger.log(ODLLevel.NOTIFICATION, "Possible characters for generated password: {0}", new Object[]{possibleCharsForPwd});
           logger.log(ODLLevel.NOTIFICATION, "Symbols: {0}", new Object[]{symbols});
           
           logger.info("Begin random password generation iteration");
           do 
           {
               char ch;
               passwordVal = "";
                   
               // Construct random password; Use max length as the size of the genereated password
               for (int i = 0; i < maxPwdLength; i++) 
               {
                   ch = possibleCharsForPwd[rand.nextInt(possibleCharsForPwd.length - 1)];
                   passwordVal = passwordVal + Character.toString(ch); 
               }

               // Check against OIM password policy that is applicable to user
               vr = pwdMgmtService.validatePasswordAgainstPolicy(passwordVal.toCharArray(), user.getEntityId(), Locale.getDefault(), false);
               logger.log(ODLLevel.NOTIFICATION, "Password Policy: {0}", new Object[]{vr.getPasswordPolicyInfo()});
               logger.log(ODLLevel.NOTIFICATION, "Does password conform to OIM password policy? {0}", new Object[]{vr.isPasswordValid()});
               
               // Check against custom password policy rules
               // At least one number or symbol
               // The email address of the user [IGNORE CASE]
               // The user's display name [IGNORE CASE]; Remove spaces
               String customPasswordPolicyRules = "^((?=.*[0-9])|(?=.*["+ symbols +"]))(?!.*(?i)(" + user.getEmail() + "))(?!.*(?i)(" + user.getDisplayName().replaceAll("\\s+","") + ")).*$";
               logger.log(ODLLevel.NOTIFICATION, "Custom Password Policy Rule: {0}", new Object[]{customPasswordPolicyRules});
               matchCustomPasswordPolicy = passwordVal.matches(customPasswordPolicyRules);
               logger.log(ODLLevel.NOTIFICATION, "Does password conform to custom password policy? {0}", new Object[]{matchCustomPasswordPolicy});
               logger.log(ODLLevel.TRACE, "Generated Password: {0}", new Object[]{passwordVal}); // TODO: Remove
               
           } while (vr.isPasswordValid() == false || matchCustomPasswordPolicy == false);

           logger.log(ODLLevel.NOTIFICATION, "Generated password validated.");
           return passwordVal;            
       } 
       
       catch (NoSuchUserException e) 
       {
           logger.log(ODLLevel.SEVERE, "Failed in generatePassword()", e);
           throw e;
       } 
       
       catch (UserLookupException e) 
       {
           logger.log(ODLLevel.SEVERE, "Failed in generatePassword()", e);
           throw e;
       }
   }
}

FacesUtils class
package oracle.iam.ui.custom;

import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
import javax.el.ELContext;
import javax.el.ExpressionFactory;
import javax.el.MethodExpression;
import javax.el.ValueExpression;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import oracle.adf.model.BindingContext;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.adf.model.binding.DCControlBinding;
import oracle.adf.view.rich.context.AdfFacesContext;
import oracle.binding.AttributeBinding;
import oracle.binding.ControlBinding;
import oracle.iam.ui.platform.utils.TaskFlowUtils;
import oracle.jbo.uicli.binding.JUCtrlActionBinding;
import oracle.jbo.uicli.binding.JUCtrlListBinding;
import oracle.jbo.uicli.binding.JUEventBinding;

public class FacesUtils {
    private FacesUtils() {
        // do not instantiate
        throw new AssertionError();
    }

    /**
     * Re-render the component.
     */
    public static void partialRender(UIComponent component) {
        if (component != null) {
            AdfFacesContext.getCurrentInstance().addPartialTarget(component);
        }
    }

    /**
     * Sets attribute value through attribute binding.
     */
    public static void setAttributeBindingValue(String attributeName,
                                                Object value) {
        AttributeBinding binding = getAttributeBinding(attributeName);
        if (binding != null) {
            binding.setInputValue(value);
        } else {
            throw new IllegalArgumentException("Binding " + attributeName +
                                               " does not exist.");
        }
    }

    /**
     * Gets attribute value using attribute binding.
     */
    public static <T> T getAttributeBindingValue(String attributeName,
                                                 Class<T> clazz) {
        AttributeBinding binding = getAttributeBinding(attributeName);
        if (binding != null) {
            return (T)binding.getInputValue();
        } else {
            throw new IllegalArgumentException("Binding " + attributeName +
                                               " does not exist.");
        }
    }

    /**
     * Gets attribute value using list binding.
     */
    public static <T> T getListBindingValue(String attributeName,
                                            Class<T> clazz) {
        ControlBinding ctrlBinding = getControlBinding(attributeName);
        if (ctrlBinding instanceof JUCtrlListBinding) {
            JUCtrlListBinding listBinding = (JUCtrlListBinding)ctrlBinding;
            return (T)listBinding.getAttributeValue();
        } else {
            throw new IllegalArgumentException("Binding " + attributeName +
                                               " is not list binding.");
        }
    }

    public static ControlBinding getControlBinding(String name) {
        ControlBinding crtlBinding = getBindings().getControlBinding(name);
        if (crtlBinding == null) {
            throw new IllegalArgumentException("Control Binding '" + name +
                                               "' not found");
        }
        return crtlBinding;
    }

    public static AttributeBinding getAttributeBinding(String name) {
        ControlBinding ctrlBinding = getControlBinding(name);
        AttributeBinding attributeBinding = null;
        if (ctrlBinding != null) {
            if (ctrlBinding instanceof AttributeBinding) {
                attributeBinding = (AttributeBinding)ctrlBinding;
            }
        }
        return attributeBinding;
    }

    public static DCBindingContainer getBindings() {
        FacesContext fc = FacesContext.getCurrentInstance();
        ExpressionFactory exprfactory =
            fc.getApplication().getExpressionFactory();
        ELContext elctx = fc.getELContext();

        ValueExpression valueExpression =
            exprfactory.createValueExpression(elctx, "#{bindings}",
                                              Object.class);

        DCBindingContainer dcbinding =
            (DCBindingContainer)valueExpression.getValue(elctx);

        return dcbinding;
    }

    /**
     * Evaluates EL expression and returns value.
     */
    public static <T> T getValueFromELExpression(String expression,
                                                 Class<T> clazz) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application app = facesContext.getApplication();
        ExpressionFactory elFactory = app.getExpressionFactory();
        ELContext elContext = facesContext.getELContext();
        ValueExpression valueExp =
            elFactory.createValueExpression(elContext, expression, clazz);
        return (T)valueExp.getValue(elContext);
    }

    /**
     * Gets MethodExpression based on the EL expression. MethodExpression can then be used to invoke the method.
     */
    public static MethodExpression getMethodExpressionFromEL(String expression,
                                                             Class<?> returnType,
                                                             Class[] paramTypes) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application app = facesContext.getApplication();
        ExpressionFactory elFactory = app.getExpressionFactory();
        ELContext elContext = facesContext.getELContext();
        MethodExpression methodExp =
            elFactory.createMethodExpression(elContext, expression, returnType,
                                             paramTypes);
        return methodExp;
    }

    public static ELContext getELContext() {
        return FacesContext.getCurrentInstance().getELContext();
    }

    /**
     * Shows FacesMessage. The message will not be bound to any component.
     */
    public static void showFacesMessage(FacesMessage fm) {
        FacesContext.getCurrentInstance().addMessage(null, fm);
    }

    /**
     * Launch bounded taskFlow based on provided parameters.
     */
    public static void launchTaskFlow(String id, String taskFlowId,
                                      String name, String icon,
                                      String description, String helpTopicId,
                                      boolean inDialog,
                                      Map<String, Object> params) {
        // create JSON payload for the contextual event
        String jsonPayLoad =
            TaskFlowUtils.createContextualEventPayLoad(id, taskFlowId, name,
                                                       icon, description,
                                                       helpTopicId, inDialog,
                                                       params);

        // create and enqueue contextual event
        DCBindingContainer bc =
            (DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
        DCControlBinding ctrlBinding =
            bc.findCtrlBinding(TaskFlowUtils.RAISE_TASK_FLOW_LAUNCH_EVENT);
        // support both bindings - using eventBinding as well as methodAction
        if (ctrlBinding instanceof JUEventBinding) {
            JUEventBinding eventProducer = (JUEventBinding)ctrlBinding;
            bc.getEventDispatcher().queueEvent(eventProducer, jsonPayLoad);
        } else if (ctrlBinding instanceof JUCtrlActionBinding) {
            JUCtrlActionBinding actionBinding =
                (JUCtrlActionBinding)ctrlBinding;
            bc.getEventDispatcher().queueEvent(actionBinding.getEventProducer(),
                                               jsonPayLoad);
        } else {
            throw new IllegalArgumentException("Incorrect binding for " +
                                               TaskFlowUtils.RAISE_TASK_FLOW_LAUNCH_EVENT);
        }
        bc.getEventDispatcher().processContextualEvents();
    }

    /**
     * Redirect to a provided url.
     */
    public static void redirect(String url) {
        try {
            FacesContext fctx = FacesContext.getCurrentInstance();
            fctx.getExternalContext().redirect(url);
            fctx.responseComplete();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Add Custom Managed Bean
1. Locate adfc-config.xml file in your ViewController project, and open it.


2. Click the Overview tab, and select Managed Beans.


3. Add a managed bean and specify the details.
Name: customPasswordResetBean
Class: oracle.iam.ui.custom.CustomPasswordReset
Scope: BackingBean

Deploying Custom Code to Oracle Identity Manager
1. Create an ADF library JAR file from the ViewController project in JDeveloper. Right click on your ViewController project, select Deploy, and click your deployment profile name.


2. Backup oracle.iam.ui.custom-dev-starter-pack.war, which is located in "$MW_HOME/Oracle_IDM1/server/apps" directory. Copy the WAR file to a staging directory.

3. Extract the WAR file in the staging directory.


4. In the WEB-INF directory, create a lib directory and place your custom ADF library JAR file there. Repackage the WAR file.



5. Move the new oracle.iam.ui.custom-dev-starter-pack.war file to "$MW_HOME/Oracle_IDM1/server/apps" directory.

6. Log into the WebLogic Server Administration Console.


7. Shutdown the OIM managed server(s). To do so, navigate to Servers -> Control -> Check oim_server1 -> Shutdown.


8.  Navigate back to the home page, and click Deployments under Your Deployed Resources.


9. Find oracle.iam.ui.custom(11.1.1,11.1.1), check mark it, and click Update on top.



10. Click Finish.



11. Start the OIM managed server(s).


UI Customization
1. Log into Identity Self Service.


2. Create and activate a sandbox. To do so, click Sandboxes located at the top right of the home page. Then click Create Sandbox, and provide Sandbox Name and Sandbox Description on the dialog form.





3. On the left pane, click Users under Administration.


4. Search for any user and view the account details. On the top right of the page, click Customize.


5. Click View on the top left page and select Source form the drop down menu.



6. Highlight the panel that has the user account operations and then click. In the Web Composer, make sure you are on panelGroupLayout: horizontal and see the user operations such as Modify User underneath.



7. Click Add Content on the web composer.


8. Click Web Components, and add Command Image Link and a Spacer. Then click Close.




9. You should see the new components added underneath panelGroupLayout: horizontal. Then edit the components.


For the spacer component, set the height to 10 and width to 10. Then click Apply.



For the command link image component, set Icon and Text. Then click Apply.



11. To adjust the menu ordering, highlight panelGroupLayout: horizontal, and then click Edit.



Click Child Components and adjust the ordering of the components.


12. Close web composer, navigate to the Sandboxes page, and export the sandbox.


13. Unzip the sandbox and modify oracle/iam/ui/manageusers/pages/mdssys/cust/site/site/userdetails.jsff.xml file to trigger off the custom managed bean on the new UI components you've created.

<?xml version='1.0' encoding='UTF-8'?>
<mds:customization version="11.1.1.64.93" xmlns:mds="http://xmlns.oracle.com/mds" motype_local_name="root" motype_nsuri="http://java.sun.com/JSP/Page">
   <mds:insert parent="pgl11" position="last">
      <af:spacer xmlns:af="http://xmlns.oracle.com/adf/faces/rich" width="10" id="e5907476489" height="10"/>
   </mds:insert>
   <mds:move node="cil3" parent="pgl11" position="last"/>
   <mds:move node="s8" parent="pgl11" position="last"/>
   <mds:move node="cil4" parent="pgl11" position="last"/>
   <mds:move node="s9" parent="pgl11" position="last"/>
   <mds:move node="cil5" parent="pgl11" position="last"/>
   <mds:move node="s10" parent="pgl11" position="last"/>
   <mds:move node="cil7" parent="pgl11" position="last"/>
   <mds:move node="s11" parent="pgl11" position="last"/>
   <mds:move node="cil6" parent="pgl11" position="last"/>
   <mds:move node="s17" parent="pgl11" position="last"/>
   <mds:insert parent="pgl11" position="last">
      <af:commandImageLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e3869694827" icon="/images/passwordField.png" text="Custom Password Reset" shortDesc="" useWindow="false">
         <af:showPopupBehavior xmlns:af="http://xmlns.oracle.com/adf/faces/rich" popupId="confPopup1" triggerType="click"/>
      </af:commandImageLink>
   </mds:insert>
   <mds:insert parent="pgl11" position="last">
      <af:popup xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="confPopup1">
         <af:dialog xmlns:af="http://xmlns.oracle.com/adf/faces/rich" title="Confirm Password Reset" id="confDialog1" type="yesNo" dialogListener="#{backingBeanScope.customPasswordResetBean.confirmReset}"/>
      </af:popup>
   </mds:insert>
   <mds:modify element="s8">
      <mds:attribute name="height" value="10"/>
      <mds:attribute name="width" value="10"/>
   </mds:modify>
   <mds:modify element="pgl11">
      <mds:attribute name="partialTriggers" value="ctb3"/>
   </mds:modify>
</mds:customization>


Below are the text important to look at:
- Triggers a popup when the custom password reset is clicked on.
<af:commandImageLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e3869694827" icon="/images/passwordField.png" text="Custom Password Reset" shortDesc="" useWindow="false">
         <af:showPopupBehavior xmlns:af="http://xmlns.oracle.com/adf/faces/rich" popupId="confPopup1" triggerType="click"/>
</af:commandImageLink>

- Executes the custom managed bean:
<af:popup xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="confPopup1">
     <af:dialog xmlns:af="http://xmlns.oracle.com/adf/faces/rich" title="Confirm Password Reset" id="confDialog1" type="yesNo" dialogListener="#{backingBeanScope.customPasswordResetBean.confirmReset}"/>
</af:popup>
/*Format: SCOPE.BEAN_NAME.METHOD_NAME */
dialogListener="#{backingBeanScope.customPasswordResetBean.confirmReset}


14. Re-zip the sandbox package, import to OIM, and then publish sandbox.







7 comments:

  1. Replies
    1. Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download Now

      >>>>> Download Full

      Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download LINK

      >>>>> Download Now

      Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download Full

      >>>>> Download LINK yq

      Delete
  2. Thanks, it worked like a charm without any issue.

    ReplyDelete
  3. This is the best oracle blog ever.

    ReplyDelete
  4. I just tried this code. I deployed the new war file in OIM. I received the error massage as java.lang.ClassNotFoundException: oracle.iam.ui.custom.CustomPaswordReset.

    Then i tried to unzip both my jar and you jar which i downloaded from https://github.com/rayedchan/OIMCustomPasswordResetBean.

    There i see anew java file as CustomReqBean.java which i don't see in my file. Could you please help me out.

    ReplyDelete
  5. This is a great example.I have done this two times and it worked for me

    ReplyDelete
  6. Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download Now

    >>>>> Download Full

    Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download LINK

    >>>>> Download Now

    Oracle Stack: Oim Managed Bean Example: Custom Password Reset >>>>> Download Full

    >>>>> Download LINK z3

    ReplyDelete