Search code examples
jsfjsf-2resourcebundleuser-roles

Properties files to control form inputs based on roles


The requirements of the application that I'm building demands that user roles are to be dynamic, they will be stored in the database, and they will also be mapped to functionalities (forms) of the application, also stored in the database.

Restricting a role from accessing a specific page won't be difficult, but the requirements also states that form inputs must be customized based on roles, which means, an input can be mandatory or not, visible or not, read-only or not based on the role.

My approach to control these restrictions is based on creating a property file for each role, which will store all the inputs of all the forms in the application, as keys, and a long string as value in which we define the state of the input, like the following:

user-inputs.properties

# form.input=mandatory:visibility

searchBooks.bookName=true:true
searchBooks.bookCategory=false:true
searchBooks.authorName=false:false

admin-inputs.properties

searchBooks.bookName=true:true
searchBooks.bookCategory=false:true
searchBooks.authorName=false:true

And then do some magic Java code, whenever a form is accessed, read its inputs properties from the file of the specific user role, and parse the values so I could provide the right value for the rendered="" and required="" attribute of an <h:inputText/>.

This could be a solution, but the inputs of the application are much more than a book name and category, means I will be putting lots of required and rendered attributes which will make JSF pages look ugly with huge amount of variables in the managed bean.

Is there a better approach/framework/solution to my issue?


Solution

  • I think that you are in the right way, and i will continue using your approach which consists of creating multiple property files, one for each user, except that we will not use a any "huge amount of variables in the managed bean".

    So, the first step consists on managing multiple resource properties using a single resource bundle prefix ( the <var></var> in <resource-bundle>), in the second step we will see how to switch between those files, and in the last step we will read from property file using JSTL.

    Managing multiple property files:

    We start by defining our ResourceBundle in the faces-config file:

    <application>
          <resource-bundle>
             <base-name>UserMessages</base-name>
             <var>msgs</var>
          </resource-bundle>
    </application>
    

    UserMessages is a ResourceBundle where we will implement the logic that allow us to switch between our property files (assuming that yourpackage.user-inputs is the fully qualified name of your user-inputs.properties):

    import java.util.Enumeration;
    import java.util.MissingResourceException;
    import java.util.ResourceBundle;
    
    import javax.faces.context.FacesContext;
    
    public class UserMessages extends ResourceBundle {
    
        public UserMessages() {
            // we are loading user-inputs.properties as the default properties file
            setParent(getBundle("yourpackage.user-inputs", FacesContext.getCurrentInstance()
                    .getViewRoot().getLocale()));
        }
    
        @Override
        protected Object handleGetObject(String key) {
            // we could just return parent.getObject(key) but we want to respect JSF recommandations
            try {
                return parent.getObject(key);
            } catch (MissingResourceException e) {
                return "???" + key + "???";
            }
        }
    
        @Override
        public Enumeration<String> getKeys() {
    
            return parent.getKeys();
        }
    
        // this is the method that will allow us to switch between our .properties
        public void setResourceBundle(String basename) {
            setParent(getBundle(basename, FacesContext.getCurrentInstance()
                    .getViewRoot().getLocale()));
        }
    } 
    

    Switching between property files:

    In order to switch from a property file to another we will need to use the method setResourceBundle(String basename) that we just declared in our class above, So in the managed bean where you are declaring your business logic and where you are intending to switch files depending on the user's role, you need to inject the bundle, like:

    //don't forget adding getters and setters or you end with NullPointerException
    @ManagedProperty("#{msgs}")
    private UserMessages userMesssages;
    

    Then, to switch to another file (admin-inputs.properties), just use it like this:

    //yourpackage.admin-inputs is the fully qualified name
    userMesssages.setResourceBundle("yourpackage.admin-inputs");
    

    NB: You can inject the bundle in that way (above) only in request scoped beans, to use it in broader scopes please see: Read i18n variables from properties file in a Bean

    Now, as we can switch easily from the user-inputs to the admin-inputs, the last step is the easiest one.

    Parsing the property file:

    The bad news, is that when using this approach you will need to add rendered="" and required="" attribute to every input you are willing to manage (but don't forget that the good ones was that you will not need to manage variables in managed beans ;) ).

    First, you need to add JSTL namespaces declaration on the top of your xhtml file:

    xmlns:fn="http://java.sun.com/jsp/jstl/functions"
    

    you can find more about JSTL functions in the javadocs, regarding the function substringAfter:

    Returns a subset of a string following a specific substring.

    Example:

      P.O. Box: ${fn:substringAfter(zip, "-")}
    

    The function substringBefore:

    Returns a subset of a string before a specific substring.

    Example:

      Zip (without P.O. Box): ${fn:substringBefore(zip, "-")}
    

    Second, as the first part of your String represents the required attribute:

    //Returns the substring of msgs['searchBooks.authorName'] before the first occurrence of the separator ':'    
    required="${fn:substringBefore(msgs['searchBooks.authorName'], ':')}"
    

    and the second part:

    //Returns the substring of msgs['searchBooks.authorName'] after the first occurrence of the separator ':'.
    rendered="${fn:substringAfter(msgs['searchBooks.authorName'], ':')}"
    

    See also: