001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.cdi;
018
019import jakarta.enterprise.context.ContextNotActiveException;
020import jakarta.enterprise.context.Conversation;
021import jakarta.enterprise.context.ConversationScoped;
022import jakarta.inject.Inject;
023
024import org.apache.wicket.Application;
025import org.apache.wicket.MetaDataKey;
026import org.apache.wicket.Page;
027import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler;
028import org.apache.wicket.core.request.handler.IPageClassRequestHandler;
029import org.apache.wicket.core.request.handler.IPageRequestHandler;
030import org.apache.wicket.request.IRequestHandler;
031import org.apache.wicket.request.Url;
032import org.apache.wicket.request.cycle.IRequestCycleListener;
033import org.apache.wicket.request.cycle.RequestCycle;
034import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
035import org.apache.wicket.request.mapper.parameter.PageParameters;
036import org.apache.wicket.request.resource.PackageResourceReference;
037import org.apache.wicket.util.lang.Args;
038import org.apache.wicket.util.lang.Classes;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * A request cycle listener that takes care of propagating persistent
044 * conversations.
045 * 
046 * @see ConversationScoped
047 * 
048 * @author igor
049 */
050public class ConversationPropagator implements IRequestCycleListener
051{
052        private static final Logger logger = LoggerFactory.getLogger(ConversationPropagator.class);
053
054        private static final MetaDataKey<Boolean> CONVERSATION_STARTED_KEY = new MetaDataKey<>()
055        {
056                private static final long serialVersionUID = 1L;
057        };
058
059        private static final MetaDataKey<String> CONVERSATION_ID_KEY = new MetaDataKey<>()
060        {
061                private static final long serialVersionUID = 1L;
062        };
063
064        public static final String CID = "cid";
065
066        /** propagation mode to use */
067        private final IConversationPropagation propagation;
068
069        private final Application application;
070
071        @Inject
072        private Conversation conversation;
073
074        /**
075         * Constructor
076         * 
077         * @param application
078         * @param propagation
079         */
080        public ConversationPropagator(Application application, IConversationPropagation propagation)
081        {
082                Args.notNull(application, "application");
083                Args.notNull(propagation, "propagation");
084
085                if (propagation == ConversationPropagation.NONE)
086                {
087                        throw new IllegalArgumentException(
088                                        "If propagation is NONE do not set up the propagator");
089                }
090
091                this.application = application;
092                this.propagation = propagation;
093
094                NonContextual.of(ConversationPropagator.class).postConstruct(this);
095        }
096
097        public IConversationPropagation getPropagation()
098        {
099                return propagation;
100        }
101
102        @Override
103        public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler)
104        {
105                if (activateForHandler(handler))
106                {
107                        logger.debug("Activating conversation {}", conversation.getId());
108                        fireOnAfterConversationStarted(cycle);
109                }
110        }
111
112        private void fireOnAfterConversationStarted(RequestCycle cycle)
113        {
114                cycle.setMetaData(CONVERSATION_STARTED_KEY, true);
115                for (IRequestCycleListener listener : application.getRequestCycleListeners())
116                {
117                        if (listener instanceof ICdiAwareRequestCycleListener)
118                        {
119                                ((ICdiAwareRequestCycleListener)listener).onAfterConversationActivated(cycle);
120                        }
121                }
122        }
123
124        @Override
125        public void onRequestHandlerExecuted(RequestCycle cycle, IRequestHandler handler)
126        {
127                // propagate current non-transient conversation to the newly scheduled
128                // page
129                try
130                {
131                        if (conversation.isTransient())
132                        {
133                                return;
134                        }
135                } catch (ContextNotActiveException cnax)
136                {
137                        logger.debug("There is no active context for the requested scope!", cnax);
138                        return;
139                }
140
141                if (propagation.propagatesVia(handler, IPageRequestHandler.getPage(handler)))
142                {
143                        logger.debug(
144                                        "Propagating non-transient conversation {} via page parameters of handler {}",
145                                        conversation.getId(), handler);
146
147                        PageParameters parameters = getPageParameters(handler);
148                        if (parameters != null)
149                        {
150                                parameters.set(CID, conversation.getId());
151                                markPageWithConversationId(handler, conversation.getId());
152                        }
153                }
154        }
155
156
157        @Override
158        public void onUrlMapped(RequestCycle cycle, IRequestHandler handler, Url url)
159        {
160                // no need to propagate the conversation to packaged resources, they
161                // should never change
162                if (handler instanceof ResourceReferenceRequestHandler)
163                {
164                        if (((ResourceReferenceRequestHandler)handler).getResourceReference() instanceof PackageResourceReference)
165                        {
166                                return;
167                        }
168                }
169
170                if (conversation.isTransient())
171                {
172                        return;
173                }
174
175                if (propagation.propagatesVia(handler, IPageRequestHandler.getPage(handler)))
176                {
177                        logger.debug("Propagating non-transient conversation {} via url", conversation.getId());
178                        url.setQueryParameter(CID, conversation.getId());
179                        markPageWithConversationId(handler, conversation.getId());
180                }
181        }
182
183        @Override
184        public void onDetach(RequestCycle cycle)
185        {
186                if (!Boolean.TRUE.equals(cycle.getMetaData(CONVERSATION_STARTED_KEY)))
187                {
188                        return;
189                }
190
191                logger.debug("Deactivating conversation {}", conversation.getId());
192                for (IRequestCycleListener listener : application.getRequestCycleListeners())
193                {
194                        if (listener instanceof ICdiAwareRequestCycleListener)
195                        {
196                                ((ICdiAwareRequestCycleListener)listener).onBeforeConversationDeactivated(cycle);
197                        }
198                }
199        }
200
201        /**
202         * Determines whether or not a conversation should be activated fro the
203         * specified handler. This method is used to filter out conversation
204         * activation for utility handlers such as the
205         * {@link BufferedResponseRequestHandler}
206         * 
207         * @param handler
208         * @return {@code true} iff a conversation should be activated
209         */
210        protected boolean activateForHandler(IRequestHandler handler)
211        {
212                if (handler != null)
213                {
214                        String handlerClassName = Classes.name(handler.getClass());
215
216                        if (handler instanceof BufferedResponseRequestHandler)
217                        {
218                                // we do not care about pages that are being rendered from a buffer
219                                return false;
220                        } else if ("org.apache.wicket.protocol.ws.api.WebSocketMessageBroadcastHandler".equals(handlerClassName))
221                        {
222                                return false;
223                        } else if ("org.apache.wicket.protocol.ws.api.WebSocketRequestHandler".equals(handlerClassName)) {
224                                // injection is not supported in web sockets communication
225                                return false;
226                        }
227                }
228                return true;
229        }
230
231        public static void markPageWithConversationId(IRequestHandler handler, String cid)
232        {
233                Page page = IPageRequestHandler.getPage(handler);
234                if (page != null)
235                {
236                        page.setMetaData(CONVERSATION_ID_KEY, cid);
237                }
238        }
239
240        public static String getConversationIdFromPage(Page page)
241        {
242                return page.getMetaData(CONVERSATION_ID_KEY);
243        }
244
245        public static void removeConversationIdFromPage(Page page)
246        {
247                page.setMetaData(CONVERSATION_ID_KEY, null);
248        }
249
250        /**
251         * Resolves page parameters from a request handler
252         * 
253         * @param handler
254         * @return page parameters or {@code null} if none
255         */
256        protected PageParameters getPageParameters(IRequestHandler handler)
257        {
258                if (handler instanceof IPageClassRequestHandler)
259                {
260                        IPageClassRequestHandler pageHandler = (IPageClassRequestHandler)handler;
261                        return pageHandler.getPageParameters();
262                }
263                return null;
264        }
265}