How many times have you Googled something, been directed to a website that you hope will provide the information you’re looking for, and find a website that’s so cluttered with information that it’s useless and you close the browser out of frustration?
Often you’ll find information on websites that isn’t relevant or that doesn’t pique your interest. For instance, if you’re a Java developer interested only in Java books, you probably found yourself losing interest when the Book Portal in chapter 5 showed books from other categories, like .NET and software engineering. Or imagine looking at a website that provides stock quotes for all stocks, regardless of which stocks are in your portfolio. In this case, you’re usually only interested in the prices or news related to your stocks.
The extraneous information on a portal or website not only has the potential to adversely affect the user experience, but it may also drive customers away to websites that provide personalized content.
It isn’t necessary for a portlet to provide some level of personalization to its users. For instance, an announcement portlet in a B2E (business-to-employee) portal may not support any personalization options, because the business’s announcements can’t be personalized based on employee preference. Nevertheless, when you’re determining the requirements for a portlet, it’s important to focus on the personalization aspects, because incorporating them will have an impact on your portlet code. Implementing personalization features in web applications can be a huge pain. Fortunately, the inherent support for personalization in Java portlet technology makes it easier to develop portlets that enrich the user experience by providing relevant content and personalized behavior.
Extra information is sometimes useful. Sometimes, when visiting a website, you’ll stumble upon some useful information that you weren’t looking for. The content of a website is generally designed around how best to serve its readers, based on usage patterns that have been observed over a period of time. For instance, when you look up a movie on IMDb (http://www.imdb.com/), it also displays recommended movies that you may be interested in.
In this chapter, we’ll look at personalization support in Java’s portlet technology and see how it can help you to quickly incorporate personalization features in your portlets. You’ll see how the Book Catalog portlet can be personalized to show books belonging to a particular category, like Java or .NET, and to prominently display books preferred by the users of the portlet, and so on.
We’ll begin this chapter with a quick introduction to personalization.
Personalization of content plays an important role in enriching the user experience. It lets your application’s users specify the content that they’re interested in, and the application in turn honors each user’s preferences. Application personalization isn’t limited to personalizing content but can also apply to customizing the behavior of the application. For instance, the Gmail portlet in the iGoogle portal allows users to specify the number of emails they want to view in the portlet (personalization of content) and whether they want to open a selected email in the same or a new browser window (personalization of behavior), as shown in Figure 10.1.
The Gmail portlet allows the user to select the number of messages that should be displayed when the portlet is in a normal window state. The portlet lets the user specify whether it should open email in the original Gmail web application and not within the portlet window; the portlet has limited features compared to the full Gmail web application.
The Java servlet technology isn’t designed to support the personalization of applications, so if a JSP- and servlet-based web application is required to support personalized content and behavior, it has to be taken care of in the design and implementation of the web application itself. For instance, a database designer will be responsible for creating a database structure for storing preferences; a developer will be responsible for writing code to save preferences in the database and read them again later.
If you’re using Java portlet technology, the portal server database provides the necessary structure to store user preferences for each portlet in the web portal. The portal server shields you from the database details of saving and retrieving the user preferences. The portal server reads the user preferences of the logged-in user from the database and makes them available to your portlets in the portlet request. As a developer, you’re only responsible for making use of the portlet API to retrieve user preferences and use them to personalize the content or behavior of the portlet.
The source code for the Book Catalog portlet discussed in this chapter is located in the ch10_BookCatalog folder. Before you build or deploy the portlet, make sure you change the value of the uploadFolder initialization parameter in the portlet deployment descriptor. Also set the liferay.portal.home property in the build.properties file to point to your Liferay Portal installation directory.
Now that you understand the basic concept of personalization, let’s dive deeper. First, let’s discuss the requirements of the Book Catalog portlet, which now include the personalization options. Once you understand the portlet’s personalization options, we’ll look at how they can be implemented in the portlet.
The Book Catalog portlet’s content and behavior is personalized according to the preferences set by the user. Figure 10.2 shows the preferences that users can set for this portlet.
The user can specify the category of books (like, Java and .NET) to view on the Book Catalog portlet’s home page (see Figure 10.3). The user can select multiple categories from the list; if no categories are selected, all are shown.
The user can specify whether the book search is case sensitive or case insensitive . In previous chapters, the Book Catalog portlet searched books in a case-sensitive manner. If the user doesn’t select the search type, it defaults to a case-sensitive search.
The user can specify the maximum number of books that should be displayed . The available options are 1000, 10, and 5. By default, a user will be shown all the books in the catalog.
The user can view a read-only option that says that the greeting message displayed on the home page of the Book Catalog has this format: “Hello! <username>”, where <username> is the name of the logged in user.
The user can enter the ISBN of the book that should be highlighted in the catalog display (see Figure 10.3). If there’s no book in the catalog corresponding to the ISBN entered here, an appropriate error message is displayed.
Clicking one of the Reset links resets the corresponding option to its default value. The Greetings Message personalization option doesn’t have a Reset link because it’s a read-only option.
The user can click Save to save the selected preferences.
Figure 10.3 shows the home page of the Book Catalog portlet, which displays the books in the catalog.
The book category information is displayed for each book in the catalog. If a user has chosen to view books belonging to certain categories on the preferences page, only books in those categories will be shown to the user. Because every book must be associated with a category, the book category information must be entered when adding a book to the catalog.
The book search will be either case sensitive or case insensitive depending upon the preference chosen by the user.
If the user has specified certain books as preferred books, these books will be highlighted in yellow in the catalog.
The greeting message format that was predefined for the Book Catalog portlet will be used to show the greeting message to the user.
The sample code for the Book Catalog portlet in this chapter is located in the ch10_BookCatalog folder of the book’s source code.
If your portlet needs to support personalization, you’ll typically follow these steps to implement personalization:
In the following sections, we’ll look at each of these steps in detail.
To show the personalization options in EDIT mode, you need to do the following:
Let’s look at how you can accomplish these tasks in the Book Catalog portlet.
If you recall from chapter 2, the purpose of EDIT portlet mode is to show personalization options for the portlet. Figure 10.2 shows the personalization options applicable to the Book Catalog portlet; those options should be generated in EDIT portlet mode.
The first thing that you need to do is add support for EDIT portlet mode to the Book Catalog portlet. You can define support for EDIT mode in the portlet deployment descriptor:
<portlet> <portlet-name>bookCatalog</portlet-name> <portlet-class> chapter10.code.listing.base.BookCatalogPortlet </portlet-class> <supports> <mime-type>text/html</mime-type> <portlet-mode>view</portlet-mode> <portlet-mode>edit</portlet-mode> </supports> </portlet>
The preceding portlet deployment descriptor defines support for EDIT portlet mode by using the <portlet-mode> subelement of the <supports> element. For more details on how to define support for portlet modes, refer to chapter 4.
Refer to the portlet.xml file in the ch10_BookCatalog folder to see how the <portlet-mode> element is used to support EDIT portlet mode in the Book Catalog portlet.
Now that the portlet supports EDIT portlet mode, let’s look at how you can show personalization options in EDIT mode.
In chapter 2 (section 2.5.1), I mentioned that there’s a render method for every portlet mode supported by a portlet. To display the personalization options, you need to write a render method in your portlet class corresponding to EDIT mode.
The following code snippet shows the render method for EDIT mode:
@RenderMode(name = "edit") public void showPrefs(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher( response.encodeURL("/WEB-INF/jsp/preferences.jsp") ); dispatcher.include(request, response); }
The showPrefs method in the preceding snippet is a render method in EDIT mode (specified by the @RenderMode annotation) that makes use of PortletRequestDispatcher to dispatch portlet requests to the preferences.jsp page. The preferences.jsp page shows all the personalization options applicable for the Book Catalog portlet, as shown in Figure 10.2. This means that generating content in EDIT (or HELP) portlet mode is no different than in VIEW mode; the purpose of the content varies but not how it’s generated.
Refer to the BookCatalogPortlet class’s showPrefs method in the ch10_BookCatalog folder to see how the @RenderMode annotation is used to mark a method as a render method for EDIT portlet mode.
Listing 10.1 shows the portion of the preferences.jsp page that shows the Preferred Category and Search Type options to the user.
Refer to the preferences.jsp page in the ch10_BookCatalog folder to see how other personalization options of the Book Catalog portlet are specified in the JSP page.
The JSP page shows the preferred book category options (Java, .NET, Mobile Technology, and Software Engineering) in a multiselect list . The book search options (case-sensitive or case-insensitive) are displayed in an HTML combo box .
Some of the details have been removed from the preferences.jsp page in listing 10.1 in order to focus on how the preferences are displayed by a JSP page. Later in this chapter, you’ll see a more refined preferences.jsp page that shows how preference values that have been set by a user are displayed by the page.
The next step is to capture the portlet preferences specified by the user and save them in a persistent store.
The process of saving portlet preferences selected by the user can be divided into two steps:
1. Retrieve portlet preferences selected by the user
2. Save preferences in the persistent store using the PortletPreferences object
Let’s look at each of these steps in detail in the context of the Book Catalog portlet.
You can retrieve preferences selected by the user from the portlet request as request parameters. In the Book Catalog portlet, when the user clicks Save on the preferences page (see Figure 10.2), an action request is dispatched to the portlet. The selected preference category and search type values are available to the portlet class like any other HTML form fields.
The next listing shows how the BookCatalogPortlet class’s savePreferences action method retrieves the preferences selected by the user.
The savePreferences method retrieves the preferred categories chosen by the user . The categories are represented as a multiselect list, so the ActionRequest’s getParameterValues method is used to retrieve all the selected categories. If you were to use the getParameterValue method instead of getParameterValues, it would return only the first value of the prefCategory request parameter.
If a request parameter in your portlet is expected to contain multiple values, you should use the PortletRequest’s getParameterValues method to retrieve it in the portlet class. You can use the getParameterValue method for single-valued request parameters.
The savePreferences method retrieves the search type (case-sensitive or case-insensitive) chosen by the user . The search type is represented as an HTML combo box from which only one value can be selected, so the ActionRequest’s getParameterValue method is used to retrieve the selected search type.
Refer to the BookCatalogPortlet class’s savePreferences method in ch10_BookCatalog to see how other personalization options are retrieved from the request.
Now that you’ve retrieved the preferences selected by the user, it’s time to save the preferences in a persistent store.
Preferences set for a portlet need to live longer than the user’s session; they must be saved in a persistent store for future use. When the user comes back to your portal later, the portlets should show content based on the preferences that the user set earlier.
Does saving the preferences in a database or other persistent store mean that you have to create a database and table structure to store preferences? Does it mean that you have to write data access code to save and retrieve preferences from the database? If you’re using Java portlet technology, you don’t need to worry about databases, table structures, and data access layers. You only need to know the Portlet 2.0 API in order to save portlet preferences.
The following listing again shows the savePreferences action method of the Book Catalog portlet. It uses the Portlet 2.0 API to save preferences in a persistent store (such as a database, flat file, and so on).
The savePreferences method uses the ActionRequest’s getPreferences method to obtain the PortletPreferences object. We’ll discuss this object later in the chapter, but for now you just need to know that it’s the object provided by the portlet container to help portlets write and read preferences to and from a persistent store.
The PortletPreferences object’s setValues method is used to set the values for the preferred book categories selected by the user.
You set the user’s search type preference in the PortletPreferences object using the setValue method. Note the use of the setValue method instead of setValues.
You call the PortletPreferences object’s store method to save the preferences that have been set so far into a persistent store.
Refer to the BookCatalogPortlet’s savePreferences method in the ch10_BookCatalog folder to see how all the preferences are set in the PortletPreferences object, and how they’re saved in the end.
Listing 10.3 shows how you’ll usually save preferences in the persistent store. There’s no need to write any data access code or create any table structures to persist user preferences. So where is the preference information getting saved? You saw in chapter 9 that portal servers are backed by a database (referred to as the portal database) that contains, among other things, information about the users of the portal server and the portlets deployed on the portal server. This database also contains table structures to store portlet preferences set by users. The PortletPreferences object, which is provided by the portal server to your portlets, knows how to save portlet preferences into the portal database or any other persistent store; it’s responsible for calling appropriate data access objects on the portal server to save the preferences.
Let’s look at the PortletPreferences object in detail to understand its purpose and methods.
You saw in the previous section that the PortletPreferences object is used to save user preferences into a persistent store. The PortletPreferences object is obtained by the portlet request’s getPortletPreferences method, and it defines methods to retrieve, set, and store preferences.
In most situations, portlet developers treat the portal database as a black box, not worrying about how preferences are stored to or retrieved from it. The Portlet API shields portlet developers from details of the portal database by providing the PortletPreferences object, which is implemented by portal server vendors and encapsulates the logic for retrieving and storing preferences from and to the portal database.
In this section, we’ll focus on saving preferences to a persistent store using the PortletPreferences object. In section 10.7, we’ll discuss how you can obtain saved preferences from the PortletPreferences object.
Table 10.1 shows some of the important methods in the PortletPreferences interface and describes them briefly.
Method |
Description |
---|---|
String getValue(String key, String default) | Returns the first String value associated with the preference identified by key. If the preference isn’t found, it returns default as the value of the preference. |
String[] getValues(String key, String[] defaults) | Returns the String values (as an array) associated with the preference identified by key. If the preference isn’t found, it returns defaults as the value of the preference. |
void setValue(String key, String value) | Sets the preference identified by key with a value identified by the value argument in the PortletPreferences object. |
void setValues(String key, String[] values) | Sets the preference identified by key with an array of values identified by the value argument in the PortletPreferences object. |
void store() | Saves preferences set in the PortletPreferences object to a persistent store. |
void reset(String key) | Resets the value of the preference identified by key to its default value. |
boolean isReadOnly(String key) | Checks whether the preference identified by key is read-only or modifiable. |
You saw in the previous section how you can use the setValue, setValues, and store methods of the PortletPreferences object. Later in this chapter, we’ll discuss the use of the getValue and getValues methods to retrieve preferences to either personalize portlet content (and behavior) or to display the preferences already set by the user on a JSP page. For now, we’ll discuss the two most interesting methods of PortletPreferences: the store and reset methods.
The PortletPreferences object’s store method persists the preferences that you had previously set using the setValue and setValues methods. If you set preferences on the PortletPreferences object after calling the store method, those preferences won’t be persisted. To make this point clear, the next listing shows an example in which preferences are set after calling the store method.
In listing 10.4, the pref3 and pref4 preferences are set after the store method is invoked; the pref3 and pref4 preferences aren’t saved in the persistent store by the portal server.
If you don’t call the store method after setting the preferences, the preferences aren’t saved to the persistent store.
The store method has two key features: atomicity and consistency. Let’s look at atomicity.
The store method doesn’t return a success or failure flag to the calling portlet, but it’s guaranteed to be atomic; the store method either successfully saves all the preferences in the persistent store or none.
The store method call can fail because the persistent store is unavailable, or the values of the preferences don’t match the criteria set by the PreferencesValidator (discussed in section 10.6). The portal server or portlet container is responsible for ensuring that saving preferences with the store method is always an atomic operation.
If multiple threads attempt to concurrently write portlet preferences into the persistent store, this may result in data inconsistency. Concurrency issues are handled by the store method implementation provided by the portal server or portlet container.
I mentioned in chapter 2 that a portlet’s render method (in any portlet mode) shouldn’t be used to change the portlet state. The PortletPreferences object’s store method follows this rule strictly, and if you invoke the store method in a portlet’s render method, in any portlet mode, it will result in an exception.
Let’s now look at the PortletPreferences interface’s reset method, which allows you to reset previously set preferences.
Figure 10.2 shows that the Book Catalog portlet includes Reset hyperlinks to allow users to reset the preferences to default values.
You can reset a preference by calling the PortletPreferences object’s reset method, as shown here:
prefs.reset("category");
Here, prefs is the PortletPreferences object, and category identifies the preference that needs to be reset.
What happens when you call the reset method? If the preference that you’re resetting has default values, the value of the preference is set to its default. If the preference doesn’t have default values, the preference is simply removed from the PortletPreferences object.
The portlet specification doesn’t specify any mechanism to define default values for portlet preferences. What the default value for a preference is and where it needs to be retrieved from depends upon the portlet container implementation. If you define your portlet preferences with initial values in the portlet deployment descriptor (discussed later in this chapter), the reset method considers the values defined for the preference in the portlet deployment descriptor to be the default values.
The only way to remove a preference from the PortletPreferences object is to use the reset method (assuming that no default values exist for the preference). You might think that using the setValue or setValues methods could also remove a preference, but this isn’t the case; even the value null is considered a valid value for a preference.
If a preference is removed from the PortletPreferences object, the getValue and getValues methods will return the default value specified in these methods. If the preference was set to null using the setValue or setValues method, the getValue and getValues methods will return null as the return value.
Let’s look at the following code listing, which shows the effect of using the reset and setValue methods to remove a portlet preference.
The value of the pref1 preference is set to null and pref2 is set to someValue using the setValue method . The value of the pref2 preference is reset using the reset method . The values of the pref1 and pref2 preferences are retrieved using the getValue method .
Refer to the BookCatalogPortlet’s resetPreference method in the ch10_BookCatalog portlet to see the PortletPreferences object’s reset method in use. The Reset links in the Book Catalog portlet send the name of the preference to be reset as a request parameter. The Book Catalog portlet’s resetPreference method retrieves the name of the preference from the portlet request and resets the preference.
In listing 10.5, the returned value of pref1 is null because null is a valid value for a preference. The returned value of pref2 is -1 because the reset method removed the pref2 preference from the PortletPreferences object (there were no default values found for pref2).
To remove a preference, don’t set its preference value to null by calling the setValue method. Instead, use the reset method to reset or remove the preference from the PortletPreferences object.
When you use the reset method, the change in the preference isn’t persisted automatically; you still need to call the PortletPreferences object’s store method to persist the changes.
Validating information entered by a user is an important aspect of any application design. Applications are designed on the assumption that the data available from the data store will be consistent and that the application will handle any known inconsistencies that may exist in the data. Because preferences are saved in the data store and are later used to customize portlet content or behavior, the preferences entered by the user may need to be validated before they’re saved in the persistent store.
Let’s look at how preferences entered by users can be validated.
Earlier in this chapter, you saw how to retrieve preferences selected by the user from the request and set these preferences in the PortletPreferences object. The next logical step is to validate these preferences before saving them in the portal database (by calling the store method).
For instance, in the Book Catalog portlet, you need to confirm that the preferred ISBN entered by the user exists in the catalog before saving it. There are several benefits of validating the ISBN:
In your portlet, you can validate the user preferences at two places during request processing:
Let’s look at both of these approaches.
This approach for validating preferences requires that you write validation logic in your portlet class. The following listing shows the Book Catalog portlet’s savePreferences method, which validates the user’s preferred ISBN before calling the PortletPreferences object’s store method.
First, you create a java.util.Map to store the error keys and error messages for validation errors. Then you obtain a reference to the BookService that’s later used to verify whether the book corresponding to the user-entered ISBN exists in the catalog, and you also obtain the ISBN number from the action request.
You check to make sure that the ISBN number entered by the user isn’t blank and that it exists in the catalog . Then, if it exists in the catalog, you add the ISBN number to the PortletPreferences object with the prefBook key . If the entered ISBN number isn’t valid or if it doesn’t exist in the catalog, you add an error key and message to the errorMap.
If there are no validation errors, you call the PortletPreferences object’s store method to save the user preferences in a persistent store.
In listing 10.6, you can store errorMap as a request attribute and retrieve it in the render method of your portlet class (assuming that your portlet uses the javax.portlet.actionScopedRequestAttributes container-runtime option) to show the error messages on your JSP page. Alternatively, you can set a generic error message using the ActionResponse’s setRenderParameter method and use the render parameter value to show the error message on your JSP page.
This validation approach works well, but it clutters the code in your portlet class, which affects code readability. Most web frameworks provide abstract validation classes, or interfaces, that are implemented by the application code to keep validation logic outside of the request-processing logic. Similarly, the Portlet API provides the PreferencesValidator interface, which helps keep your preference-validation logic outside your portlet class.
Let’s look at how to validate preferences using the PreferencesValidator interface.
The PreferencesValidator interface of the Portlet API offers the benefit of keeping your preference-validation logic outside of your portlet class.
Preference validation is different from business validation. Business validation is done in the portlet class or in a class that extends or implements the class or interface of the portlet framework that you’re using to create portlets. For instance, in chapter 8, the MyValidator class of the Book Catalog portlet implements Spring’s Validator interface to perform business validations.
This listing shows the BookCatalogPrefsValidator class, which implements the PreferencesValidator interface and provides the preference-validation logic.
The BookCatalogPrefsValidator implements the Portlet API PreferencesValidator interface. PreferencesValidator defines the validate method, which is implemented by the BookCatalogPrefsValidator class . The validate method retrieves the preferred book’s ISBN from the PortletPreferences object and uses BookService to check whether a book exists in the catalog with that ISBN. If no book matching the ISBN is found in the catalog, the class throws a ValidatorException .
See the BookCatalogPrefsValidator class in ch10_BookCatalog for the complete implementation of the preferences validator used by the Book Catalog portlet.
We’ll discuss how to configure BookCatalogPrefsValidator for the Book Catalog portlet later in this section. For now, you can assume that the validate method of BookCatalogPrefsValidator is called internally by the PortletPreferences object’s store method. In listing 10.7, the ValidatorException is a Portlet API exception class that must be thrown by the validate method of your validator class if validation fails. ValidatorException acts as an indicator to the store method that the validation of one or more preferences has failed for some reason, and that it must not save the preferences in the persistent store.
BookCatalogPrefsValidator makes use of the BookService class to invoke the isUniqueISBN method. If you’re using the Spring Portlet MVC framework, a class like BookService is configured in the portlet application context XML file with a dependency on a data access object (DAO). In such cases, you can configure the preferences validator class in the portlet application context XML file (instead of portlet.xml), autowire dependencies, and programmatically validate preferences by invoking the validate method. Alternatively, you can use Spring’s @Configurable annotation along with load- or compile-time weaving to autowire dependencies into the preferences validator instance that is not created by Spring.
The ValidatorException class defines multiple constructors, and the one used in listing 10.7 accepts two parameters:
The collection of preference keys can be used by the portlet class to display an appropriate error message. In listing 10.7, the exception message is “ISBN number does not exist in catalog,” and the collection of failed preferences keys contains a single entry: prefBookISBN.
It’s recommended that, if a preference fails validation, you keep the name of the failed preference key the same as the name of the preference key in the PortletPreferences object. For instance, in listing 10.7, the name of the preference key in the PortletPreferences object is prefBookISBN; if the validation fails for this preference, the same name is added to the collection of failed preferences keys that’s passed as an argument to the ValidatorException constructor.
In your validator class, you have the option to throw a ValidatorException as soon as validation fails for a preference, or you can keep adding keys of failed preferences to the collection and throw the ValidatorException at the end, as shown here:
List<String> failedKeys = new ArrayList<String>(); if(...) { failedKeys.add("pref1"); } if(...) { failedKeys.add("pref2"); } if(failedKeys.size() > 0) { throw new ValidatorException("Exception",failedKeys) }
The preceding code continues adding keys of preferences that failed validation to the failedKeys list, and it throws a ValidatorException after all the preferences have been validated.
Later in this chapter, you’ll see how BookCatalogPortlet makes use of this collection of keys of preferences that failed validation in order to show appropriate validation error messages to the user.
The BookCatalogPrefsValidator class that you created earlier must be configured in the portlet deployment descriptor to instruct the portlet container to use it to validate portlet preferences. The following listing shows how BookCatalogPrefsValidator is configured in the portlet deployment descriptor.
In this listing, the <preferencesvalidator> subelement of the <portletpreferences> element is used to define the custom PreferencesValidator class that will validate preferences for the bookCatalog portlet. It’s important to note that you must specify the fully qualified class name of your custom PreferencesValidator class.
Refer to the portlet.xml file in the ch10_BookCatalog folder to see the configuration of the preferences validator in the Book Catalog portlet.
Now that all the validation code is in BookCatalogPrefsValidator, the BookCatalogPortlet’s savePreferences method (shown earlier in listing 10.6) contains no preferences validation logic, as you can see here.
First you check whether the user has entered the ISBN for a preferred book or not; if so, you set it in the PortletPreferences object . You surround the call to the store method with a try-catch block . If you’re using a custom PreferencesValidator to validate preferences, then it’s important to catch any ValidatorException in your portlet class to show appropriate validation error messages to the user. You add the collection of failed preference keys as a request attribute .
You must not call the PortletPreferences object’s store method from inside the validate method of the PreferencesValidator class because the portlet container constrains the validate method to perform validation only.
In listing 10.9, there’s a reason for checking the ISBN for a blank value before setting it in the PortletPreferences object. BookCatalogPrefsValidator is responsible for validating the preferences set in the PortletPreferences object, and should not be used to modify or change them in any way.
Let’s see what BookCatalogPrefsValidator would look like if you set the ISBN directly in the PortletPreferences object without doing a check for a blank entry. The modified BookCatalogPrefsValidator is shown next.
BookCatalogPrefsValidator checks whether the prefBookISBN preference is empty or null. The validator attempts to remove the prefBookISBN preference from the PortletPreferences object using the reset method, because saving blank or null as the value of the preferred book’s ISBN is invalid.
In listing 10.10, BookCatalogPrefsValidator isn’t only validating preferences; it’s also modifying the value of the prefBookISBN preference, which isn’t recommended. This is similar to using validators in the Spring or Struts frameworks; validators shouldn’t change the attributes of the object being validated.
If your portlet offers many personalization options, you may have multiple personalization pages, with each page showing a subset of options. In such cases, the validation code in PreferencesValidator can become cluttered. You can consider creating multiple PreferencesValidator classes and invoking them programmatically from your portlet class.
Figure 10.4 summarizes the role played by PreferencesValidator in portlet request processing.
The action request for the BookCatalogPortlet is triggered when the user clicks Save (shown in Figure 10.2) to save the preferences.
The BookCatalogPortlet class’s savePreferences action method is invoked by the portlet container, which is responsible for saving preferences in the portal database. Then, the savePreferences method invokes the setValue or setValues method of PortletPreferences to set the preferences from the request parameters.
The savePreferences method calls the PortletPreferences object’s store method. The store method internally calls the BookCatalogPrefsValidator’s validate method, which in turn validates the preferences set in PortletPreferences. If the validate method doesn’t throw a ValidateException or any other exception, the store method saves the preferences in a persistent store . After this point, the normal portlet request-processing lifecycle continues.
Let’s now take a look at how you can show a customized exception message when a preference fails validation.
You may recall that the PreferencesValidator throws a ValidatorException (see listing 10.7) which is then caught by the portlet class (see listing 10.9) to display a generic exception message to the user. ValidatorException is limited to providing a collection of preference names indicating which ones failed validation, but that isn’t sufficient to show a user-friendly exception message in the portlet. In this section, we’ll discuss an approach that will allow you to show a user-friendly exception message in the Book Catalog portlet.
Figure 10.5 shows a custom exception message displayed by the Book Catalog portlet when the prefBookISBN preference fails validation: “Please enter a valid value for the ‘Preferred ISBN Number’ preference”.
A custom error message is shown to the user when the portlet fails to find a book with the ISBN entered by the user in the Preferred Book ISBN Number field. Let’s see how the Book Catalog portlet manages to show a custom error message when validation fails for a preference.
The next listing shows how the BookCatalogPortlet class makes use of the failed preferences names and keys, received with ValidatorException, to show a user-friendly message.
First, you create an empty list in which you can store validation error messages. The store method of PortletPreferences is invoked . The preference name or key returned by ValidatorException is obtained using its getFailedKeys method.
The error message corresponding to a failed preference is obtained from the resource bundle. The name with which the error message is stored in the resource bundle follows this format:
javax.portlet.preference.<preference-name>.error
Here, <preference-name> is the name or key of the preference that failed validation. The error message in the resource bundle contains a placeholder for a user-friendly preference name or key, as shown here:
javax.portlet.preference.prefBookISBN.error = Please enter a valid value for the '{0}' preference
The placeholder for the preference name is identified by '{0}'.
Then you obtain the name of the preference from the resource bundle. The name of the preference follows this format:
javax.portlet.preference.<preference-name>.name
Here, <preference-name> is the name or key of the preference that failed validation. Then, with errorMessage.replace, you replace the preference name placeholder in the error message with its user-friendly name.
Finally, you add all the error messages to the request, which is then used by the JSP page to display the error messages.
Listing 10.11 shows one of the many possible ways you can display user-friendly validation error messages, and it’s sufficient in most portlet development scenarios.
Phew! We’ve made it through adding personalization support, validating preferences, and saving portlet preferences. We’ve covered a lot of ground, but it wasn’t too complicated. Stick with me for a few more pages and you’ll learn how to retrieve saved preferences and use them to customize the content and behavior of your portlets.
Here’s the good news: you don’t need to do anything in your portlet implementation class to retrieve preferences from the persistent store. Really!
Let’s look at how you can access preferences in your portlet class and in the JSP pages included by the portlet. Later in this section, we’ll look at how portlet preferences can be used to personalize content and behavior.
The Book Catalog portlet’s category preference attribute identifies the user’s preferred book category, like .NET, Java, or Software Engineering. (Listing 10.3 shows that the category preference is set in the BookCatalogPortlet class’s savePreferences method.) If the user had earlier saved .NET and Java as their preferred book categories, the user’s preference page (shown in Figure 10.2) should show the Java and .NET values selected in the list box. That means you need to retrieve the preferences from the persistent store in order to display them.
Like when saving preferences, you don’t need to know anything about the database or persistent store where saved preferences are kept. When your portlet’s request-processing methods, like render, serveResource, processAction, and processEvent, are invoked, the portlet container is responsible for reading the current values of the portlet preferences from the persistent store and making them available in the PortletPreferences object.
The following code snippet shows how you can obtain a PortletPreferences object in a portlet’s render method:
@RenderMode(name = "edit") public void showPrefs(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletPreferences prefs = request.getPreferences(); ... }
In the preceding code snippet, the PortletRequest’s getPreferences method is used to obtain the PortletPreferences object. When you retrieve the PortletPreferences object with any of the request-processing methods, the portlet container guarantees it will provide saved preferences in the PortletPreferences object. If you had defined preferences with initial values in the portlet deployment descriptor, those preferences may also be available in the PortletPreferences object. Later in this chapter, we’ll discuss how to define preferences in the portlet deployment descriptor, but for now you can assume that if a preference is defined in the portlet deployment descriptor for which no values exist in the persistent store, the PortletPreferences object picks the preference and its values from the portlet deployment descriptor.
For instance, consider the following code snippet, which retrieves the pref1 preference:
@RenderMode(name = "edit") public void showPrefs(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletPreferences prefs = request.getPreferences(); String value = prefs.getValue("pref1", "-99"); ... }
In the preceding code, the value of the pref1 preference attribute depends upon whether the preference was earlier saved in the persistent store, whether it’s defined in the portlet deployment descriptor, and whether the persistent store is currently available. Let’s look at the pref1 preference attribute’s value in those different situations:
Now that you know where the preference values come from, let’s get back to our initial goal of showing a user’s preferences on the preferences page. You can either retrieve preferences in the render method of EDIT mode, as described above, or directly in the JSP page by using the portletPreferences or portletPreferencesValues scripting variables introduced by the defineObjects tag of portlet tag library (portlet tag library tags were discussed in chapter 6). In most cases, directly retrieving portlet preferences in the JSP is sufficient for showing preferences.
The portletPreferences scripting variable represents a PortletPreferences object, and portletPreferencesValues represents a Map<String, String[]) object that contains a map of portlet preference names and values. You can use either of the two scripting variables in your JSP page to show preferences.
In listing 10.1, you saw how the preferences.jsp page of the Book Catalog portlet displays personalization options. Let’s now modify the preferences.jsp page to highlight the preferences that were already saved in the persistent store. The modified page is shown here.
This listing makes use of scripting variables to show current preference values for the Preferred Category and Greetings Message preferences that apply to the user. First, you use the defineObjects tag in the JSP page to introduce scripting variables, which makes it easy to develop included and forwarded JSP pages in portlets. You retrieve values for the category preference using the portletPreferences scripting variable ; then you show a book category as selected if it represents one of the values of the category preference. You show the value of the greetingMessage preference using the portletPreferencesValues scripting variable.
So far in this chapter, you’ve learned about saving and retrieving preferences to and from the persistent store. The real value of the preferences shows up when you use them to customize the content and behavior of your portlet. In this section, we’ll look at how the Book Catalog portlet makes use of preferences to customize its behavior and content.
Table 10.2 revisits the preferences defined at the beginning of this chapter and describes their effect on the content and behavior of the Book Catalog portlet.
Preference |
Effect on content and behavior |
---|---|
Preferred Category | Allows a user to personalize the Book Catalog portlet to show books from specified categories. For instance, if the user has chosen to view books in the Java and .NET categories, only books related to those categories are shown to the user when they visit the portal page. |
Search Type | Allows a user to choose between case-insensitive and case-sensitive searches for books. |
Maximum Number of Books to Show | Allows a user to specify the number of books that will be shown in the Book Catalog portlet. By default, the Book Catalog portlet shows up to 1000 books on the page. |
Greetings Message | Represents the greetings message that’s shown to the user on the portlet’s home page. It can’t be changed by the user and is shown as a read-only option. |
Preferred Book ISBN Number | If the user specifies one or more preferred books, the books are shown highlighted, on a yellow background, on the home page of the Book Catalog portlet. |
In this section, we’ll look at how content is personalized based on the Preferred Category preference and how behavior is personalized based on the Search Type preference. The rest of the preferences in the Book Catalog portlet are implemented along similar lines.
The personalization of content affects what information is displayed by the portlet. The personalization of behavior deals with how the portlet behaves in response to user actions.
The Book Catalog portlet either displays books from user-selected categories or it shows books from all categories. The next listing shows how the showBooks method of the BookCatalogPortlet class personalizes catalog content based on the book category.
The showBooks method retrieves the PortletPreferences object from the portlet request. Then you obtain the values of the category preference as a String[], because there can be multiple values associated with this preference. You check whether the category preference contains any values. If it contains a single value of -99, that means that there are no values associated with the category preference.
All the books are obtained from the catalog if no valid value is found for the category preference. The BookService class’s getBooks method returns all books in the catalog, irrespective of the category to which they belong.
If valid category values exist , you obtain books belonging to those categories using the BookService’s getBooksByCategories method. You set the books that were obtained , either by the getBooks or getBooksByCategories method, as a request attribute.
The home JSP page of the Book Catalog portlet is then responsible for using the books stored in the request attribute to display the list of books in the catalog. The list may represent all the books in the catalog or the set of books that belong to the user’s preferred categories.
Refer to the BookCatalogPortlet class’s showBooks method in the ch10_BookCatalog folder to see how the Book Catalog portlet makes use of the category preference attribute to personalize content.
Listing 10.13 highlights some of the important decisions that you may have to make while writing code that uses preference information to customize portlet content or behavior. Here are two of these decisions:
The source code in the ch10_BookCatalog folder shows how the Book Catalog portlet customizes content. The home.jsp page shows how content is personalized based on the Maximum Number of Books to Show and Greetings Message options. The <c:forEach> tag used in home.jsp puts a limit on the number of books to display by using its end attribute. It also uses the portletPreferencesValues scripting variable to read the value of the greetingMessage preference from the portlet.xml file to show the greeting message. Later in this chapter, we’ll discuss how you can define preferences in the portlet deployment descriptor.
You can personalize the behavior of a portlet in the same way that you personalize portlet content. The Search Type preference of the Book Catalog portlet lets a user choose between case-sensitive and case-insensitive book catalog searches; instead of affecting the content displayed by the portlet, it affects the portlet’s behavior.
The following listing shows how the searchBooks method of the Book Catalog portlet personalizes search behavior based on the value of the Search Type preference.
The implementation of the searchBooks method here is similar to the showBooks method in listing 10.13: both methods retrieve portlet preferences and personalize the content or behavior of the portlet. The BookCatalogPortlet class’s searchBooks action method obtains the PortletPreferences object from the request. You retrieve the value of the searchType preference , which identifies the search type: case-sensitive or case-insensitive, with the default being case-sensitive. The BookService class’s searchBooks method is invoked, and it’s passed the user’s search type preference. The searchBooks method makes use of the search type preference to perform a case-sensitive or case-insensitive search.
So far in this chapter, you’ve seen examples in which portlet preferences are defined in the portlet class. Let’s now look at how you can define preferences in the portlet deployment descriptor.
Earlier in this chapter, you saw how you can define preferences on the fly, in the portlet code itself. Portlet preferences are similar to any other configuration option in the portlet.xml file—defining portlet preferences in portlet.xml informs the portlet deployer about the preferences supported by the portlet and their initial values.
You can define portlet preferences in the portlet deployment descriptor using the <preference> subelement of the <portletpreferences> element. The following listing shows how the Book Catalog portlet defines the maxNumOfBooks and greeting-Message preferences in the portlet.xml file.
First you define a preference using the <preference> element . The <name> element is used to specify a name for the preference , and <value> elements are used to define initial values for the preference . The <read-only> element is used to mark a preference as read-only .
If you define portlet preferences in the portlet.xml file, not only does this increase the maintainability of portlet preferences, but it also provides the following additional features:
Defining preferences in the portlet deployment descriptor has the added advantage that it informs portlet deployers about various personalization options available for the portlet. It’s recommended that you define preferences in the portlet deployment descriptor, with initial values, to clearly indicate the preferences that are available for the portlet.
In this chapter, you saw just how easy it is to personalize portlets. You saw that the PortletPreferences object of Portlet 2.0 is at the heart of portlet personalization, because it lets you transparently save and retrieve user preferences.
If you liked the concept of preferences, you’re going to love inter-portlet communication, which is one of the most exciting features of portlet technology. That’s what the next chapter is about.
18.216.47.169