Properties files to control form inputs based on roles - jsf

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?

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:
JSF Internationalization f:loadbundle or through faces-config:
Performance point
Difference between by Application#getResourceBundle() and ResourceBundle#getBundle() in JSF 2.0
How to remove the surrounding ??? when message is not found in
bundle
Context Sensitive Resource Bundle entries in JavaServer Faces
applications – going beyond plain language, region & variant
locales

Related

Load bundle string (in right language) when value depends on bean condition

I have a Problem with jsf and multiple languages. So my strings are in WEB_INF/classes/texte_<lang>.properties files. And are accessed for example like that
<h:outputLabel value="#{messages.err_text}"/>
which works fine.
The problem is, i have <h:outputLabel... element where i want to show an error message depending on the error. I would like something that works like this:
<h:outputLabel value="#{someBean.errMsg}/>
With a Bean like that
#ManagedBean()
#SessionScoped
public class SomeBean{
public String getErrMsg(){
if(something){
return "#{messages.err_text}"
}else if(somethingElse){
return "#{messages.err_text2}"
}else{
return "#{messages.err_text3}"
}
}
}
Just to be clear it doesn't work that way. I'm looking for a similar solution (or any solution) that works.
Thanks for reading.
Don't do it that way. The model shouldn't be aware of the view. Localization also doesn't strictly belong in the model. The model should instead prepare some state which the view has to be aware of.
One way would be:
public String getErrMsg(){
if (something) {
return "err_text";
} else if (somethingElse) {
return "err_text2";
} else {
return "err_text3";
}
}
<h:outputLabel value="#{messages[someBean.errMsg]}" />
Other way would be returning an enum as demonstrated in the following related questions: Localizing enum values in resource bundle and How to use enum values in f:selectItem(s).
The reason why what you have now is not working, is because the value attribute of the outputText is evaluated as a plain String, and not as an EL expression.
Going by what you are working with now, the best way to proceed is to inject the resource bundle directly into your bean:
#ManagedProperty("#{messages}")
ResourceBundle messages;
And then,
public String getErrMsg(){
if(something){
messages.getString("err_text");
}
}
In case you're not aware, traditionally, error messages are presented using the h:message component.
On an unrelated note to your original question, you should also know that it's not generally advisable to have processing logic buried in your getter. For one thing, the getter is called multiple times during the rendering of your page. Also for this to work properly, you should be able to guarantee that the value something will stay consistent across the entire lifecycle of a single JSF request

CDI extension, altering processed type

Using Weld 1.1.13.Final in test with Arquillian....
Let's say I inject into a field something volatile. Something like a property subject to change that I want the bean owning the injection point to receive change events. Thought about creating a CDI extension.
Caught ProcessAnnotatedType event and looking for all fields that have an custom annotation on field injection points:
<T> void pat(#Observes ProcessAnnotatedType<T> event, BeanManager bm) {
final AnnotatedType<T> target = event.getAnnotatedType();
for (AnnotatedField<? super T> field : target.getFields())
if (field.isAnnotationPresent(Value.class)) { // ignore that I don't check #Inject here for the moment
CtClass wrapper = pool.get(target.getJavaClass().getName());
ConstPool cp = wrapper.getClassFile().getConstPool();
CtMethod m = CtNewMethod.make(....)
....
wrapper.addMethod(m);
event.setAnnotatedType(bm.createAnnotatedType(wrapper.toClass()));
}
}
Had even grabbed thereafter all the injection points for fields and replaced the underlying WeldField with a new Field corresponding the "wrapper" type. Otherwise bean validation fails.
But this only works for stuff setup during startup not when for example Arquillian uses the Bean Manager to initialize a class that injects one of my "wraps". Things fail since the Bean Resolver uses the Type as a hash key to find beans.
Basically I don't think I can "mask" a class that is annotated (made into a bean) by the CDI with an extra method to receive custom events. Would have been cool but a Type is a Type (i.e. no idea how to proxy or fake the equals/hashCode).
Got it. Turns out the compute value function (google extension) inside the TypeSafeBeanResolver resolver (at least the CDI Weld implementation) is smart. If I just extend the class:
CtClass wrapper = pool.makeClass(target.getJavaClass().getName()+"Proxy");
wrapper.setSuperclass(pool.get(target.getJavaClass().getName()));
.....
final AnnotatedType<T> other = bm.createAnnotatedType(wrapper
.toClass());
then everything works fine. Tested capturing an event in a bean. Will post the code on a Gist with a comment.

Implementing a Locale provider that works in JSF and JAX-RS

