JSF2 navigation: post->redirect->get

0 Flares 0 Flares ×

JSF2 improves a lot both how navigation can be done (you can now return a view id from an action method, no need to describe every navigation case in faces-config.xml) and how URLs are handled (finally, GET support). JSF2 introduces view parameters (for those who know Seam: standardized page parameters). Each page can define a metadata section, where the view parameters are described, bound to bean values, converted and validated.

As an example, a blog-entry-viewing page would define the id of the entry to be displayed as follows:

1
2
3
4
5
<f:metadata>
   <f:viewParam name="entry_id" value="#{blog.entry}" required="true">
      <f:converter converterId="blog-entry-converter" />
   </f:viewParam>
</f:metadata>

Unfortunately I had some trouble with one thing: how to redirect to a page, including the view parameters, after a POST? This post->redirect->get pattern is very common. E.g. when you post a new comment for a blog entry and press submit, the data is persisted and you want to be redirected back to view.jsf?entry_id=819 (using the default JSF command-button behavior, you would land on a plain, non-bookmarkable view.jsf).

Dan Allen wrote a series of very good introductory articles to JSF2 on DZone. There, he writes that what I described above should be possible to achieve by adding a <redirect include-view-params="true"/> tag to the appropriate <navigation-case> in faces-config.xml. Unfortunately, the xsd doesn’t allow such an attribute and it doesn’t work – I suppose that this construct didn’t make it into the final version of the spec (although somebody may correct me if I’m wrong).

Another solution, this time working, can be found on Ed Burns’s blog. The trick is to return a string containing the view id and some additional parameters from the action method or use them as the command button/link action, e.g.:

1
2
3
4
public String action() {
     // business logic ...
     return "view.xhtml?faces-redirect=true&includeViewParams=true"
}

However this way you’ll have to repeat the combination of the “magical parameters” a lot in your code. And it’s pretty easy to do a spelling mistake in one of the strings you return. Furthermore, it’s not possible to easily include one view parameter, without repeating the value mapping.

The way I solved this is by introducing a Nav component (I’m using Weld), which holds information about pages. It contains a nested Page class, which has a “fluent” interface for building a link. Navigation then looks as follows:

1
2
3
4
5
6
7
@Inject
private Nav nav;
 
public String action() {
     // business logic ...
     return nav.getViewEntry().redirect().includeViewParams().s();
}

Or, if you want to include only one parameter:

1
2
3
4
public String action() {
     // business logic ...
     return nav.getViewEntry().redirect().includeViewParam("name").s();
}

In xhtml pages, you can also use the nav component to generate links:

1
<h:link outcome="#{nav.manageIndex.s}">Manage</h:link>

Notice that you completely abstract away from the actual names of the xhtml views (pages) – they are stored centrally only in the nav component! This makes any refactorings really easy.

Speaking of the nav component, here’s the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
 * @author Adam Warski (adam at warski dot org)
 */
@Named
@ApplicationScoped
public class Nav {
    public static class Page {
        private final String viewId;
        private final Map<String, String> params;
 
        private Page(String viewId) {
            this.viewId = viewId;
            this.params = new LinkedHashMap<String, String>();
        }
 
        private Page(String viewId, Map<String, String> params) {
            this.viewId = viewId;
            this.params = params;
        }
 
        public Page redirect() {
            return includeParam("faces-redirect", "true");
        }
 
        public Page includeViewParams() {
            return includeParam("includeViewParams", "true");
        }
 
        public Page includeViewParam(String name) {
            // Getting the metadata facet of the view
            FacesContext ctx = FacesContext.getCurrentInstance();
            ViewDeclarationLanguage vdl = ctx.getApplication().getViewHandler()
                  .getViewDeclarationLanguage(ctx, viewId);
            ViewMetadata viewMetadata = vdl.getViewMetadata(ctx, viewId);
            UIViewRoot viewRoot = viewMetadata.createMetadataView(ctx);
            UIComponent metadataFacet = viewRoot.getFacet(
                  UIViewRoot.METADATA_FACET_NAME);
 
            // Looking for a view parameter with the specified name
            UIViewParameter viewParam = null;
            for (UIComponent child : metadataFacet.getChildren()) {
                if (child instanceof UIViewParameter) {
                    UIViewParameter tempViewParam = (UIViewParameter) child;
                    if (name.equals(tempViewParam.getName())) {
                        viewParam = tempViewParam;
                        break;
                    }
                }
            }
 
            if (viewParam == null) {
                throw new FacesException("Unknown parameter: '" + name + 
                     "' for view: " + viewId);
            }
 
            // Getting the value
            String value = viewParam.getStringValue(ctx);
            return includeParam(name, value);
        }
 
        public Page includeParam(String name, String value) {
            Map<String, String> newParams = new LinkedHashMap<String, String>(params);
            newParams.put(name, value);
            return new Page(viewId, newParams);
        }
 
        public String s() {
            StringBuilder sb = new StringBuilder();
            sb.append(viewId);
 
            String paramSeparator = "?";
            for (Map.Entry<String, String> nameValue : params.entrySet()) {
                sb.append(paramSeparator).append(nameValue.getKey())
                      .append("=").append(nameValue.getValue());
                paramSeparator = "&";
            }
 
            return sb.toString();
        }
 
        public String getS() { return s(); }
    }
 
    private final Page manageIndex = new Page("/manage/index.xhtml");
    private final Page manageUsers = new Page("/manage/users.xhtml");
    private final Page home = new Page("/home.xhtml");
    private final Page thisPage = new Page("");
    // other pages ...
 
    public Page getManageIndex() {
        return manageIndex;
    }
 
    // other getters ...
}

Looking forward, the Page class may also include e.g. security management, however that would require some more JSF bindings.

Adam

0 Flares Twitter 0 Facebook 0 Google+ 0 LinkedIn 0 Email -- 0 Flares ×