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.javax;
018
019import java.io.EOFException;
020import java.io.IOException;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.concurrent.ConcurrentHashMap;
025
026import javax.websocket.CloseReason;
027import javax.websocket.Endpoint;
028import javax.websocket.EndpointConfig;
029import javax.websocket.Session;
030
031import org.apache.wicket.Application;
032import org.apache.wicket.IApplicationListener;
033import org.apache.wicket.ThreadContext;
034import org.apache.wicket.protocol.http.WebApplication;
035import org.apache.wicket.util.lang.Checks;
036import org.apache.wicket.util.string.Strings;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * JSR 356 WebSocket Endpoint that integrates with Wicket Native WebSocket's IWebSocketProcessor
042 */
043public class WicketEndpoint extends Endpoint
044{
045        private static final Logger LOG = LoggerFactory.getLogger(WicketEndpoint.class);
046
047        /**
048         * A set of started applications for which this endpoint is registered.
049         */
050        private static final Set<String> RUNNING_APPLICATIONS = ConcurrentHashMap.newKeySet();
051
052        /**
053         * The name of the request parameter that holds the application name
054         */
055        private static final String WICKET_APP_PARAM_NAME = "wicket-app-name";
056
057        private String applicationName;
058        private JavaxWebSocketProcessor javaxWebSocketProcessor;
059
060        @Override
061        public void onOpen(Session session, EndpointConfig endpointConfig)
062        {
063                applicationName = getApplicationName(session);
064
065                WebApplication app = (WebApplication)WebApplication.get(applicationName);
066                if (RUNNING_APPLICATIONS.add(applicationName))
067                {
068                        app.getApplicationListeners().add(new ApplicationListener());
069                }
070
071                try
072                {
073                        ThreadContext.setApplication(app);
074                        javaxWebSocketProcessor = new JavaxWebSocketProcessor(session, app, endpointConfig);
075                        javaxWebSocketProcessor.onOpen(new JavaxWebSocketSession(session), app);
076                }
077                finally
078                {
079                        ThreadContext.detach();
080                }
081        }
082
083        @Override
084        public void onClose(Session session, CloseReason closeReason)
085        {
086                super.onClose(session, closeReason);
087
088                final int closeCode = closeReason.getCloseCode().getCode();
089                final String reasonPhrase = closeReason.getReasonPhrase();
090
091                LOG.debug("Web Socket connection with id '{}' has been closed with code '{}' and reason: {}",
092                                session.getId(), closeCode, reasonPhrase);
093
094                if (javaxWebSocketProcessor != null && isApplicationAlive(applicationName))
095                {
096                        javaxWebSocketProcessor.onClose(closeCode, reasonPhrase);
097                }
098        }
099
100        @Override
101        public void onError(Session session, Throwable t)
102        {
103                if (isIgnorableError(t))
104                {
105                        LOG.debug("An error occurred in web socket connection with id : {}", session.getId(), t);
106                }
107                else
108                {
109                        LOG.error("An error occurred in web socket connection with id : {}", session.getId(), t);
110                }
111
112                super.onError(session, t);
113
114                if (javaxWebSocketProcessor != null && isApplicationAlive(applicationName))
115                {
116                        javaxWebSocketProcessor.onError(t);
117                }
118        }
119
120        private boolean isIgnorableError(Throwable t)
121        {
122                return
123                        t instanceof EOFException ||
124                    (t instanceof IOException && "Broken pipe".equals(t.getMessage()));
125        }
126
127        private boolean isApplicationAlive(String appName)
128        {
129                return RUNNING_APPLICATIONS.contains(appName);
130        }
131
132        private String getApplicationName(Session session)
133        {
134                String appName = null;
135
136                Map<String, List<String>> parameters = session.getRequestParameterMap();
137                if (parameters != null)
138                {
139                        appName = parameters.get(WICKET_APP_PARAM_NAME).get(0);
140                }
141                else
142                {
143                        // Glassfish 4 has null parameters map and non-null query string ...
144                        String queryString = session.getQueryString();
145                        if (!Strings.isEmpty(queryString))
146                        {
147                                String[] params = Strings.split(queryString, '&');
148                                for (String paramPair : params)
149                                {
150                                        String[] nameValues = Strings.split(paramPair, '=');
151                                        if (WICKET_APP_PARAM_NAME.equals(nameValues[0]))
152                                        {
153                                                appName = nameValues[1];
154                                        }
155                                }
156                        }
157                }
158
159                Checks.notNull(appName, "The application name cannot be read from the upgrade request's parameters");
160
161                return appName;
162        }
163
164        private static class ApplicationListener implements IApplicationListener
165        {
166                @Override
167                public void onBeforeDestroyed(Application application)
168                {
169                        String appName = application.getName();
170                        RUNNING_APPLICATIONS.remove(appName);
171                        application.getApplicationListeners().remove(this);
172                }
173        }
174}