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.protocol.ws.api;
018
019import java.util.Map;
020import java.util.Set;
021
022import jakarta.servlet.SessionTrackingMode;
023import jakarta.servlet.http.Cookie;
024import jakarta.servlet.http.HttpServletRequest;
025import org.apache.wicket.Component;
026import org.apache.wicket.behavior.Behavior;
027import org.apache.wicket.markup.head.IHeaderResponse;
028import org.apache.wicket.markup.head.JavaScriptHeaderItem;
029import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
030import org.apache.wicket.protocol.http.WebApplication;
031import org.apache.wicket.protocol.ws.WebSocketSettings;
032import org.apache.wicket.util.cookies.CookieUtils;
033import org.apache.wicket.util.lang.Args;
034import org.apache.wicket.util.lang.Generics;
035import org.apache.wicket.util.string.Strings;
036import org.apache.wicket.util.template.PackageTextTemplate;
037
038/**
039 * A behavior that contributes {@link WicketWebSocketJQueryResourceReference}
040 */
041public class BaseWebSocketBehavior extends Behavior
042{
043        private final String resourceName;
044        private final String connectionToken;
045
046        /**
047         * Constructor.
048         *
049         * Contributes WebSocket initialization code that will
050         * work with {@link org.apache.wicket.protocol.ws.api.WebSocketBehavior}
051         */
052        protected BaseWebSocketBehavior()
053        {
054                this.resourceName = null;
055                this.connectionToken = null;
056        }
057
058        /**
059         * Constructor.
060         *
061         * Contributes WebSocket initialization code that will
062         * work with {@link org.apache.wicket.protocol.ws.api.WebSocketResource}
063         *
064         * To use WebSocketResource the application have to setup the
065         * resource as a shared one in its {@link org.apache.wicket.Application#init()}
066         * method:
067         * <code><pre>
068         *     getSharedResources().add(resourceName, new MyWebSocketResource())
069         * </pre></code>
070         *
071         *  @param resourceName
072         *          the name of the shared {@link org.apache.wicket.protocol.ws.api.WebSocketResource}
073         */
074        public BaseWebSocketBehavior(String resourceName)
075        {
076                this(resourceName, null);
077        }
078
079        /**
080         * Constructor.
081         *
082         * Contributes WebSocket initialization code that will
083         * work with {@link org.apache.wicket.protocol.ws.api.WebSocketResource}
084         *
085         * To use WebSocketResource the application have to setup the
086         * resource as a shared one in its {@link org.apache.wicket.Application#init()}
087         * method:
088         * <code><pre>
089         *     getSharedResources().add(resourceName, new MyWebSocketResource())
090         * </pre></code>
091         *
092         *  @param resourceName
093         *          the name of the shared {@link org.apache.wicket.protocol.ws.api.WebSocketResource}
094         *  @param connectionToken
095         *              an optional token to support connections to the same resource from multiple browser tabs
096         */
097        public BaseWebSocketBehavior(String resourceName, String connectionToken)
098        {
099                this.resourceName = Args.notEmpty(resourceName, "resourceName");
100                this.connectionToken = connectionToken;
101        }
102
103        @Override
104        public void renderHead(Component component, IHeaderResponse response)
105        {
106                super.renderHead(component, response);
107
108                response.render(JavaScriptHeaderItem.forReference(WicketWebSocketJQueryResourceReference.get()));
109
110                Map<String, Object> parameters = getParameters(component);
111                String webSocketSetupScript = getWebSocketSetupScript(parameters);
112
113                response.render(OnDomReadyHeaderItem.forScript(webSocketSetupScript));
114        }
115
116        protected String getWebSocketSetupScript(Map<String, Object> parameters) {
117                PackageTextTemplate webSocketSetupTemplate =
118                                new PackageTextTemplate(WicketWebSocketJQueryResourceReference.class,
119                                                "res/js/wicket-websocket-setup.js.tmpl");
120
121                return webSocketSetupTemplate.asString(parameters);
122        }
123
124        /**
125         * Override to return a context. By default, this is the page class name.
126         *
127         * @param component the {@link org.apache.wicket.Component}
128         * @return the context for this websocket behavior.
129         */
130        protected String getContext(Component component) {
131                return component.getPage().getClass().getName();
132        }
133
134        private Map<String, Object> getParameters(Component component) {
135                Map<String, Object> variables = Generics.newHashMap();
136
137                variables.put("context", getContext(component));
138
139                // set falsy JS values for the non-used parameters
140                if (Strings.isEmpty(resourceName))
141                {
142                        int pageId = component.getPage().getPageId();
143                        variables.put("pageId", pageId);
144                        variables.put("resourceName", "");
145                        variables.put("connectionToken", "");
146                }
147                else
148                {
149                        variables.put("resourceName", resourceName);
150                        variables.put("connectionToken", connectionToken);
151                        variables.put("pageId", false);
152                }
153
154                WebSocketSettings webSocketSettings = WebSocketSettings.Holder.get(component.getApplication());
155
156                CharSequence baseUrl = getBaseUrl(webSocketSettings);
157                Args.notNull(baseUrl, "baseUrl");
158                variables.put("baseUrl", baseUrl);
159
160                Integer port = getPort(webSocketSettings);
161                variables.put("port", port);
162                Integer securePort = getSecurePort(webSocketSettings);
163                variables.put("securePort", securePort);
164
165                CharSequence contextPath = getContextPath(webSocketSettings);
166                Args.notNull(contextPath, "contextPath");
167                variables.put("contextPath", contextPath);
168
169                // preserve the application name for JSR356 based impl
170                variables.put("applicationName", component.getApplication().getName());
171
172                CharSequence filterPrefix = getFilterPrefix(webSocketSettings);
173                Args.notNull(filterPrefix, "filterPrefix");
174                variables.put("filterPrefix", filterPrefix);
175
176                final CharSequence sessionId = getSessionId(component);
177                variables.put("sessionId", sessionId);
178                return variables;
179        }
180
181        protected Integer getPort(WebSocketSettings webSocketSettings)
182        {
183                return webSocketSettings.getPort();
184        }
185
186        protected Integer getSecurePort(WebSocketSettings webSocketSettings)
187        {
188                return webSocketSettings.getSecurePort();
189        }
190
191        protected CharSequence getFilterPrefix(final WebSocketSettings webSocketSettings) {
192                return webSocketSettings.getFilterPrefix();
193        }
194
195        protected CharSequence getContextPath(final WebSocketSettings webSocketSettings) {
196                return webSocketSettings.getContextPath();
197        }
198
199        protected CharSequence getBaseUrl(final WebSocketSettings webSocketSettings) {
200                return webSocketSettings.getBaseUrl();
201        }
202
203        /**
204         * @param component
205         *          The component this behavior is bound to
206         * @return The http session id if it is tracked in the url, otherwise empty string
207         */
208        protected CharSequence getSessionId(final Component component)
209        {
210                String sessionId = "";
211                final WebApplication application = (WebApplication) component.getApplication();
212                final Set<SessionTrackingMode> effectiveSessionTrackingModes = application.getServletContext().getEffectiveSessionTrackingModes();
213                Object containerRequest = component.getRequest().getContainerRequest();
214                if (effectiveSessionTrackingModes.size() == 1 && SessionTrackingMode.URL.equals(effectiveSessionTrackingModes.iterator().next()))
215                {
216                        sessionId = component.getSession().getId();
217                }
218                else if (containerRequest instanceof HttpServletRequest)
219                {
220                        CookieUtils cookieUtils = new CookieUtils();
221                        final String jsessionCookieName = cookieUtils.getSessionIdCookieName(application);
222                        final Cookie jsessionid = cookieUtils.getCookie(jsessionCookieName);
223                        HttpServletRequest httpServletRequest = (HttpServletRequest) containerRequest;
224                        if (jsessionid == null || httpServletRequest.isRequestedSessionIdValid() == false)
225                        {
226                                sessionId = component.getSession().getId();
227                        }
228                }
229                return sessionId;
230        }
231
232        @Override
233        public boolean getStatelessHint(Component component)
234        {
235                return false;
236        }
237}