I've been joyfully using omnifaces' Faces.getLocale() to aquire the locale used by the currently logged in user (which in turn gets this from a <f:view> definition). I really like the fallback approach from view to client to system default locale as it fits the requirements for locale selection in my application:
If a user is logged in, use his language preference (obtained from the backend entity)
If no user preference can be found, use the highest ranking language from the Accept-Languages HTTP header
If no locale has been selected by now, use the system default.
Now I've started using JAX-RS (resteasy implementation) and find it quite difficult to write a service that will provide my backend code with the current user's locale.
I can't use Faces.getLocale(), since that requires a FacesContext which isn't present during JAX-RS request processing.
I can't use the #Context SecurityContext annotation in a #Provider (which would give me the user preferred locale) or #Context HttpHeaders (access to the client locale) since JAX-RS only injects those when it uses the provider itself, not when my backend code instantiates the class.
And I don't want to litter my method signatures with Locale parameters, since virtually everything requires a locale to be present.
To have a concrete example: I have a vcard generator that generates little NOTE fields depending on the user's preferred locale. I can both call the vcard generating method via JSF/EL:
<h:commandLink action="#{vcfGenerator.forPerson(person)}"
value="Go" target="_blank" />
And via a REST service:
#GET #Path('person/{id:[1-9][0-9]*}/vcard')
#Produces('text/vcard')
String exportVcard(#PathParam('id') Long personId, #Context HttpHeaders headers) {
VcfGenerator exporter = Component.getInstance(VcfGenerator) as VcfGenerator
Person person = entityManager.find(Person, personId)
if (! person)
return Response.noContent().build()
def locale = headers.acceptableLanguages[0] ?: Locale.ROOT
return exporter.generateVCF(person, locale).toString()
}
This works (VcfGenerator has a set of JSF-only methods that use Faces.getLocale()), but is a pain to maintain. So instead of passing the Locale object, I'd like to say:
Vcard generateVCF(Person person) {
Locale activeLocale = LocaleProvider.instance().getContext(VcfGenerator.class)
ResourceBundle bundle = ResourceBundle.getBundle("messages", activeLocale, new MyControl())
// use bundle to construct the vcard
}
Has anyone done similar work and can share insights?
I know this has been posted a while ago, but as it has not been marked as resolved, here is how I got a workaround working for this specific case:
First I got a custom ResourceBundle working, as #BalusC described here: http://balusc.blogspot.fr/2010/10/internationalization-in-jsf-with-utf-8.html
Then I updated the constructor in order to detect if a FacesContext is currently being in use, from this :
public Text() {
setParent(ResourceBundle.getBundle(BUNDLE_NAME,
FacesContext.getCurrentInstance().getViewRoot().getLocale(), UTF8_CONTROL));
}
To This:
public Text() {
FacesContext ctx = FacesContext.getCurrentInstance();
setParent(ResourceBundle.getBundle(BUNDLE_NAME,
ctx != null ? ctx.getViewRoot().getLocale() : Locale.ENGLISH, UTF8_CONTROL));
}
This now works both in JSF and JAX-RS context.
Hope this help,

JSF Converter implementation

I have implemented Autoconverter (with forceSelection=false) in maintainance screen.
To edit existing record, User will select ID from Autocomplete list.
To add new record, user will enter new ID in same box.
In converter, Application will try to search record in DB using ID.
If not found, New empty object is created with supplied ID and to avoid duplications, this object is added to array list maintained in Converter.
This works as expected on single browser session. but while testing with multiple browser, I found that Array list is shared across all instances.
I am not sure whether approach I have taken is right? if not can you please suggest me an alternative approach.
private List<SchoolMasterDetails> schoolMasterDetailsDB = new ArrayList<SchoolMasterDetails>();
#Override
public Object getAsObject(FacesContext facesContext, UIComponent component, String submittedValue) {
SchoolMasterDetails selectedObject = null;
System.out.println("getAsObject ==> Entering.");
System.out.println("getAsObject ==> '" + submittedValue + "'");
if (!submittedValue.trim().equals("")) {
selectedObject = (SchoolMasterDetails) getMasterService().getSchoolbyCode(submittedValue);
if (selectedObject == null) {
// search Object on localDB
for (SchoolMasterDetails p : schoolMasterDetailsDB) {
if (p.getSchoolCode().equalsIgnoreCase(submittedValue.trim())) {
System.out.println("getAsObject from ArrayList ==> " + p);
return p; // return selectedObject from list of created objects
}
}
System.out.println("getAsObject ==> selectedObject is null, Hence Creating new Object");
selectedObject = new SchoolMasterDetails();
selectedObject.setSchoolCode(submittedValue.trim());
selectedObject.setSchoolName("TEST TEST TEST");
schoolMasterDetailsDB.add(selectedObject);
}
else {
System.out.println("getAsObject from Database ==> " + selectedObject);
}
}
System.out.println("getAsObject ==> " + selectedObject);
}
System.out.println("getAsObject ==> Exiting.");
return selectedObject;
}
Regards,
Shirish
As far as I understand this (still learning myself), a converter fulfills exactly one purpose: It prepares your custom objects to be used in the views (getAsString) and translates Strings back into objects (getAsObject). It will be used whenever an input (a radio list, textfield, autocomplete) is tied to a variable in a backing bean that is of the type of your custom object. It is in your freedom to decide what String should be used to represent your object and how you use this String in return to look up objects.
With this in mind I would not use a converter to store a local list of objects, nor let it handle the creation process itself. Instead, I'd assume there is a backing bean somewhere, which holds your data objects and takes care of all your logic. This bean can have a list of, say, schoolMasters that can be queried for the objects it contains (similar to what your doing). You could then either implement the lookup there in a way that it handles the not-found case and always returns a valid object (which may be a new one), or you could catch the not-found-case in the converter and then trigger a createNew() from the bean to get a new instance.
IMHO this separates the management of the instances more clearly from the translating purpose of your converter. Also, from your code, it seems like you have two places to look up objects - via getMasterService() (a local method?) and inside your stored ArrayList. I don't quite get this...
As for your problem with the browsers sharing an instance: This sounds like a scope issue. If your backing bean, which is supposed to store and manage your data, is in application scope then the same set of data will be available as long as the application runs. This data will be available across browsers and also across users.
On the other hand, if you put the bean in session scope, each session will create its own instance of the bean and store unique data. Similarly, view scoped beans live as long as a single view and request beans are trashed and regenerate for each http request. You can read more here: How to choose the right scope
The answers there talk about beans (which is where your data usually lives). I'm not sure about converters, I see them as classes that are available application wide, so that each session and view can use them for translation - if you maintain a list there, it may well be globally available.

