null

Почему URL отстаёт на шаг от отображемой страницы в JSF?

В данной заметке расскажу о проблеме с навигацией в JSF, когда после перехода в адресной строке браузера отображается URL предыдущей страницы, а не той на которую перешли.

 

История была такая: попросили посмотреть и решить проблему. Открываем страницу и видим, что url не соответствует содержимому. Находимся мы на page2.xhtml, а url page1.xthml. Пробуем перейти на page3.xhtml через меню, навигация происходит, но в url теперь page2.xhtml.

Окей. Идём в код смотреть содержимое xhtml страницы.

<p:menubar class="main-menu" model="#{someBean.model}"/>

Навигация реализована через некий элемент menuBar из библиотеки тегов Primefaces. Имеет смысл отправиться в код бина.

public MenuModel getModel() {
        MenuModel model = new DefaultMenuModel();

        int i = 0;
        for (Page page : currentModule.pages()) {
            DefaultMenuItem item = new DefaultMenuItem(i18nMap.get(getMessageId(page)));
            item.setId(String.valueOf(i));
            if(page.equals(currentPage)) {
                item.setStyleClass("selected");
            }
            item.setAjax(false);
            item.setCommand("#{some.redirectListener}");
            item.setParam("page", page);
            model.addElement(item);
            i++;
        }
        
        return model;
    }

Если немного почитать исходный код Primefaces и углубиться в реализацию можно увидеть, что setCommand ожидает получить  listener, который будет вызван при нажатии на элемент в меню. Данный listener принимает ActionEvent и возвращает строку. Используя строку, которую вернёт ему listener он совершит переход на другую страницу. Логику работы можно посмотреть в сорцах библиотеки. Вот интересующий нас класс. Приведу небольшой участок кода этого класса.

if (command != null) {
                String actionExpressionString = menuItem.getCommand();
                MethodExpression noArgExpr = facesContext.getApplication().getExpressionFactory().
                        createMethodExpression(eLContext, actionExpressionString,
                                String.class, new Class[0]);
                Object invokeResult = null;

                try {
                    invokeResult = noArgExpr.invoke(eLContext, null);
                }
                catch (MethodNotFoundException methodNotFoundException) {
                    try {
                        MethodExpression argExpr = facesContext.getApplication().getExpressionFactory().
                                createMethodExpression(eLContext, actionExpressionString,
                                        String.class, new Class[]{ActionEvent.class});

                        invokeResult = argExpr.invoke(eLContext, new Object[]{event});
                    }
                    catch (MethodNotFoundException methodNotFoundException2) {
                        MethodExpression argExpr = facesContext.getApplication().getExpressionFactory().
                                createMethodExpression(eLContext, actionExpressionString,
                                        String.class, new Class[]{MenuActionEvent.class});

                        invokeResult = argExpr.invoke(eLContext, new Object[]{event});
                    }
                }
                finally {
                    String outcome = (invokeResult != null) ? invokeResult.toString() : null;

                    facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, actionExpressionString, outcome);
                }
            }

Если в MenuItem был задан атрибут команда, то из контекста приложений извлекается метод бина указанный в команде. После этого происходит вызов данного метода и запись результата в переменную outcome. После этого outcome передаётся в NagivationHandler приложения и jsf обрабатывает действие навигации.

 

Здесь нужно вспомнить одну простую вещь. Если в outcome попадает просто URL, то JSF рассматривает данное действие не как redirect, а как forward. Из-за этого url браузера не меняется, меняется лишь содержимое страницы. Для того чтобы произвести redirect, необходимо в URL добавить соответствующий параметр запрос faces_redirect равный true. Давайте теперь взгляем как формируется url.

public String redirectListener(ActionEvent event) {
        MenuItem menuItem = ((MenuActionEvent) event).getMenuItem();
        return goToPage(Page.valueOf(menuItem.getParams().get("page").get(0)));
    }
private String goToPage(Page page) {
        currentPage = page;
        return currentPage.url();
    }

Как видно из кода, url - атрибут сущности Page. В коде попавшемся мне, Page реализовали через enum. у каждого элемента enum'а был задан атрибут url. Поэтому, чтобы решить проблему необходимо внести небольшую правку в getter этого атрибута. В результате должны получить следующее:

public String url() {
            return url + "?faces-redirect=true";
        }

С этим параметром теперь всё должно работать корректно и JSF по клику будет осуществлять не forwarding, а redirect.

На сегодня, пожалуй, откланяюсь.