Search code examples
jsftomcatjsf-2.2myfacesomnifaces

Bug with omnifaces 1.10 when OmniPartialViewContext#startDocument performs transparent redirection


I am using the following stack :

  • PrimeFaces 5.3.1
  • MyFaces 2.2.8
  • OpenWebBeans 1.6.2
  • OmniFaces 1.10
  • DeltaSpike 1.5.1
  • Tomcat 8.0.28

AFAI understand, only MyFaces and OmniFaces are important there.

I have a bug when an ajax request is performed by a client whose session is expired and when the access to the page is controlled by a <security-constraint> in the webapp web.xml.

In that case, OmniPartialViewContext#startDocument perform a "transparent redirect", to provide a better explanation (see https://github.com/omnifaces/omnifaces/blob/master/src/main/java/org/omnifaces/context/OmniPartialViewContext.java#L275 ) (this function is unchanged from version 1.10 to current 1.11).

    @Override
    public void startDocument() throws IOException {
        wrapped.startDocument();
        String loginURL = WebXml.INSTANCE.getFormLoginPage();

        if (loginURL != null) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            String loginViewId = normalizeViewId(facesContext, loginURL);

            if (loginViewId.equals(getViewId(facesContext))) {
                String originalURL = getRequestAttribute(facesContext, "javax.servlet.forward.request_uri");

                if (originalURL != null) {
                    redirect(originalURL);
                }
            }
        }
    }

This is a problem because, higher in the stack, org.apache.myfaces.context.servlet.PartialViewContextImpl.processPartialRendering does lines 466 and following :

    {
        String currentEncoding = writer.getCharacterEncoding();
        writer.writePreamble("<?xml version=\"1.0\" encoding=\""+
            (currentEncoding == null ? "UTF-8" : currentEncoding) +"\"?>");
        writer.startDocument();

        writer.writeAttribute("id", viewRoot.getContainerClientId(_facesContext),"id");

So, an exception like the following is raised when trying to write the id, because no element is currently opened.

16-Nov-2015 16:36:35.980 SEVERE [http-apr-8444-exec-10] org.omnifaces.exceptionhandler.FullAjaxExceptionHandler.logException FullAjaxExceptionHandler: An exception occurred during rendering JSF ajax response. Error page '/error.xhtml' will be shown.
java.lang.IllegalStateException: Must be called before the start element is closed (attribute 'id')
        at org.apache.myfaces.shared.renderkit.html.HtmlResponseWriterImpl.writeAttribute(HtmlResponseWriterImpl.java:816)
        at javax.faces.context.ResponseWriterWrapper.writeAttribute(ResponseWriterWrapper.java:109)
        at org.apache.myfaces.context.PartialResponseWriterImpl.writeAttribute(PartialResponseWriterImpl.java:407)
        at javax.faces.context.ResponseWriterWrapper.writeAttribute(ResponseWriterWrapper.java:109)
        at javax.faces.context.ResponseWriterWrapper.writeAttribute(ResponseWriterWrapper.java:109)
        at org.apache.myfaces.context.servlet.PartialViewContextImpl.processPartialRendering(PartialViewContextImpl.java:473)
        at org.apache.myfaces.context.servlet.PartialViewContextImpl.processPartial(PartialViewContextImpl.java:415)
        at org.primefaces.context.PrimePartialViewContext.processPartial(PrimePartialViewContext.java:60)
        at javax.faces.context.PartialViewContextWrapper.processPartial(PartialViewContextWrapper.java:85)
        at javax.faces.component.UIViewRoot.encodeChildren(UIViewRoot.java:516)
        at javax.faces.component.UIComponentBase.encodeAll(UIComponentBase.java:541)
        at org.apache.myfaces.view.facelets.FaceletViewDeclarationLanguage.renderView(FaceletViewDeclarationLanguage.java:1891)
        at org.apache.myfaces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:313)
        at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:58)
        at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:58)
        at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:58)
        at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:58)
        at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:58)
        at org.apache.myfaces.lifecycle.RenderResponseExecutor.execute(RenderResponseExecutor.java:116)
        at org.apache.myfaces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:267)
        at org.apache.deltaspike.jsf.impl.listener.request.DeltaSpikeLifecycleWrapper.render(DeltaSpikeLifecycleWrapper.java:111)
        at javax.faces.lifecycle.LifecycleWrapper.render(LifecycleWrapper.java:31)
        at org.apache.deltaspike.jsf.impl.listener.request.JsfClientWindowAwareLifecycleWrapper.render(JsfClientWindowAwareLifecycleWrapper.java:160)
        at org.apache.deltaspike.jsf.impl.listener.request.DeltaSpikeLifecycleWrapper.render(DeltaSpikeLifecycleWrapper.java:111)
        at javax.faces.lifecycle.LifecycleWrapper.render(LifecycleWrapper.java:31)
        at org.apache.deltaspike.jsf.impl.listener.request.JsfClientWindowAwareLifecycleWrapper.render(JsfClientWindowAwareLifecycleWrapper.java:160)
        at javax.faces.webapp.FacesServlet.service(FacesServlet.java:200)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:720)
        at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:466)
        at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:391)
        at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:318)
        at org.apache.catalina.authenticator.FormAuthenticator.forwardToLoginPage(FormAuthenticator.java:384)
        at org.apache.catalina.authenticator.FormAuthenticator.authenticate(FormAuthenticator.java:229)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:577)
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616)
        at org.apache.catalina.authenticator.SingleSignOn.invoke(SingleSignOn.java:291)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:518)
        at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091)
        at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:673)
        at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2503)
        at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2492)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)

