# Tomcat wirft keine ViewExpiredException



## chrisbad (2. Jun 2012)

Hallo zusammen,

aktuell nutze ich Primefaces 3.2 und JSF2´, Mojarra in einem Tomcat 7 Container.

Natürlich habe ich auch das alt bekannte Problem, dass AJAX auf einer geöffneten Seite nicht mehr funktioniert sobald die Session abgelaufen ist.

Also habe ich die Anleitung von Ed Burns  umgesetzt.

Das Problem: Es wird gar keine Exception geworfen wenn ich z. B. versuche nach einem SessionTimeout einen bereiets geladenen DataTable weiter zu blättern. Folglich kann der Exceptionhandler auch keinen Redirect machen.

Geprüft habe ich das mit diversen Sysouts - Der Handler wird aufgerufen aber der ExceptionStack ist leer .hasNext() = false.

Was mache ich falsch?


----------



## JimPanse (4. Jun 2012)

Kannst du bitte deine conf. posten. Ich benutze ungefähr den gleichen Ansatz und bei mir funktioniert es.

Greetz


----------



## chrisbad (4. Jun 2012)

klaro

meine web.xml

[XML]<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <description>JSF 2.0 - Hello World</description>

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>
            javax.faces.webapp.FacesServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
        <url-pattern>*.jsf</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>1</session-timeout>
    </session-config>

    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>

    <context-param>
        <param-name>facelets.DEVELOPMENT</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>Server</param-value>
    </context-param>

    <security-constraint>
        <display-name>Admin</display-name>
        <web-resource-collection>
            <web-resource-name>admin</web-resource-name>
            <description/>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <security-constraint>
        <display-name>Manager</display-name>
        <web-resource-collection>
            <web-resource-name>/manager/*</web-resource-name>
            <description/>
            <url-pattern>/manager/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>admin</role-name>
            <role-name>manager</role-name>
        </auth-constraint>
    </security-constraint>

    <security-constraint>
        <display-name>User</display-name>
        <web-resource-collection>
            <web-resource-name>/user/*</web-resource-name>
            <description/>
            <url-pattern>/user/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>admin</role-name>
            <role-name>manager</role-name>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>MemoryRealm</realm-name>
        <form-login-config>
            <form-login-page>/login.xhtml</form-login-page>
            <form-error-page>/login-error.xhtml</form-error-page>
        </form-login-config>
    </login-config>

    <security-role>
        <description/>
        <role-name>admin</role-name>
    </security-role>

    <security-role>
        <description/>
        <role-name>manager</role-name>
    </security-role>

    <security-role>
        <description/>
        <role-name>user</role-name>
    </security-role>

    <listener>
        <listener-class>
            de.firma.onlineshop.filter.JdbcRegisterListener
        </listener-class>
    </listener>

</web-app>
[/XML]

faces.config

[XML]<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

    <factory>
        <exception-handler-factory>de.firma.onlineshop.filter.ViewExpiredExceptionExceptionHandlerFactory</exception-handler-factory>
    </factory>

    <!--
    <lifecycle>
        <phase-listener>de.firma.onlineshop.filter.MyPhaseListener</phase-listener>
    </lifecycle>
    -->

</faces-config>[/XML]

ExceptionHandlerFactory


```
package de.firma.onlineshop.filter;

import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;

public class ViewExpiredExceptionExceptionHandlerFactory extends ExceptionHandlerFactory {

    private ExceptionHandlerFactory parent;

    public ViewExpiredExceptionExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Override
    public ExceptionHandler getExceptionHandler() {
        ExceptionHandler result = parent.getExceptionHandler();
        result = new ViewExpiredExceptionExceptionHandler(result);

        return result;
    }


}
```

ExceptionHandler


```
package de.firma.onlineshop.filter;

import java.util.Iterator;
import java.util.Map;
import javax.faces.FacesException;
import javax.faces.application.NavigationHandler;
import javax.faces.application.ViewExpiredException;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.FacesContext;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.ExceptionQueuedEventContext;

public class ViewExpiredExceptionExceptionHandler extends ExceptionHandlerWrapper {

    private ExceptionHandler wrapped;

    public ViewExpiredExceptionExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ExceptionHandler getWrapped() {
        return this.wrapped;
    }

    @Override
    public void handle() throws FacesException {
        System.out.println("Listener is called...");

        for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
            ExceptionQueuedEvent event = i.next();
            ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
            Throwable t = context.getException();
            if (t instanceof ViewExpiredException) {
                ViewExpiredException vee = (ViewExpiredException) t;
                FacesContext fc = FacesContext.getCurrentInstance();
                Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
                NavigationHandler nav =
                        fc.getApplication().getNavigationHandler();
                try {
                    // Push some useful stuff to the request scope for
                    // use in the page
                    requestMap.put("currentViewId", vee.getViewId());

                    nav.handleNavigation(fc, null, "viewExpired");
                    fc.renderResponse();

                } finally {
                    i.remove();
                }
            }
        }
        // At this point, the queue will not contain any ViewExpiredEvents.
        // Therefore, let the parent handle them.
        getWrapped().handle();

    }
}
```

Weder auf der Console, im Log oder im ExceptionStack eine Exception wenn die Session in den TimeOut geht...


----------



## JimPanse (4. Jun 2012)

Hi,

auf anhieb finde ich jetzt nichts außer das der Eintrag fehlt wohin deine App navigieren soll bei dem Kommando 'viewExpired'. Die Frage ist wird die Session jemals erstellt? Ich denke du hast testweise den session timeout auf 1 gestellt. Welche Sicherheitsimpl. verwendest du denn? JAAS? Vielleicht fängt der Filter die Exception vorher ab.

+ du mappst das FacesServlet auf *.xhtml -> besser wäre *.jsf oder /faces/*.

Testweise kannst du noch eine Session listener rauf setzen um zu sehen wann die session genau abläuft.


```
@WebListener
public class SessionContextListener implements HttpSessionListener {


	@Override
	public void sessionCreated(HttpSessionEvent arg0) {
		System.out.println("Web-Session created");
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent arg0) {
		System.out.println("Web-Session destroyed");
	}
}
```

Greetz


----------



## chrisbad (5. Jun 2012)

Haha, genau das hab ich jetzt auch probiert bevor ich hier rein geschaut habe ,-)

Funktioniert aber leider nicht (ganz).

Die Lösung des Problems bestand erstmal darin, dass der Tomcat gar keine ViewExpiredException werfen kann, da es sich um Ajax-Requests aus Primefaces handelt. In meinem Szenario war die Lösung die Folgende:

Alles was nicht Ajax ist, kann ich getrost aussen vor lassen, da sich die User bei mir anmelden müssen.
Inhalte von Seiten die loggedout aufgerufen werden können sind egal. In den gesicherten Bereichen wird man bei einem HttpRequest sowieso wieder zur Login-Seite geleitet wenn die Session abgelaufen ist.

Das Problem waren nur die Ajax-Requests. Da bleibt Primefaces dann einfach stehen.
Ich habe probiert @WebListener und @WebFilter. Bei ersterem ist das redirecten sehr schwer, letzteres hört auf zu funktionieren wenn die Session abgelaufen ist.

Die Lösung war ein PhaseListener mit entsprechendem Eintrag in der faces-config-xml.
Zuerst wird überprüft ob es sich um einen Ajax-Request handelt und dann ob die Session noch valid ist.
Falls nicht wird über FacesContext.getCurr[...]redirect("/index.jsf") umgeleitet. Und das funktioniert jetzt wunderbar.
Dadurch kommt auch auf den Seiten auf denen man nicht eingeloggt ist nur ein Redirect zu stande, wenn ein Ajax-Request ausgeführt wird.

Das war's für mich gewesen, danke dir trotzdem...


----------



## Fant (5. Jun 2012)

chrisbad hat gesagt.:


> Die Lösung war ein PhaseListener mit entsprechendem Eintrag in der faces-config-xml.
> Zuerst wird überprüft ob es sich um einen Ajax-Request handelt und dann ob die Session noch valid ist.
> Falls nicht wird über FacesContext.getCurr[...]redirect("/index.jsf") umgeleitet. Und das funktioniert jetzt wunderbar.
> Dadurch kommt auch auf den Seiten auf denen man nicht eingeloggt ist nur ein Redirect zu stande, wenn ein Ajax-Request ausgeführt wird.



Kannst du das vielleicht noch etwas genauer ausführen? Das klingt nämlich genau nach dem, was ich momentan suche ... ich weiß nur trotzdem noch nicht so wirklich, wie ich das nun umzusetzen hätte.

Gruß Fant


----------



## chrisbad (5. Jun 2012)

```
public class SessionTimeoutPhaseListener implements PhaseListener {

    @Override
    public void afterPhase(PhaseEvent pe) {
    }

    @Override
    public void beforePhase(PhaseEvent pe) {
        ExternalContext ctx = pe.getFacesContext().getExternalContext();
        HttpServletRequest request = (HttpServletRequest) ctx.getRequest();
        HttpSession session = request.getSession(false);

        if ("partial/ajax".equals(request.getHeader("Faces-Request"))) {
            if (!request.isRequestedSessionIdValid() || session == null) {
                try {
                    ctx.redirect(ctx.getRequestContextPath() + "/session-expired.xhtml");
                } catch (IOException ex) {
                    Logger.getLogger(SessionTimeoutPhaseListener.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }
}
```

Soweit bin ich jetzt - der Phaselistener muss in der faces-config.xml konfiguriert werden.
Allerdings funktioniert der jetzt "nicht mehr" (hatte ich aber schon mal) auf der Startseite. In einem gesicherten Bereich funktioniert er super. 

Das Problem ist, dass beim ersten Aufruf der App noch keine Session existiert.
if session == null funktioniert dann leider nicht. isRequestedSessionIdValid funktioniert dann natürlich auch nicht. Hier muss ich noch rausfinden welchen Wert die jeweiligen Vars zu den relevanten Zeitpunkten haben.

Ein Filter hat nicht funktioniert, weil der bei mir nicht mehr funktionierte sobald die Session abgelaufen war. Ein Weblistener mit Annotations hat auch nicht mehr funktioniert sobald die Session weg war.

Falls du das Problem mit dem ersten Aufruf gelöst bekommst wäre ich für einen Tip auch dankbar...

de Chris


----------



## JimPanse (6. Jun 2012)

Fast richtig aber


```
public class SecurityPhaseListener implements PhaseListener, Serializable {
	@Override
	public void afterPhase(PhaseEvent arg0) {
			final FacesContext facescontext = arg0.getFacesContext();
		
		// not exist yet (non-faces-request) 
		if (facescontext.getViewRoot() == null) {
			facescontext.getApplication().getNavigationHandler().handleNavigation(facescontext, null, "index");
			// ignore other phases
			facescontext.renderResponse();
		} else {

			// current view identifier
			final String viewId = facescontext.getViewRoot().getViewId();

			final boolean loginPage = viewId.lastIndexOf("index") > -1;
//  JAAS
			final boolean login = facescontext.getExternalContext().getRemoteUser() != null;
// or (facescontext.getExternalContext().getSessionMap().get("security-token") != null);
			
			// not login page and user is not login - redirect to login page
			if (!loginPage && !login) {

				facescontext.getApplication().getNavigationHandler().handleNavigation(facescontext, null, "index");
				// ignore other phases
				facescontext.renderResponse();

				// login page and user is login - redirect to start page
			} else if (loginPage && login) {
				facescontext.getApplication().getNavigationHandler().handleNavigation(facescontext, null, "start");
				// ignore other phases
				facescontext.renderResponse();
			}
		}
// go on
	}

	/**
	 * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
	 */
	@Override
	public void beforePhase(PhaseEvent arg0) {
		// nothing to do -> view will be restore or new created
	}

	/**
	 * @see javax.faces.event.PhaseListener#getPhaseId()
	 */
	@Override
	public PhaseId getPhaseId() {
		// before the view will be created
		return PhaseId.RESTORE_VIEW;
	}

}
```

faces-config.xml

[XML]
<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="2.0" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
Java EE : XML Schemas for Java EE Deployment Descriptors
        http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
	<lifecycle>
		<phase-listener>de.my.package.SecurityPhaseListener</phase-listener>
	</lifecycle>
	<navigation-rule>
		<from-view-id>/*</from-view-id>
		<navigation-case>
			<from-outcome>index</from-outcome>
			<to-view-id>/index.xhtml</to-view-id>
			<redirect />
		</navigation-case>
		<navigation-case>
			<from-outcome>start</from-outcome>
			<to-view-id>/public/start.xhtml</to-view-id>
			<redirect />
		</navigation-case>
	</navigation-rule>
</faces-config>

[/XML]

Grüße


----------



## chrisbad (8. Jun 2012)

Howdy,

sorry war ne Zeit lang geschäftlich unterwegs.

Im Prinzip macht dein Code genau das was er soll, ich war auf einem leicht anderen Weg auch schon soweit, aber das Problem ist, dass das nur funktioniert wenn man in einem gesicherten Bereich unterwegs ist.

Wenn ich die ganz normale Startseite aufrufe, und dort z. B. einen DataTable mit Ajax habe funktioniert der auch nur, so lange die Session existiert. Läuft die Session ab, funktioniert auch der DataTable nicht mehr (z. B. Blättern).

Dafür suche ich eine Lösung, habe aber noch keine gefunden.
Eine Möglichkeit wäre natürlich die Beans RequestScoped zu machen, oder? Aber ich hätte halt schon gerne, das z. B. Filtereinstellungen erhalten bleiben solange die Session läuft.
Die User können bei mir die App teilweise schon benutzen ohne sich anmelden zu müssen, müssten sich alle automatisch anmelden würde deine Lösung super funktionieren...

LG Chris


----------

