001/*
002 * (C) Copyright 2006-2008 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     arussel
018 */
019package org.nuxeo.ecm.webapp.shield;
020
021import java.io.IOException;
022
023import javax.faces.component.UIViewRoot;
024import javax.faces.context.FacesContext;
025import javax.servlet.ServletException;
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.jboss.seam.contexts.Contexts;
032import org.jboss.seam.contexts.FacesLifecycle;
033import org.jboss.seam.contexts.Lifecycle;
034import org.jboss.seam.mock.MockExternalContext;
035import org.jboss.seam.mock.MockFacesContext;
036import org.nuxeo.ecm.platform.web.common.exceptionhandling.service.ExceptionHandlingListener;
037import org.nuxeo.runtime.transaction.TransactionHelper;
038
039/**
040 * Plays with conversations, trying to rollback transaction.
041 *
042 * @author arussel
043 */
044public class SeamExceptionHandlingListener implements ExceptionHandlingListener {
045
046    private static final Log log = LogFactory.getLog(SeamExceptionHandlingListener.class);
047
048    /**
049     * Initiates a mock faces context when needed and tries to restore current conversation
050     */
051    @Override
052    public void beforeSetErrorPageAttribute(Throwable t, HttpServletRequest request, HttpServletResponse response)
053            throws IOException, ServletException {
054        // cut/paste from seam Exception filter.
055        // we recreate the seam context to be able to use the messages.
056
057        // patch following https://jira.jboss.org/browse/JBPAPP-1427
058        // Ensure that the call in which the exception occurred was cleaned up
059        // - it might not be, and there is no harm in trying
060
061        // we keep around the synchronization component because
062        // SeSynchronizations expects it to be unchanged when called by
063        // the finally block of UTTransaction.rollback
064        String SEAM_TX_SYNCS = "org.jboss.seam.transaction.synchronizations";
065        Object syncs = null;
066        if (Contexts.isEventContextActive()) {
067            syncs = Contexts.getEventContext().get(SEAM_TX_SYNCS);
068        }
069
070        Lifecycle.endRequest();
071
072        FacesContext facesContext = FacesContext.getCurrentInstance();
073        if (facesContext == null) {
074            // the FacesContext is gone - create a fake one for Redirect and
075            // HttpError to call
076            MockFacesContext mockContext = createFacesContext(request, response);
077            mockContext.setCurrent();
078            facesContext = mockContext;
079            log.debug("Created mock faces context for exception handling");
080        } else {
081            // do not use a mock faces context when a real one still exists:
082            // when released, the mock faces context is nullified, and this
083            // leads to errors like in the following stack trace:
084            /**
085             * java.lang.NullPointerException: FacesContext is null at org.ajax4jsf
086             * .context.AjaxContext.getCurrentInstance(AjaxContext.java:159) at
087             * org.ajax4jsf.context.AjaxContext.getCurrentInstance(AjaxContext. java:144) at
088             * org.ajax4jsf.component.AjaxViewRoot.getViewId(AjaxViewRoot .java:580) at
089             * com.sun.faces.lifecycle.Phase.doPhase(Phase.java:104)
090             */
091            log.debug("Using existing faces context for exception handling");
092        }
093
094        // NXP-5998: conversation initialization seems to be already handled by
095        // SeamPhaseListener => do not handle conversation as otherwise it'll
096        // stay unlocked.
097
098        // if the event context was cleaned up, fish the conversation id
099        // directly out of the ServletRequest attributes, else get it from
100        // the event context
101        // Manager manager = Contexts.isEventContextActive() ? (Manager)
102        // Contexts.getEventContext().get(
103        // Manager.class)
104        // : (Manager)
105        // request.getAttribute(Seam.getComponentName(Manager.class));
106        // String conversationId = manager == null ? null
107        // : manager.getCurrentConversationId();
108
109        FacesLifecycle.beginExceptionRecovery(facesContext.getExternalContext());
110
111        // restore syncs
112        if (syncs != null) {
113            Contexts.getEventContext().set(SEAM_TX_SYNCS, syncs);
114        }
115
116        // If there is an existing long-running conversation on
117        // the failed request, propagate it
118        // if (conversationId == null) {
119        // Manager.instance().initializeTemporaryConversation();
120        // } else {
121        // ConversationPropagation.instance().setConversationId(conversationId);
122        // Manager.instance().restoreConversation();
123        // }
124    }
125
126    @Override
127    public void beforeForwardToErrorPage(Throwable t, HttpServletRequest request, HttpServletResponse response)
128            throws IOException, ServletException {
129        // nothing
130    }
131
132    /**
133     * Rollbacks transaction if necessary
134     */
135    @Override
136    public void startHandling(Throwable t, HttpServletRequest request, HttpServletResponse response)
137            throws ServletException {
138        if (TransactionHelper.isTransactionActive()) {
139            TransactionHelper.setTransactionRollbackOnly();
140        }
141    }
142
143    /**
144     * Cleans up context created in
145     * {@link #beforeSetErrorPageAttribute(Throwable, HttpServletRequest, HttpServletResponse)} when needed.
146     */
147    @Override
148    public void afterDispatch(Throwable t, HttpServletRequest request, HttpServletResponse response)
149            throws IOException, ServletException {
150        FacesContext context = FacesContext.getCurrentInstance();
151        // XXX: it's not clear what should be done here: current tests
152        // depending on the faces context just allow to avoid errors after
153        if (context instanceof MockFacesContext) {
154            // do not end the request if it's a real faces context, otherwise
155            // we'll get the following stack trace:
156            /**
157             * java.lang.IllegalStateException: No active application scope at
158             * org.jboss.seam.core.Init.instance(Init.java:76) at org.jboss.seam.
159             * jsf.SeamPhaseListener.handleTransactionsAfterPhase( SeamPhaseListener.java:330) at
160             * org.jboss.seam.jsf.SeamPhaseListener .afterServletPhase(SeamPhaseListener.java:241) at org.jboss.seam.jsf
161             * .SeamPhaseListener.afterPhase(SeamPhaseListener.java:192) at
162             * com.sun.faces.lifecycle.Phase.handleAfterPhase(Phase.java:175) at
163             * com.sun.faces.lifecycle.Phase.doPhase(Phase.java:114) at com.sun.faces
164             * .lifecycle.LifecycleImpl.render(LifecycleImpl.java:139)
165             */
166            FacesLifecycle.endRequest(context.getExternalContext());
167            // do not release an actual FacesContext that we did not create,
168            // otherwise we get the following stack trace:
169            /**
170             * java.lang.IllegalStateException at com.sun.faces.context.FacesContextImpl
171             * .assertNotReleased(FacesContextImpl.java:395) at com.sun.faces.context
172             * .FacesContextImpl.getExternalContext(FacesContextImpl.java:147) at
173             * com.sun.faces.util.RequestStateManager.getStateMap( RequestStateManager.java:276) at
174             * com.sun.faces.util.RequestStateManager .remove(RequestStateManager.java:243) at
175             * com.sun.faces.context.FacesContextImpl .release(FacesContextImpl.java:345)
176             */
177            context.release();
178        }
179    }
180
181    protected MockFacesContext createFacesContext(HttpServletRequest request, HttpServletResponse response) {
182        MockFacesContext mockFacesContext = new MockFacesContext(new MockExternalContext(
183                request.getSession().getServletContext(), request, response), new MockApplication());
184        mockFacesContext.setViewRoot(new UIViewRoot());
185        return mockFacesContext;
186    }
187
188    @Override
189    public void responseComplete() {
190        FacesContext facesContext = FacesContext.getCurrentInstance();
191        if (facesContext != null) {
192            facesContext.responseComplete();
193        } else {
194            log.error("Cannot set response complete: faces context is null");
195        }
196    }
197
198}