How can I correct that (appart from dropping OmniFaces, which is really cool, and that I would really like to keep. :-) ) ?


Solution

  • Eventually, I am using the following solution, which can be seen as a work around, as patching MyFaces or OmniFaces is out of my reach...

    I am adding my own PartialViewContext at the top of the processing chain. This way, I can perform a clean redirect on an ajax request when I can see it is trying to get the login page.

    To do so, one has to :

    • implement a PartialViewContextFactory
    • implement a PartialViewContext
    • declare the PartialViewContextFactory in the faces-config.xml file

    So, in my webapp faces-config.xml, I put :

    <factory>
        <partial-view-context-factory>fr.senat.context.SenatPartialViewContextFactory</partial-view-context-factory>
    </factory>    
    

    My PartialViewContextFactory is dead simple :

    package fr.senat.context;
    
    import javax.faces.context.FacesContext;
    import javax.faces.context.PartialViewContext;
    import javax.faces.context.PartialViewContextFactory;
    import lombok.Getter;
    
    /**
    *
    * @author lpenet
    */
    public class SenatPartialViewContextFactory extends PartialViewContextFactory {
        @Getter
        private final PartialViewContextFactory wrapped;
    
        public SenatPartialViewContextFactory(PartialViewContextFactory wrapped) {
                this.wrapped = wrapped;
        }
    
        @Override
        public PartialViewContext getPartialViewContext(FacesContext context) {
                return new SenatPartialViewContext(wrapped.getPartialViewContext(context));
        }
    }
    

    and the PartialViewContext is quite simple too :

    package fr.senat.context;
    
    import java.io.IOException;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.faces.component.UIViewRoot;
    import javax.faces.context.FacesContext;
    import javax.faces.context.PartialResponseWriter;
    import javax.faces.context.PartialViewContext;
    import javax.faces.context.PartialViewContextWrapper;
    import javax.faces.event.PhaseId;
    import javax.servlet.http.HttpServletRequest;
    import lombok.Getter;
    import org.apache.myfaces.context.servlet.PartialViewContextImpl;
    import org.omnifaces.config.WebXml;
    import static org.omnifaces.util.FacesLocal.getRequestAttribute;
    import static org.omnifaces.util.FacesLocal.getViewId;
    import static org.omnifaces.util.FacesLocal.normalizeViewId;
    
    /**
     *
     * @author lpenet
     */
    public class SenatPartialViewContext  extends PartialViewContextWrapper {
    
        @Getter
        private final PartialViewContext wrapped;
    
        public SenatPartialViewContext(PartialViewContext wrapped) {
                this.wrapped = wrapped;
        }
    
        private void processPartialRendering() throws IOException
        {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            UIViewRoot viewRoot = facesContext.getViewRoot();
    
            String loginURL = WebXml.INSTANCE.getFormLoginPage();
            if (loginURL != null) {
                String loginViewId = normalizeViewId(facesContext, loginURL);
    
                if (loginViewId.equals(getViewId(facesContext))) {
                        String originalURL = getRequestAttribute(facesContext, "javax.servlet.forward.request_uri");
    
                        if (originalURL != null) {
                            PartialResponseWriter writer = facesContext.getPartialViewContext().getPartialResponseWriter();
                            writer.startDocument();
                            HttpServletRequest request = (HttpServletRequest) facesContext.getExternalContext().getRequest();
                            writer.redirect(request.getContextPath() + loginURL);
                            writer.endDocument();
                            return;
                        }
                }
            }
    
            wrapped.processPartial(PhaseId.RENDER_RESPONSE);
        }
    
        @Override
        public void processPartial(PhaseId phaseId)
        {
            if (phaseId == PhaseId.RENDER_RESPONSE)
            {
                try {
                    processPartialRendering();
                }
            catch (IOException ex)
            {
                Logger log = Logger.getLogger(PartialViewContextImpl.class.getName());
                if (log.isLoggable(Level.SEVERE))
                {
                    log.log(Level.SEVERE, "", ex);
                }
    
            }
            } else {
                wrapped.processPartial(phaseId);
            }
        }
    
    }