JSF/Seam messages.properties without _xx which language is that?

i just wanted to know as which language the default messages.properties is read.
i thought that it is the in the faces-config.xml configured default locale is:
<locale-config>
<default-locale>de</default-locale>
<supported-locale>de</supported-locale>
<supported-locale>en</supported-locale>
</locale-config>
it contains no <message-bundle> tag,i created a messages.properties, messages_en.properties and messages_de.properties. To access the values i use this code
ResourceBundle resourceBundle = SeamResourceBundle.getBundle();
String bundleMessage = resourceBundle.getString("key.something");
In the menu i used this to show (and switch) the language what works fine
<h:selectOneMenu value="#{localeSelector.localeString}">
<f:selectItems value="#{localeSelector.supportedLocales}"/>
</h:selectOneMenu>
Now it doesn't matter what language i select, je always uses the messages.properties and not _de or _en. Do i need a concrete class for <message-bundle> to find also the _de and _en resource bundles?
EDIT:
ResourceBundle resourceBundle = SeamResourceBundle.getBundle();
java.util.Locale locale = resourceBundle.getLocale();
Contains always the correct locale de or en but always uses messages.properties and if this file is deleted, returns just the key as if he found no other file. The messages*.properties are in the /WEB-INF/classes folder.
i tried now to take Map<String, String> messages = org.jboss.seam.international.Messages.instance(); It contains also the values from messages.properties and not _de or _en
Using #{messages[key.label]} in the *.xhtml file also returns just the messages.properties values but not from _de or _en.
But a messages_de properties or _en directly in the xyz.war file with a <a4j:loadBundle var="i18n" basename="messages"/> does work. (thats how i did the i18n in the "not Java" frontend)
two more tries always return just the default properties and not _de or _en
resourceBundle = context.getApplication().getResourceBundle(context, "messages");
java.util.Locale locale = new java.util.Locale("de");
resourceBundle = ResourceBundle.getBundle("messages",locale);
if i create a new messages2_de.properties and *_en* and use the code above, everything works fine.
java.util.Locale locale = new java.util.Locale("de");
resourceBundle = ResourceBundle.getBundle("messages2",locale);
EDIT (Apparently JBoss Seam is a bit different)
As this document says, you probably should not instantiate bundles yourself.
Instead, you would define bundles you want to use and let Seam read message for you:
#In("#{messages['Hello']}") private String helloMessage;
Generally getBundle() method of any of ResourceBundle derived implementations will give you invariant bundle if you omit Locale parameter. This is by design.
If you need to access localized version, you need to get Locale from UIViewRoot:
Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
ResourceBundle resourceBundle = SeamResourceBundle.getBundle();
String bundleMessage = resourceBundle.getString("key.something");
I am not aware how your localeSelector bean is coded, but it too should set Locale in UIViewRoot.
Normally, the bundle without _xx is simply the bundle that is used if the key is not found in any of the more specific bundles for the current language.
Although I don't know what SeamResourceBundle exactly does, you do have to tell it somewhere what the 'current' language is. You say switching the language works, but what exactly do you do upon switching? At what point do you execute SeamResourceBundle.getBundle()?
Is key.something actually defined in all 3 bundles?
My bad. You couldn't find the error. The project had one messages.properties in /WEB-INF/classes and a second set(but without default properties) directly in the web content directory with the same names.
So i guess he took the only existing default messages.properties from the classes folder and the messages_de/en.properties from the web-content folder.

Resources