57 changed files with 3220 additions and 50 deletions
@ -0,0 +1,25 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.http; |
||||||
|
|
||||||
|
|
||||||
|
public interface Cookie { |
||||||
|
|
||||||
|
String getName(); |
||||||
|
|
||||||
|
String getValue(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.http; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
|
||||||
|
public class Cookies { |
||||||
|
|
||||||
|
private final List<Cookie> cookies; |
||||||
|
|
||||||
|
|
||||||
|
public Cookies() { |
||||||
|
this.cookies = new ArrayList<Cookie>(); |
||||||
|
} |
||||||
|
|
||||||
|
private Cookies(Cookies cookies) { |
||||||
|
this.cookies = Collections.unmodifiableList(cookies.getCookies()); |
||||||
|
} |
||||||
|
|
||||||
|
public static Cookies readOnlyCookies(Cookies cookies) { |
||||||
|
return new Cookies(cookies); |
||||||
|
} |
||||||
|
|
||||||
|
public List<Cookie> getCookies() { |
||||||
|
return this.cookies; |
||||||
|
} |
||||||
|
|
||||||
|
public Cookie getCookie(String name) { |
||||||
|
for (Cookie c : this.cookies) { |
||||||
|
if (c.getName().equals(name)) { |
||||||
|
return c; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public Cookie addCookie(String name, String value) { |
||||||
|
DefaultCookie cookie = new DefaultCookie(name, value); |
||||||
|
this.cookies.add(cookie); |
||||||
|
return cookie; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.http; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
public class DefaultCookie implements Cookie { |
||||||
|
|
||||||
|
private final String name; |
||||||
|
|
||||||
|
private final String value; |
||||||
|
|
||||||
|
DefaultCookie(String name, String value) { |
||||||
|
Assert.hasText(name, "cookie name must not be empty"); |
||||||
|
this.name = name; |
||||||
|
this.value = value; |
||||||
|
} |
||||||
|
|
||||||
|
public String getName() { |
||||||
|
return name; |
||||||
|
} |
||||||
|
|
||||||
|
public String getValue() { |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.http.server; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO.. |
||||||
|
*/ |
||||||
|
public interface AsyncServerHttpRequest extends ServerHttpRequest { |
||||||
|
|
||||||
|
void setTimeout(long timeout); |
||||||
|
|
||||||
|
void startAsync(); |
||||||
|
|
||||||
|
boolean isAsyncStarted(); |
||||||
|
|
||||||
|
void completeAsync(); |
||||||
|
|
||||||
|
boolean isAsyncCompleted(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.http.server; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
|
|
||||||
|
import javax.servlet.AsyncContext; |
||||||
|
import javax.servlet.AsyncEvent; |
||||||
|
import javax.servlet.AsyncListener; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
|
||||||
|
public class AsyncServletServerHttpRequest extends ServletServerHttpRequest |
||||||
|
implements AsyncServerHttpRequest, AsyncListener { |
||||||
|
|
||||||
|
private Long timeout; |
||||||
|
|
||||||
|
private AsyncContext asyncContext; |
||||||
|
|
||||||
|
private AtomicBoolean asyncCompleted = new AtomicBoolean(false); |
||||||
|
|
||||||
|
private final List<Runnable> timeoutHandlers = new ArrayList<Runnable>(); |
||||||
|
|
||||||
|
private final List<Runnable> completionHandlers = new ArrayList<Runnable>(); |
||||||
|
|
||||||
|
private final HttpServletResponse servletResponse; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new instance for the given request/response pair. |
||||||
|
*/ |
||||||
|
public AsyncServletServerHttpRequest(HttpServletRequest request, HttpServletResponse response) { |
||||||
|
super(request); |
||||||
|
this.servletResponse = response; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Timeout period begins after the container thread has exited. |
||||||
|
*/ |
||||||
|
public void setTimeout(long timeout) { |
||||||
|
Assert.state(!isAsyncStarted(), "Cannot change the timeout with concurrent handling in progress"); |
||||||
|
this.timeout = timeout; |
||||||
|
} |
||||||
|
|
||||||
|
public void addTimeoutHandler(Runnable timeoutHandler) { |
||||||
|
this.timeoutHandlers.add(timeoutHandler); |
||||||
|
} |
||||||
|
|
||||||
|
public void addCompletionHandler(Runnable runnable) { |
||||||
|
this.completionHandlers.add(runnable); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isAsyncStarted() { |
||||||
|
return ((this.asyncContext != null) && getServletRequest().isAsyncStarted()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether async request processing has completed. |
||||||
|
* <p>It is important to avoid use of request and response objects after async |
||||||
|
* processing has completed. Servlet containers often re-use them. |
||||||
|
*/ |
||||||
|
public boolean isAsyncCompleted() { |
||||||
|
return this.asyncCompleted.get(); |
||||||
|
} |
||||||
|
|
||||||
|
public void startAsync() { |
||||||
|
Assert.state(getServletRequest().isAsyncSupported(), |
||||||
|
"Async support must be enabled on a servlet and for all filters involved " + |
||||||
|
"in async request processing. This is done in Java code using the Servlet API " + |
||||||
|
"or by adding \"<async-supported>true</async-supported>\" to servlet and " + |
||||||
|
"filter declarations in web.xml."); |
||||||
|
Assert.state(!isAsyncCompleted(), "Async processing has already completed"); |
||||||
|
if (isAsyncStarted()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.asyncContext = getServletRequest().startAsync(getServletRequest(), this.servletResponse); |
||||||
|
this.asyncContext.addListener(this); |
||||||
|
if (this.timeout != null) { |
||||||
|
this.asyncContext.setTimeout(this.timeout); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void dispatch() { |
||||||
|
Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext"); |
||||||
|
this.asyncContext.dispatch(); |
||||||
|
} |
||||||
|
|
||||||
|
public void completeAsync() { |
||||||
|
Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext"); |
||||||
|
if (isAsyncStarted() && !isAsyncCompleted()) { |
||||||
|
this.asyncContext.complete(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Implementation of AsyncListener methods
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
public void onStartAsync(AsyncEvent event) throws IOException { |
||||||
|
} |
||||||
|
|
||||||
|
public void onError(AsyncEvent event) throws IOException { |
||||||
|
} |
||||||
|
|
||||||
|
public void onTimeout(AsyncEvent event) throws IOException { |
||||||
|
for (Runnable handler : this.timeoutHandlers) { |
||||||
|
handler.run(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void onComplete(AsyncEvent event) throws IOException { |
||||||
|
for (Runnable handler : this.completionHandlers) { |
||||||
|
handler.run(); |
||||||
|
} |
||||||
|
this.asyncContext = null; |
||||||
|
this.asyncCompleted.set(true); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface SockJsHandler { |
||||||
|
|
||||||
|
void newSession(SockJsSession session) throws Exception; |
||||||
|
|
||||||
|
void handleMessage(SockJsSession session, String message) throws Exception; |
||||||
|
|
||||||
|
void handleException(SockJsSession session, Throwable exception); |
||||||
|
|
||||||
|
void sessionClosed(SockJsSession session); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class SockJsHandlerAdapter implements SockJsHandler { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void newSession(SockJsSession session) throws Exception { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleMessage(SockJsSession session, String message) throws Exception { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleException(SockJsSession session, Throwable exception) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sessionClosed(SockJsSession session) { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface SockJsSession { |
||||||
|
|
||||||
|
void sendMessage(String text) throws Exception; |
||||||
|
|
||||||
|
void close(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,128 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class SockJsSessionSupport implements SockJsSession { |
||||||
|
|
||||||
|
protected Log logger = LogFactory.getLog(this.getClass()); |
||||||
|
|
||||||
|
private final String sessionId; |
||||||
|
|
||||||
|
private final SockJsHandler delegate; |
||||||
|
|
||||||
|
private State state = State.NEW; |
||||||
|
|
||||||
|
private long timeCreated = System.currentTimeMillis(); |
||||||
|
|
||||||
|
private long timeLastActive = System.currentTimeMillis(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param sessionId |
||||||
|
* @param delegate the recipient of SockJS messages |
||||||
|
*/ |
||||||
|
public SockJsSessionSupport(String sessionId, SockJsHandler delegate) { |
||||||
|
Assert.notNull(sessionId, "sessionId is required"); |
||||||
|
Assert.notNull(delegate, "SockJsHandler is required"); |
||||||
|
this.sessionId = sessionId; |
||||||
|
this.delegate = delegate; |
||||||
|
} |
||||||
|
|
||||||
|
public String getId() { |
||||||
|
return this.sessionId; |
||||||
|
} |
||||||
|
|
||||||
|
public SockJsHandler getSockJsHandler() { |
||||||
|
return this.delegate; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isNew() { |
||||||
|
return State.NEW.equals(this.state); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isOpen() { |
||||||
|
return State.OPEN.equals(this.state); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isClosed() { |
||||||
|
return State.CLOSED.equals(this.state); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Polling and Streaming sessions periodically close the current HTTP request and |
||||||
|
* wait for the next request to come through. During this "downtime" the session is |
||||||
|
* still open but inactive and unable to send messages and therefore has to buffer |
||||||
|
* them temporarily. A WebSocket session by contrast is stateful and remain active |
||||||
|
* until closed. |
||||||
|
*/ |
||||||
|
public abstract boolean isActive(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the time since the session was last active, or otherwise if the |
||||||
|
* session is new, the time since the session was created. |
||||||
|
*/ |
||||||
|
public long getTimeSinceLastActive() { |
||||||
|
if (isNew()) { |
||||||
|
return (System.currentTimeMillis() - this.timeCreated); |
||||||
|
} |
||||||
|
else { |
||||||
|
return isActive() ? 0 : System.currentTimeMillis() - this.timeLastActive; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Should be invoked whenever the session becomes inactive. |
||||||
|
*/ |
||||||
|
protected void updateLastActiveTime() { |
||||||
|
this.timeLastActive = System.currentTimeMillis(); |
||||||
|
} |
||||||
|
|
||||||
|
public void connectionInitialized() throws Exception { |
||||||
|
this.state = State.OPEN; |
||||||
|
this.delegate.newSession(this); |
||||||
|
} |
||||||
|
|
||||||
|
public void delegateMessages(String... messages) throws Exception { |
||||||
|
for (String message : messages) { |
||||||
|
this.delegate.handleMessage(this, message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void close() { |
||||||
|
this.state = State.CLOSED; |
||||||
|
} |
||||||
|
|
||||||
|
public String toString() { |
||||||
|
return getClass().getSimpleName() + " [id=" + sessionId + "]"; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private enum State { NEW, OPEN, CLOSED } |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs; |
||||||
|
|
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public enum TransportType { |
||||||
|
|
||||||
|
WEBSOCKET("websocket", HttpMethod.GET, false /* CORS ? */), |
||||||
|
|
||||||
|
XHR("xhr", HttpMethod.POST, true), |
||||||
|
XHR_SEND("xhr_send", HttpMethod.POST, true), |
||||||
|
|
||||||
|
JSONP("jsonp", HttpMethod.GET, false), |
||||||
|
JSONP_SEND("jsonp_send", HttpMethod.POST, false), |
||||||
|
|
||||||
|
XHR_STREAMING("xhr_streaming", HttpMethod.POST, true), |
||||||
|
EVENT_SOURCE("eventsource", HttpMethod.GET, false), |
||||||
|
HTML_FILE("htmlfile", HttpMethod.GET, false); |
||||||
|
|
||||||
|
|
||||||
|
private final String value; |
||||||
|
|
||||||
|
private final HttpMethod httpMethod; |
||||||
|
|
||||||
|
private final boolean corsSupported; |
||||||
|
|
||||||
|
|
||||||
|
private TransportType(String value, HttpMethod httpMethod, boolean supportsCors) { |
||||||
|
this.value = value; |
||||||
|
this.httpMethod = httpMethod; |
||||||
|
this.corsSupported = supportsCors; |
||||||
|
} |
||||||
|
|
||||||
|
public String value() { |
||||||
|
return this.value; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The HTTP method for this transport. |
||||||
|
*/ |
||||||
|
public HttpMethod getHttpMethod() { |
||||||
|
return this.httpMethod; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Are cross-domain requests (CORS) supported? |
||||||
|
*/ |
||||||
|
public boolean isCorsSupported() { |
||||||
|
return this.corsSupported; |
||||||
|
} |
||||||
|
|
||||||
|
public static TransportType fromValue(String transportValue) { |
||||||
|
for (TransportType type : values()) { |
||||||
|
if (type.value().equals(transportValue)) { |
||||||
|
return type; |
||||||
|
} |
||||||
|
} |
||||||
|
throw new IllegalArgumentException("No matching constant for [" + transportValue + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return this.value; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,151 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import java.io.EOFException; |
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.concurrent.ScheduledFuture; |
||||||
|
|
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSession; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Provides partial implementations of {@link SockJsSession} methods to send messages, |
||||||
|
* including heartbeat messages and to manage session state. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractServerSession extends SockJsSessionSupport { |
||||||
|
|
||||||
|
private final SockJsConfiguration sockJsConfig; |
||||||
|
|
||||||
|
private ScheduledFuture<?> heartbeatTask; |
||||||
|
|
||||||
|
|
||||||
|
public AbstractServerSession(String sessionId, SockJsHandler delegate, SockJsConfiguration sockJsConfig) { |
||||||
|
super(sessionId, delegate); |
||||||
|
Assert.notNull(sockJsConfig, "sockJsConfig is required"); |
||||||
|
this.sockJsConfig = sockJsConfig; |
||||||
|
} |
||||||
|
|
||||||
|
public SockJsConfiguration getSockJsConfig() { |
||||||
|
return this.sockJsConfig; |
||||||
|
} |
||||||
|
|
||||||
|
public final synchronized void sendMessage(String message) { |
||||||
|
Assert.isTrue(!isClosed(), "Cannot send a message, session has been closed"); |
||||||
|
sendMessageInternal(message); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void sendMessageInternal(String message); |
||||||
|
|
||||||
|
public final synchronized void close() { |
||||||
|
if (!isClosed()) { |
||||||
|
logger.debug("Closing session"); |
||||||
|
|
||||||
|
// set the status
|
||||||
|
super.close(); |
||||||
|
|
||||||
|
if (isActive()) { |
||||||
|
// deliver messages "in flight" before sending close frame
|
||||||
|
writeFrame(SockJsFrame.closeFrameGoAway()); |
||||||
|
} |
||||||
|
|
||||||
|
cancelHeartbeat(); |
||||||
|
closeInternal(); |
||||||
|
|
||||||
|
getSockJsHandler().sessionClosed(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void closeInternal(); |
||||||
|
|
||||||
|
/** |
||||||
|
* For internal use within a TransportHandler and the (TransportHandler-specific) |
||||||
|
* session sub-class. The frame is written only if the connection is active. |
||||||
|
*/ |
||||||
|
protected void writeFrame(SockJsFrame frame) { |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Preparing to write " + frame); |
||||||
|
} |
||||||
|
try { |
||||||
|
writeFrameInternal(frame); |
||||||
|
} |
||||||
|
catch (EOFException ex) { |
||||||
|
logger.warn("Failed to send message due to client disconnect. Terminating connection abruptly"); |
||||||
|
deactivate(); |
||||||
|
close(); |
||||||
|
} |
||||||
|
catch (Throwable t) { |
||||||
|
logger.error("Failed to send message. Terminating connection abruptly", t); |
||||||
|
deactivate(); |
||||||
|
close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Some {@link TransportHandler} types cannot detect if a client connection is closed |
||||||
|
* or lost and will eventually fail to send messages. When that happens, we need a way |
||||||
|
* to disconnect the underlying connection before calling {@link #close()}. |
||||||
|
*/ |
||||||
|
protected abstract void deactivate(); |
||||||
|
|
||||||
|
public synchronized void sendHeartbeat() { |
||||||
|
if (isActive()) { |
||||||
|
writeFrame(SockJsFrame.heartbeatFrame()); |
||||||
|
scheduleHeartbeat(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void scheduleHeartbeat() { |
||||||
|
Assert.notNull(getSockJsConfig().getHeartbeatScheduler(), "heartbeatScheduler not configured"); |
||||||
|
cancelHeartbeat(); |
||||||
|
if (!isActive()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
Date time = new Date(System.currentTimeMillis() + getSockJsConfig().getHeartbeatTime()); |
||||||
|
this.heartbeatTask = getSockJsConfig().getHeartbeatScheduler().schedule(new Runnable() { |
||||||
|
public void run() { |
||||||
|
sendHeartbeat(); |
||||||
|
} |
||||||
|
}, time); |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Scheduled heartbeat after " + getSockJsConfig().getHeartbeatTime()/1000 + " seconds"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void cancelHeartbeat() { |
||||||
|
if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) { |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Cancelling heartbeat"); |
||||||
|
} |
||||||
|
this.heartbeatTask.cancel(false); |
||||||
|
} |
||||||
|
this.heartbeatTask = null; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,431 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Random; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.scheduling.TaskScheduler; |
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.support.DefaultTransportHandlerRegistrar; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.util.DigestUtils; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.websocket.server.HandshakeRequestHandler; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Provides support for SockJS configuration options and serves the static SockJS URLs. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractSockJsService implements SockJsConfiguration { |
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
private static final int ONE_YEAR = 365 * 24 * 60 * 60; |
||||||
|
|
||||||
|
|
||||||
|
private String sockJsServiceName = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); |
||||||
|
|
||||||
|
private String clientLibraryUrl = "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js"; |
||||||
|
|
||||||
|
private int streamBytesLimit = 128 * 1024; |
||||||
|
|
||||||
|
private boolean jsessionIdCookieNeeded = true; |
||||||
|
|
||||||
|
private long heartbeatTime = 25 * 1000; |
||||||
|
|
||||||
|
private TaskScheduler heartbeatScheduler; |
||||||
|
|
||||||
|
private long disconnectDelay = 5 * 1000; |
||||||
|
|
||||||
|
private boolean webSocketsEnabled = true; |
||||||
|
|
||||||
|
private HandshakeRequestHandler handshakeRequestHandler; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Class constructor... |
||||||
|
* |
||||||
|
*/ |
||||||
|
public AbstractSockJsService() { |
||||||
|
this.heartbeatScheduler = createScheduler("SockJs-heartbeat-"); |
||||||
|
} |
||||||
|
|
||||||
|
protected TaskScheduler createScheduler(String threadNamePrefix) { |
||||||
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); |
||||||
|
scheduler.setThreadNamePrefix(threadNamePrefix); |
||||||
|
return scheduler; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A unique name for the service, possibly the prefix at which it is deployed. |
||||||
|
* Used mainly for logging purposes. |
||||||
|
*/ |
||||||
|
public void setSockJsServiceName(String serviceName) { |
||||||
|
this.sockJsServiceName = serviceName; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The SockJS service name. |
||||||
|
* @see #setSockJsServiceName(String) |
||||||
|
*/ |
||||||
|
public String getSockJsServiceName() { |
||||||
|
return this.sockJsServiceName; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Transports which don't support cross-domain communication natively (e.g. |
||||||
|
* "eventsource", "htmlfile") rely on serving a simple page (using the |
||||||
|
* "foreign" domain) from an invisible iframe. Code run from this iframe |
||||||
|
* doesn't need to worry about cross-domain issues since it is running from |
||||||
|
* a domain local to the SockJS server. The iframe does need to load the |
||||||
|
* SockJS javascript client library and this option allows configuring its |
||||||
|
* url. |
||||||
|
* <p> |
||||||
|
* By default this is set to point to |
||||||
|
* "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js". |
||||||
|
*/ |
||||||
|
public AbstractSockJsService setSockJsClientLibraryUrl(String clientLibraryUrl) { |
||||||
|
this.clientLibraryUrl = clientLibraryUrl; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The URL to the SockJS JavaScript client library. |
||||||
|
* @see #setSockJsClientLibraryUrl(String) |
||||||
|
*/ |
||||||
|
public String getSockJsClientLibraryUrl() { |
||||||
|
return this.clientLibraryUrl; |
||||||
|
} |
||||||
|
|
||||||
|
public AbstractSockJsService setStreamBytesLimit(int streamBytesLimit) { |
||||||
|
this.streamBytesLimit = streamBytesLimit; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public int getStreamBytesLimit() { |
||||||
|
return streamBytesLimit; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Some load balancers do sticky sessions, but only if there is a JSESSIONID |
||||||
|
* cookie. Even if it is set to a dummy value, it doesn't matter since |
||||||
|
* session information is added by the load balancer. |
||||||
|
* <p> |
||||||
|
* Set this option to indicate if a JSESSIONID cookie should be created. The |
||||||
|
* default value is "true". |
||||||
|
*/ |
||||||
|
public AbstractSockJsService setJsessionIdCookieNeeded(boolean jsessionIdCookieNeeded) { |
||||||
|
this.jsessionIdCookieNeeded = jsessionIdCookieNeeded; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether setting JSESSIONID cookie is necessary. |
||||||
|
* @see #setJsessionIdCookieNeeded(boolean) |
||||||
|
*/ |
||||||
|
public boolean isJsessionIdCookieNeeded() { |
||||||
|
return this.jsessionIdCookieNeeded; |
||||||
|
} |
||||||
|
|
||||||
|
public AbstractSockJsService setHeartbeatTime(long heartbeatTime) { |
||||||
|
this.heartbeatTime = heartbeatTime; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public long getHeartbeatTime() { |
||||||
|
return this.heartbeatTime; |
||||||
|
} |
||||||
|
|
||||||
|
public TaskScheduler getHeartbeatScheduler() { |
||||||
|
return this.heartbeatScheduler; |
||||||
|
} |
||||||
|
|
||||||
|
public void setHeartbeatScheduler(TaskScheduler heartbeatScheduler) { |
||||||
|
Assert.notNull(heartbeatScheduler, "heartbeatScheduler is required"); |
||||||
|
this.heartbeatScheduler = heartbeatScheduler; |
||||||
|
} |
||||||
|
|
||||||
|
public AbstractSockJsService setDisconnectDelay(long disconnectDelay) { |
||||||
|
this.disconnectDelay = disconnectDelay; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public long getDisconnectDelay() { |
||||||
|
return this.disconnectDelay; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Some load balancers don't support websockets. This option can be used to |
||||||
|
* disable the WebSocket transport on the server side. |
||||||
|
* <p> |
||||||
|
* The default value is "true". |
||||||
|
*/ |
||||||
|
public AbstractSockJsService setWebSocketsEnabled(boolean webSocketsEnabled) { |
||||||
|
this.webSocketsEnabled = webSocketsEnabled; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether WebSocket transport is enabled. |
||||||
|
* @see #setWebSocketsEnabled(boolean) |
||||||
|
*/ |
||||||
|
public boolean isWebSocketsEnabled() { |
||||||
|
return this.webSocketsEnabled; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* SockJS exposes an entry point at "/websocket" for raw WebSocket |
||||||
|
* communication without additional custom framing, e.g. no open frame, no |
||||||
|
* heartbeats, only raw WebSocket protocol. This property allows setting a |
||||||
|
* handler for requests for raw WebSocket communication. |
||||||
|
*/ |
||||||
|
public AbstractSockJsService setWebsocketHandler(HandshakeRequestHandler handshakeRequestHandler) { |
||||||
|
this.handshakeRequestHandler = handshakeRequestHandler; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @param request |
||||||
|
* @param response |
||||||
|
* @param sockJsPath |
||||||
|
* |
||||||
|
* @throws Exception |
||||||
|
*/ |
||||||
|
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, String sockJsPath) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
logger.debug(request.getMethod() + " [" + sockJsPath + "]"); |
||||||
|
|
||||||
|
try { |
||||||
|
request.getHeaders(); |
||||||
|
} |
||||||
|
catch (IllegalArgumentException ex) { |
||||||
|
// Ignore invalid Content-Type (TODO!!)
|
||||||
|
} |
||||||
|
|
||||||
|
if (sockJsPath.equals("") || sockJsPath.equals("/")) { |
||||||
|
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8"))); |
||||||
|
response.getBody().write("Welcome to SockJS!\n".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
else if (sockJsPath.equals("/info")) { |
||||||
|
this.infoHandler.handle(request, response); |
||||||
|
return; |
||||||
|
} |
||||||
|
else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) { |
||||||
|
this.iframeHandler.handle(request, response); |
||||||
|
return; |
||||||
|
} |
||||||
|
else if (sockJsPath.equals("/websocket")) { |
||||||
|
Assert.notNull(this.handshakeRequestHandler, "No handler for raw Websockets configured"); |
||||||
|
this.handshakeRequestHandler.doHandshake(request, response); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/"); |
||||||
|
if (pathSegments.length != 3) { |
||||||
|
logger.debug("Expected /{server}/{session}/{transport} but got " + sockJsPath); |
||||||
|
response.setStatusCode(HttpStatus.NOT_FOUND); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
String serverId = pathSegments[0]; |
||||||
|
String sessionId = pathSegments[1]; |
||||||
|
String transport = pathSegments[2]; |
||||||
|
|
||||||
|
if (!validateRequest(serverId, sessionId, transport)) { |
||||||
|
response.setStatusCode(HttpStatus.NOT_FOUND); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
handleRequestInternal(request, response, sessionId, TransportType.fromValue(transport)); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
protected boolean validateRequest(String serverId, String sessionId, String transport) { |
||||||
|
|
||||||
|
if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) { |
||||||
|
logger.debug("Empty server, session, or transport value"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Server and session id's must not contain "."
|
||||||
|
if (serverId.contains(".") || sessionId.contains(".")) { |
||||||
|
logger.debug("Server or session contain a \".\""); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (!isWebSocketsEnabled() && transport.equals(TransportType.WEBSOCKET.value())) { |
||||||
|
logger.debug("Websocket transport is disabled"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
String sessionId, TransportType transportType) throws Exception; |
||||||
|
|
||||||
|
protected void addCorsHeaders(ServerHttpRequest request, ServerHttpResponse response, HttpMethod... httpMethods) { |
||||||
|
|
||||||
|
String origin = request.getHeaders().getFirst("origin"); |
||||||
|
origin = ((origin == null) || origin.equals("null")) ? "*" : origin; |
||||||
|
|
||||||
|
response.getHeaders().add("Access-Control-Allow-Origin", origin); |
||||||
|
response.getHeaders().add("Access-Control-Allow-Credentials", "true"); |
||||||
|
|
||||||
|
List<String> accessControllerHeaders = request.getHeaders().get("Access-Control-Request-Headers"); |
||||||
|
if (accessControllerHeaders != null) { |
||||||
|
for (String header : accessControllerHeaders) { |
||||||
|
response.getHeaders().add("Access-Control-Allow-Headers", header); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!ObjectUtils.isEmpty(httpMethods)) { |
||||||
|
response.getHeaders().add("Access-Control-Allow-Methods", StringUtils.arrayToDelimitedString(httpMethods, ", ")); |
||||||
|
response.getHeaders().add("Access-Control-Max-Age", String.valueOf(ONE_YEAR)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void addCacheHeaders(ServerHttpResponse response) { |
||||||
|
response.getHeaders().setCacheControl("public, max-age=" + ONE_YEAR); |
||||||
|
response.getHeaders().setExpires(new Date().getTime() + ONE_YEAR * 1000); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addNoCacheHeaders(ServerHttpResponse response) { |
||||||
|
response.getHeaders().setCacheControl("no-store, no-cache, must-revalidate, max-age=0"); |
||||||
|
} |
||||||
|
|
||||||
|
protected void sendMethodNotAllowed(ServerHttpResponse response, List<HttpMethod> httpMethods) throws IOException { |
||||||
|
logger.debug("Sending Method Not Allowed (405)"); |
||||||
|
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED); |
||||||
|
response.getHeaders().setAllow(new HashSet<HttpMethod>(httpMethods)); |
||||||
|
response.getBody(); // ensure headers are flushed (TODO!)
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private interface SockJsRequestHandler { |
||||||
|
|
||||||
|
void handle(ServerHttpRequest request, ServerHttpResponse response) throws Exception; |
||||||
|
} |
||||||
|
|
||||||
|
private static final Random random = new Random(); |
||||||
|
|
||||||
|
private final SockJsRequestHandler infoHandler = new SockJsRequestHandler() { |
||||||
|
|
||||||
|
private static final String INFO_CONTENT = |
||||||
|
"{\"entropy\":%s,\"origins\":[\"*:*\"],\"cookie_needed\":%s,\"websocket\":%s}"; |
||||||
|
|
||||||
|
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws Exception { |
||||||
|
|
||||||
|
if (HttpMethod.GET.equals(request.getMethod())) { |
||||||
|
|
||||||
|
response.getHeaders().setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); |
||||||
|
|
||||||
|
addCorsHeaders(request, response); |
||||||
|
addNoCacheHeaders(response); |
||||||
|
|
||||||
|
String content = String.format(INFO_CONTENT, random.nextInt(), isJsessionIdCookieNeeded(), isWebSocketsEnabled()); |
||||||
|
response.getBody().write(content.getBytes()); |
||||||
|
} |
||||||
|
else if (HttpMethod.OPTIONS.equals(request.getMethod())) { |
||||||
|
|
||||||
|
response.setStatusCode(HttpStatus.NO_CONTENT); |
||||||
|
|
||||||
|
addCorsHeaders(request, response, HttpMethod.GET, HttpMethod.OPTIONS); |
||||||
|
addCacheHeaders(response); |
||||||
|
|
||||||
|
response.getBody(); // ensure headers are flushed (TODO!)
|
||||||
|
} |
||||||
|
else { |
||||||
|
sendMethodNotAllowed(response, Arrays.asList(HttpMethod.OPTIONS, HttpMethod.GET)); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private final SockJsRequestHandler iframeHandler = new SockJsRequestHandler() { |
||||||
|
|
||||||
|
private static final String IFRAME_CONTENT = |
||||||
|
"<!DOCTYPE html>\n" + |
||||||
|
"<html>\n" + |
||||||
|
"<head>\n" + |
||||||
|
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" + |
||||||
|
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + |
||||||
|
" <script>\n" + |
||||||
|
" document.domain = document.domain;\n" + |
||||||
|
" _sockjs_onload = function(){SockJS.bootstrap_iframe();};\n" + |
||||||
|
" </script>\n" + |
||||||
|
" <script src=\"%s\"></script>\n" + |
||||||
|
"</head>\n" + |
||||||
|
"<body>\n" + |
||||||
|
" <h2>Don't panic!</h2>\n" + |
||||||
|
" <p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>\n" + |
||||||
|
"</body>\n" + |
||||||
|
"</html>"; |
||||||
|
|
||||||
|
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws Exception { |
||||||
|
|
||||||
|
if (!HttpMethod.GET.equals(request.getMethod())) { |
||||||
|
sendMethodNotAllowed(response, Arrays.asList(HttpMethod.GET)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
String content = String.format(IFRAME_CONTENT, getSockJsClientLibraryUrl()); |
||||||
|
byte[] contentBytes = content.getBytes(Charset.forName("UTF-8")); |
||||||
|
StringBuilder builder = new StringBuilder("\"0"); |
||||||
|
DigestUtils.appendMd5DigestAsHex(contentBytes, builder); |
||||||
|
builder.append('"'); |
||||||
|
String etagValue = builder.toString(); |
||||||
|
|
||||||
|
List<String> ifNoneMatch = request.getHeaders().getIfNoneMatch(); |
||||||
|
if (!CollectionUtils.isEmpty(ifNoneMatch) && ifNoneMatch.get(0).equals(etagValue)) { |
||||||
|
response.setStatusCode(HttpStatus.NOT_MODIFIED); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
response.getHeaders().setContentType(new MediaType("text", "html", Charset.forName("UTF-8"))); |
||||||
|
response.getHeaders().setContentLength(contentBytes.length); |
||||||
|
|
||||||
|
addCacheHeaders(response); |
||||||
|
response.getHeaders().setETag(etagValue); |
||||||
|
response.getBody().write(contentBytes); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import org.springframework.scheduling.TaskScheduler; |
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface SockJsConfiguration { |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Streaming transports save responses on the client side and don't free |
||||||
|
* memory used by delivered messages. Such transports need to recycle the |
||||||
|
* connection once in a while. This property sets a minimum number of bytes |
||||||
|
* that can be send over a single HTTP streaming request before it will be |
||||||
|
* closed. After that client will open a new request. Setting this value to |
||||||
|
* one effectively disables streaming and will make streaming transports to |
||||||
|
* behave like polling transports. |
||||||
|
* <p> |
||||||
|
* The default value is 128K (i.e. 128 * 1024). |
||||||
|
*/ |
||||||
|
public int getStreamBytesLimit(); |
||||||
|
|
||||||
|
/** |
||||||
|
* The amount of time in milliseconds before a client is considered |
||||||
|
* disconnected after not having a receiving connection, i.e. an active |
||||||
|
* connection over which the server can send data to the client. |
||||||
|
* <p> |
||||||
|
* The default value is 5000. |
||||||
|
*/ |
||||||
|
public long getDisconnectDelay(); |
||||||
|
|
||||||
|
/** |
||||||
|
* The amount of time in milliseconds when the server has not sent any |
||||||
|
* messages and after which the server should send a heartbeat frame to the |
||||||
|
* client in order to keep the connection from breaking. |
||||||
|
* <p> |
||||||
|
* The default value is 25,000 (25 seconds). |
||||||
|
*/ |
||||||
|
public long getHeartbeatTime(); |
||||||
|
|
||||||
|
/** |
||||||
|
* A scheduler instance to use for scheduling heartbeat frames. |
||||||
|
* <p> |
||||||
|
* By default a {@link ThreadPoolTaskScheduler} with default settings is used. |
||||||
|
*/ |
||||||
|
public TaskScheduler getHeartbeatScheduler(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,167 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.io.JsonStringEncoder; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class SockJsFrame { |
||||||
|
|
||||||
|
private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o"); |
||||||
|
|
||||||
|
private static final SockJsFrame HEARTBEAT_FRAME = new SockJsFrame("h"); |
||||||
|
|
||||||
|
private static final SockJsFrame CLOSE_GO_AWAY_FRAME = closeFrame(3000, "Go away!"); |
||||||
|
|
||||||
|
private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN = closeFrame(2010, "Another connection still open"); |
||||||
|
|
||||||
|
|
||||||
|
private final String content; |
||||||
|
|
||||||
|
|
||||||
|
private SockJsFrame(String content) { |
||||||
|
this.content = content; |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame openFrame() { |
||||||
|
return OPEN_FRAME; |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame heartbeatFrame() { |
||||||
|
return HEARTBEAT_FRAME; |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame messageFrame(String... messages) { |
||||||
|
return new MessageFrame(messages); |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame closeFrameGoAway() { |
||||||
|
return CLOSE_GO_AWAY_FRAME; |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame closeFrameAnotherConnectionOpen() { |
||||||
|
return CLOSE_ANOTHER_CONNECTION_OPEN; |
||||||
|
} |
||||||
|
|
||||||
|
public static SockJsFrame closeFrame(int code, String reason) { |
||||||
|
return new SockJsFrame("c[" + code + ",\"" + reason + "\"]"); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public String getContent() { |
||||||
|
return this.content; |
||||||
|
} |
||||||
|
|
||||||
|
public byte[] getContentBytes() { |
||||||
|
return this.content.getBytes(Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
public String toString() { |
||||||
|
String quoted = this.content.replace("\n", "\\n").replace("\r", "\\r"); |
||||||
|
return "SockJsFrame content='" + quoted + "'"; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class MessageFrame extends SockJsFrame { |
||||||
|
|
||||||
|
public MessageFrame(String... messages) { |
||||||
|
super(prepareContent(messages)); |
||||||
|
} |
||||||
|
|
||||||
|
public static String prepareContent(String... messages) { |
||||||
|
Assert.notNull(messages, "messages required"); |
||||||
|
StringBuilder sb = new StringBuilder(); |
||||||
|
sb.append("a["); |
||||||
|
for (int i=0; i < messages.length; i++) { |
||||||
|
sb.append('"'); |
||||||
|
// TODO: dependency on Jackson
|
||||||
|
char[] quotedChars = JsonStringEncoder.getInstance().quoteAsString(messages[i]); |
||||||
|
sb.append(escapeSockJsCharacters(quotedChars)); |
||||||
|
sb.append('"'); |
||||||
|
if (i < messages.length - 1) { |
||||||
|
sb.append(','); |
||||||
|
} |
||||||
|
} |
||||||
|
sb.append(']'); |
||||||
|
return sb.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
private static String escapeSockJsCharacters(char[] chars) { |
||||||
|
StringBuilder result = new StringBuilder(); |
||||||
|
for (char ch : chars) { |
||||||
|
if (isSockJsEscapeCharacter(ch)) { |
||||||
|
result.append('\\').append('u'); |
||||||
|
String hex = Integer.toHexString(ch).toLowerCase(); |
||||||
|
for (int i = 0; i < (4 - hex.length()); i++) { |
||||||
|
result.append('0'); |
||||||
|
} |
||||||
|
result.append(hex); |
||||||
|
} |
||||||
|
else { |
||||||
|
result.append(ch); |
||||||
|
} |
||||||
|
} |
||||||
|
return result.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isSockJsEscapeCharacter(char ch) { |
||||||
|
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F') |
||||||
|
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F') |
||||||
|
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public interface FrameFormat { |
||||||
|
|
||||||
|
SockJsFrame format(SockJsFrame frame); |
||||||
|
} |
||||||
|
|
||||||
|
public static class DefaultFrameFormat implements FrameFormat { |
||||||
|
|
||||||
|
private final String format; |
||||||
|
|
||||||
|
public DefaultFrameFormat(String format) { |
||||||
|
Assert.notNull(format, "format is required"); |
||||||
|
this.format = format; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param format a String with a single %s formatting character where the |
||||||
|
* frame content is to be inserted; e.g. "data: %s\r\n\r\n" |
||||||
|
* @return new SockJsFrame instance with the formatted content |
||||||
|
*/ |
||||||
|
public SockJsFrame format(SockJsFrame frame) { |
||||||
|
String content = String.format(this.format, preProcessContent(frame.getContent())); |
||||||
|
return new SockJsFrame(content); |
||||||
|
} |
||||||
|
|
||||||
|
protected String preProcessContent(String content) { |
||||||
|
return content; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.websocket.WebSocketSession; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class SockJsWebSocketSessionAdapter extends AbstractServerSession { |
||||||
|
|
||||||
|
private static Log logger = LogFactory.getLog(SockJsWebSocketSessionAdapter.class); |
||||||
|
|
||||||
|
private WebSocketSession webSocketSession; |
||||||
|
|
||||||
|
|
||||||
|
public SockJsWebSocketSessionAdapter(String sessionId, SockJsHandler delegate, SockJsConfiguration sockJsConfig) { |
||||||
|
super(sessionId, delegate, sockJsConfig); |
||||||
|
} |
||||||
|
|
||||||
|
public void setWebSocketSession(WebSocketSession webSocketSession) throws Exception { |
||||||
|
this.webSocketSession = webSocketSession; |
||||||
|
scheduleHeartbeat(); |
||||||
|
connectionInitialized(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isActive() { |
||||||
|
return (this.webSocketSession != null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sendMessageInternal(String message) { |
||||||
|
cancelHeartbeat(); |
||||||
|
writeFrame(SockJsFrame.messageFrame(message)); |
||||||
|
scheduleHeartbeat(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void writeFrameInternal(SockJsFrame frame) throws Exception { |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Write " + frame); |
||||||
|
} |
||||||
|
this.webSocketSession.sendText(frame.getContent()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void closeInternal() { |
||||||
|
this.webSocketSession.close(); |
||||||
|
this.webSocketSession = null; |
||||||
|
updateLastActiveTime(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void deactivate() { |
||||||
|
this.webSocketSession.close(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the toriginal author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface TransportHandler { |
||||||
|
|
||||||
|
TransportType getTransportType(); |
||||||
|
|
||||||
|
SockJsSessionSupport createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config); |
||||||
|
|
||||||
|
void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception; |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface TransportHandlerRegistrar { |
||||||
|
|
||||||
|
void registerTransportHandlers(TransportHandlerRegistry registry); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public interface TransportHandlerRegistry { |
||||||
|
|
||||||
|
void registerHandler(TransportHandler handler); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs.server; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.websocket.WebSocketHandler; |
||||||
|
import org.springframework.websocket.WebSocketSession; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class WebSocketSockJsHandlerAdapter implements WebSocketHandler { |
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(WebSocketSockJsHandlerAdapter.class); |
||||||
|
|
||||||
|
private final SockJsWebSocketSessionAdapter sockJsSession; |
||||||
|
|
||||||
|
// TODO: the JSON library used must be configurable
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||||
|
|
||||||
|
|
||||||
|
public WebSocketSockJsHandlerAdapter(SockJsWebSocketSessionAdapter sockJsSession) { |
||||||
|
this.sockJsSession = sockJsSession; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void newSession(WebSocketSession webSocketSession) throws Exception { |
||||||
|
logger.debug("WebSocket connection established"); |
||||||
|
webSocketSession.sendText(SockJsFrame.openFrame().getContent()); |
||||||
|
this.sockJsSession.setWebSocketSession(webSocketSession); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleTextMessage(WebSocketSession session, String message) throws Exception { |
||||||
|
if (logger.isDebugEnabled()) { |
||||||
|
logger.debug("Received payload " + message + " for " + sockJsSession); |
||||||
|
} |
||||||
|
if (StringUtils.isEmpty(message)) { |
||||||
|
logger.debug("Ignoring empty payload"); |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
String[] messages = this.objectMapper.readValue(message, String[].class); |
||||||
|
this.sockJsSession.delegateMessages(messages); |
||||||
|
} |
||||||
|
catch (IOException e) { |
||||||
|
logger.error("Broken data received. Terminating WebSocket connection abruptly", e); |
||||||
|
session.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleBinaryMessage(WebSocketSession session, InputStream message) throws Exception { |
||||||
|
// should not happen
|
||||||
|
throw new UnsupportedOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleException(WebSocketSession session, Throwable exception) { |
||||||
|
exception.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sessionClosed(WebSocketSession session, int statusCode, String reason) throws Exception { |
||||||
|
logger.debug("WebSocket connection closed for " + this.sockJsSession); |
||||||
|
this.sockJsSession.close(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,216 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.support; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.atomic.AtomicLong; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.InitializingBean; |
||||||
|
import org.springframework.http.Cookie; |
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.scheduling.TaskScheduler; |
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.AbstractSockJsService; |
||||||
|
import org.springframework.sockjs.server.TransportHandler; |
||||||
|
import org.springframework.sockjs.server.TransportHandlerRegistrar; |
||||||
|
import org.springframework.sockjs.server.TransportHandlerRegistry; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class DefaultSockJsService extends AbstractSockJsService implements TransportHandlerRegistry, InitializingBean { |
||||||
|
|
||||||
|
private static final AtomicLong webSocketSessionIdSuffix = new AtomicLong(); |
||||||
|
|
||||||
|
|
||||||
|
private final SockJsHandler sockJsHandler; |
||||||
|
|
||||||
|
private TaskScheduler sessionTimeoutScheduler; |
||||||
|
|
||||||
|
private final Map<String, SockJsSessionSupport> sessions = new ConcurrentHashMap<String, SockJsSessionSupport>(); |
||||||
|
|
||||||
|
private final Map<TransportType, TransportHandler> transportHandlers = new HashMap<TransportType, TransportHandler>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Class constructor... |
||||||
|
* |
||||||
|
*/ |
||||||
|
public DefaultSockJsService(SockJsHandler sockJsHandler) { |
||||||
|
Assert.notNull(sockJsHandler, "sockJsHandler is required"); |
||||||
|
this.sockJsHandler = sockJsHandler; |
||||||
|
this.sessionTimeoutScheduler = createScheduler("SockJs-sessionTimeout-"); |
||||||
|
new DefaultTransportHandlerRegistrar().registerTransportHandlers(this); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A scheduler instance to use for scheduling periodic expires session cleanup. |
||||||
|
* <p> |
||||||
|
* By default a {@link ThreadPoolTaskScheduler} with default settings is used. |
||||||
|
*/ |
||||||
|
public TaskScheduler getSessionTimeoutScheduler() { |
||||||
|
return this.sessionTimeoutScheduler; |
||||||
|
} |
||||||
|
|
||||||
|
public void setSessionTimeoutScheduler(TaskScheduler sessionTimeoutScheduler) { |
||||||
|
Assert.notNull(sessionTimeoutScheduler, "sessionTimeoutScheduler is required"); |
||||||
|
this.sessionTimeoutScheduler = sessionTimeoutScheduler; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void registerHandler(TransportHandler transportHandler) { |
||||||
|
Assert.notNull(transportHandler, "transportHandler is required"); |
||||||
|
this.transportHandlers.put(transportHandler.getTransportType(), transportHandler); |
||||||
|
} |
||||||
|
|
||||||
|
public void setTransportHandlerRegistrar(TransportHandlerRegistrar registrar) { |
||||||
|
Assert.notNull(registrar, "registrar is required"); |
||||||
|
this.transportHandlers.clear(); |
||||||
|
registrar.registerTransportHandlers(this); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void afterPropertiesSet() throws Exception { |
||||||
|
|
||||||
|
this.sessionTimeoutScheduler.scheduleAtFixedRate(new Runnable() { |
||||||
|
public void run() { |
||||||
|
try { |
||||||
|
int count = sessions.size(); |
||||||
|
if (logger.isTraceEnabled() && (count != 0)) { |
||||||
|
logger.trace("Checking " + count + " session(s) for timeouts [" + getSockJsServiceName() + "]"); |
||||||
|
} |
||||||
|
for (SockJsSessionSupport session : sessions.values()) { |
||||||
|
if (session.getTimeSinceLastActive() > getDisconnectDelay()) { |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Removing " + session + " for [" + getSockJsServiceName() + "]"); |
||||||
|
} |
||||||
|
session.close(); |
||||||
|
sessions.remove(session.getId()); |
||||||
|
} |
||||||
|
} |
||||||
|
if (logger.isTraceEnabled() && (count != 0)) { |
||||||
|
logger.trace(sessions.size() + " remaining session(s) [" + getSockJsServiceName() + "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (Throwable t) { |
||||||
|
logger.error("Failed to complete session timeout checks for [" + getSockJsServiceName() + "]", t); |
||||||
|
} |
||||||
|
} |
||||||
|
}, getDisconnectDelay()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
String sessionId, TransportType transportType) throws Exception { |
||||||
|
|
||||||
|
TransportHandler transportHandler = this.transportHandlers.get(transportType); |
||||||
|
|
||||||
|
if (transportHandler == null) { |
||||||
|
logger.debug("Transport handler not found"); |
||||||
|
response.setStatusCode(HttpStatus.NOT_FOUND); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
HttpMethod supportedMethod = transportType.getHttpMethod(); |
||||||
|
if (!supportedMethod.equals(request.getMethod())) { |
||||||
|
if (HttpMethod.OPTIONS.equals(request.getMethod()) && transportType.isCorsSupported()) { |
||||||
|
response.setStatusCode(HttpStatus.NO_CONTENT); |
||||||
|
addCorsHeaders(request, response, supportedMethod, HttpMethod.OPTIONS); |
||||||
|
addCacheHeaders(response); |
||||||
|
response.getBody(); // ensure headers are flushed (TODO!)
|
||||||
|
} |
||||||
|
else { |
||||||
|
List<HttpMethod> supportedMethods = Arrays.asList(supportedMethod); |
||||||
|
if (transportType.isCorsSupported()) { |
||||||
|
supportedMethods.add(HttpMethod.OPTIONS); |
||||||
|
} |
||||||
|
sendMethodNotAllowed(response, supportedMethods); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
SockJsSessionSupport session = getSockJsSession(sessionId, transportHandler); |
||||||
|
if (session == null) { |
||||||
|
response.setStatusCode(HttpStatus.NOT_FOUND); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
addNoCacheHeaders(response); |
||||||
|
|
||||||
|
if (isJsessionIdCookieNeeded()) { |
||||||
|
Cookie cookie = request.getCookies().getCookie("JSESSIONID"); |
||||||
|
String jsid = (cookie != null) ? cookie.getValue() : "dummy"; |
||||||
|
// TODO: Jetty sets Expires header, so bypass Cookie object for now
|
||||||
|
response.getHeaders().set("Set-Cookie", "JSESSIONID=" + jsid + ";path=/"); // TODO
|
||||||
|
} |
||||||
|
|
||||||
|
if (transportType.isCorsSupported()) { |
||||||
|
addCorsHeaders(request, response); |
||||||
|
} |
||||||
|
|
||||||
|
transportHandler.handleRequest(request, response, session); |
||||||
|
|
||||||
|
response.close(); // ensure headers are flushed (TODO !!)
|
||||||
|
} |
||||||
|
|
||||||
|
public SockJsSessionSupport getSockJsSession(String sessionId, TransportHandler transportHandler) { |
||||||
|
|
||||||
|
TransportType transportType = transportHandler.getTransportType(); |
||||||
|
|
||||||
|
// Always create new session for WebSocket requests
|
||||||
|
sessionId = TransportType.WEBSOCKET.equals(transportType) ? |
||||||
|
sessionId + "#" + webSocketSessionIdSuffix.getAndIncrement() : sessionId; |
||||||
|
|
||||||
|
SockJsSessionSupport session = this.sessions.get(sessionId); |
||||||
|
if (session != null) { |
||||||
|
return session; |
||||||
|
} |
||||||
|
|
||||||
|
if (TransportType.XHR_SEND.equals(transportType) || TransportType.JSONP_SEND.equals(transportType)) { |
||||||
|
logger.debug(transportType + " did not find session"); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
synchronized (this.sessions) { |
||||||
|
session = this.sessions.get(sessionId); |
||||||
|
if (session != null) { |
||||||
|
return session; |
||||||
|
} |
||||||
|
|
||||||
|
logger.debug("Creating new session with session id \"" + sessionId + "\""); |
||||||
|
session = transportHandler.createSession(sessionId, this.sockJsHandler, this); |
||||||
|
this.sessions.put(sessionId, session); |
||||||
|
|
||||||
|
return session; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.support; |
||||||
|
|
||||||
|
import org.springframework.sockjs.server.TransportHandlerRegistrar; |
||||||
|
import org.springframework.sockjs.server.TransportHandlerRegistry; |
||||||
|
import org.springframework.sockjs.server.transport.EventSourceTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.HtmlFileTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.JsonpPollingTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.JsonpTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.XhrPollingTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.XhrStreamingTransportHandler; |
||||||
|
import org.springframework.sockjs.server.transport.XhrTransportHandler; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class DefaultTransportHandlerRegistrar implements TransportHandlerRegistrar { |
||||||
|
|
||||||
|
public void registerTransportHandlers(TransportHandlerRegistry registry) { |
||||||
|
|
||||||
|
registry.registerHandler(new XhrPollingTransportHandler()); |
||||||
|
registry.registerHandler(new XhrTransportHandler()); |
||||||
|
|
||||||
|
registry.registerHandler(new JsonpPollingTransportHandler()); |
||||||
|
registry.registerHandler(new JsonpTransportHandler()); |
||||||
|
|
||||||
|
registry.registerHandler(new XhrStreamingTransportHandler()); |
||||||
|
registry.registerHandler(new EventSourceTransportHandler()); |
||||||
|
registry.registerHandler(new HtmlFileTransportHandler()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.TransportHandler; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException; |
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractHttpReceivingTransportHandler implements TransportHandler { |
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(this.getClass()); |
||||||
|
|
||||||
|
// TODO: the JSON library used must be configurable
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||||
|
|
||||||
|
|
||||||
|
public ObjectMapper getObjectMapper() { |
||||||
|
return this.objectMapper; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SockJsSessionSupport createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
String[] messages = null; |
||||||
|
try { |
||||||
|
messages = readMessages(request); |
||||||
|
} |
||||||
|
catch (JsonMappingException ex) { |
||||||
|
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
response.getBody().write("Payload expected.".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
catch (IOException ex) { |
||||||
|
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
response.getBody().write("Broken JSON encoding.".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Received messages: " + Arrays.asList(messages)); |
||||||
|
} |
||||||
|
|
||||||
|
session.delegateMessages(messages); |
||||||
|
|
||||||
|
response.setStatusCode(getResponseStatus()); |
||||||
|
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8"))); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract String[] readMessages(ServerHttpRequest request) throws IOException; |
||||||
|
|
||||||
|
protected abstract HttpStatus getResponseStatus(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
import org.springframework.sockjs.server.TransportHandler; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractHttpSendingTransportHandler implements TransportHandler { |
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(this.getClass()); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
AbstractHttpServerSession httpServerSession = (AbstractHttpServerSession) session; |
||||||
|
|
||||||
|
// Set content type before writing
|
||||||
|
response.getHeaders().setContentType(getContentType()); |
||||||
|
|
||||||
|
if (httpServerSession.isNew()) { |
||||||
|
handleNewSession(request, response, httpServerSession); |
||||||
|
} |
||||||
|
else if (httpServerSession.isActive()) { |
||||||
|
logger.debug("another " + getTransportType() + " connection still open: " + httpServerSession); |
||||||
|
httpServerSession.writeFrame(response.getBody(), SockJsFrame.closeFrameAnotherConnectionOpen()); |
||||||
|
} |
||||||
|
else { |
||||||
|
logger.debug("starting " + getTransportType() + " async request"); |
||||||
|
httpServerSession.setCurrentRequest(request, response, getFrameFormat(request)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void handleNewSession(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
AbstractHttpServerSession session) throws Exception { |
||||||
|
|
||||||
|
logger.debug("Opening " + getTransportType() + " connection"); |
||||||
|
session.setFrameFormat(getFrameFormat(request)); |
||||||
|
session.writeFrame(response.getBody(), SockJsFrame.openFrame()); |
||||||
|
session.connectionInitialized(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract MediaType getContentType(); |
||||||
|
|
||||||
|
protected abstract FrameFormat getFrameFormat(ServerHttpRequest request); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.OutputStream; |
||||||
|
import java.util.concurrent.ArrayBlockingQueue; |
||||||
|
import java.util.concurrent.BlockingQueue; |
||||||
|
|
||||||
|
import org.springframework.http.server.AsyncServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.server.AbstractServerSession; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
import org.springframework.sockjs.server.TransportHandler; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* An abstract base class for use with HTTP-based transports. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractHttpServerSession extends AbstractServerSession { |
||||||
|
|
||||||
|
private FrameFormat frameFormat; |
||||||
|
|
||||||
|
private final BlockingQueue<String> messageCache = new ArrayBlockingQueue<String>(100); |
||||||
|
|
||||||
|
private AsyncServerHttpRequest asyncRequest; |
||||||
|
|
||||||
|
private OutputStream outputStream; |
||||||
|
|
||||||
|
|
||||||
|
public AbstractHttpServerSession(String sessionId, SockJsHandler delegate, SockJsConfiguration sockJsConfig) { |
||||||
|
super(sessionId, delegate, sockJsConfig); |
||||||
|
} |
||||||
|
|
||||||
|
public void setFrameFormat(FrameFormat frameFormat) { |
||||||
|
this.frameFormat = frameFormat; |
||||||
|
} |
||||||
|
|
||||||
|
public synchronized void setCurrentRequest(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
FrameFormat frameFormat) throws IOException { |
||||||
|
|
||||||
|
if (isClosed()) { |
||||||
|
logger.debug("connection already closed"); |
||||||
|
writeFrame(response.getBody(), SockJsFrame.closeFrameGoAway()); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Assert.isInstanceOf(AsyncServerHttpRequest.class, request, "Expected AsyncServerHttpRequest"); |
||||||
|
|
||||||
|
this.asyncRequest = (AsyncServerHttpRequest) request; |
||||||
|
this.asyncRequest.setTimeout(-1); |
||||||
|
this.asyncRequest.startAsync(); |
||||||
|
|
||||||
|
this.outputStream = response.getBody(); |
||||||
|
this.frameFormat = frameFormat; |
||||||
|
|
||||||
|
scheduleHeartbeat(); |
||||||
|
tryFlush(); |
||||||
|
} |
||||||
|
|
||||||
|
public synchronized boolean isActive() { |
||||||
|
return ((this.asyncRequest != null) && (!this.asyncRequest.isAsyncCompleted())); |
||||||
|
} |
||||||
|
|
||||||
|
protected BlockingQueue<String> getMessageCache() { |
||||||
|
return this.messageCache; |
||||||
|
} |
||||||
|
|
||||||
|
protected final synchronized void sendMessageInternal(String message) { |
||||||
|
// assert close() was not called
|
||||||
|
// threads: TH-Session-Endpoint or any other thread
|
||||||
|
this.messageCache.add(message); |
||||||
|
tryFlush(); |
||||||
|
} |
||||||
|
|
||||||
|
private void tryFlush() { |
||||||
|
if (isActive() && !getMessageCache().isEmpty()) { |
||||||
|
logger.trace("Flushing messages"); |
||||||
|
flush(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Only called if the connection is currently active |
||||||
|
*/ |
||||||
|
protected abstract void flush(); |
||||||
|
|
||||||
|
protected void closeInternal() { |
||||||
|
resetRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
protected synchronized void writeFrameInternal(SockJsFrame frame) throws IOException { |
||||||
|
if (isActive()) { |
||||||
|
writeFrame(this.outputStream, frame); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method may be called by a {@link TransportHandler} to write a frame |
||||||
|
* even when the connection is not active, as long as a valid OutputStream |
||||||
|
* is provided. |
||||||
|
*/ |
||||||
|
public void writeFrame(OutputStream outputStream, SockJsFrame frame) throws IOException { |
||||||
|
frame = this.frameFormat.format(frame); |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Writing " + frame); |
||||||
|
} |
||||||
|
outputStream.write(frame.getContentBytes()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void deactivate() { |
||||||
|
this.outputStream = null; |
||||||
|
this.asyncRequest = null; |
||||||
|
updateLastActiveTime(); |
||||||
|
} |
||||||
|
|
||||||
|
protected synchronized void resetRequest() { |
||||||
|
if (isActive()) { |
||||||
|
this.asyncRequest.completeAsync(); |
||||||
|
} |
||||||
|
this.outputStream = null; |
||||||
|
this.asyncRequest = null; |
||||||
|
updateLastActiveTime(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public abstract class AbstractStreamingTransportHandler extends AbstractHttpSendingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public StreamingHttpServerSession createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config) { |
||||||
|
return new StreamingHttpServerSession(sessionId, handler, config); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
writePrelude(request, response); |
||||||
|
|
||||||
|
super.handleRequest(request, response, session); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void writePrelude(ServerHttpRequest request, ServerHttpResponse response) |
||||||
|
throws IOException; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void handleNewSession(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
AbstractHttpServerSession session) throws IOException, Exception { |
||||||
|
|
||||||
|
super.handleNewSession(request, response, session); |
||||||
|
|
||||||
|
session.setCurrentRequest(request, response, getFrameFormat(request)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.DefaultFrameFormat; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class EventSourceTransportHandler extends AbstractStreamingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.EVENT_SOURCE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MediaType getContentType() { |
||||||
|
return new MediaType("text", "event-stream", Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void writePrelude(ServerHttpRequest request, ServerHttpResponse response) throws IOException { |
||||||
|
response.getBody().write('\r'); |
||||||
|
response.getBody().write('\n'); |
||||||
|
response.getBody().flush(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FrameFormat getFrameFormat(ServerHttpRequest request) { |
||||||
|
return new DefaultFrameFormat("data: %s\r\n\r\n"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,115 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.DefaultFrameFormat; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.util.JavaScriptUtils; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class HtmlFileTransportHandler extends AbstractStreamingTransportHandler { |
||||||
|
|
||||||
|
private static final String PARTIAL_HTML_CONTENT; |
||||||
|
|
||||||
|
static { |
||||||
|
StringBuilder sb = new StringBuilder( |
||||||
|
"<!doctype html>\n" + |
||||||
|
"<html><head>\n" + |
||||||
|
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" + |
||||||
|
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + |
||||||
|
"</head><body><h2>Don't panic!</h2>\n" + |
||||||
|
" <script>\n" + |
||||||
|
" document.domain = document.domain;\n" + |
||||||
|
" var c = parent.%s;\n" + |
||||||
|
" c.start();\n" + |
||||||
|
" function p(d) {c.message(d);};\n" + |
||||||
|
" window.onload = function() {c.stop();};\n" + |
||||||
|
" </script>" |
||||||
|
); |
||||||
|
|
||||||
|
// Safari needs at least 1024 bytes to parse the website.
|
||||||
|
// http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors
|
||||||
|
int spaces = 1024 - sb.length(); |
||||||
|
for (int i=0; i < spaces; i++) { |
||||||
|
sb.append(' '); |
||||||
|
} |
||||||
|
|
||||||
|
PARTIAL_HTML_CONTENT = sb.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.HTML_FILE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MediaType getContentType() { |
||||||
|
return new MediaType("text", "html", Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
String callback = request.getQueryParams().getFirst("c"); |
||||||
|
if (! StringUtils.hasText(callback)) { |
||||||
|
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
response.getBody().write("\"callback\" parameter required".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
super.handleRequest(request, response, session); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void writePrelude(ServerHttpRequest request, ServerHttpResponse response) throws IOException { |
||||||
|
|
||||||
|
// we already validated the parameter..
|
||||||
|
String callback = request.getQueryParams().getFirst("c"); |
||||||
|
|
||||||
|
String html = String.format(PARTIAL_HTML_CONTENT, callback); |
||||||
|
response.getBody().write(html.getBytes("UTF-8")); |
||||||
|
response.getBody().flush(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FrameFormat getFrameFormat(ServerHttpRequest request) { |
||||||
|
return new DefaultFrameFormat("<script>\np(\"%s\");\n</script>\r\n") { |
||||||
|
@Override |
||||||
|
protected String preProcessContent(String content) { |
||||||
|
return JavaScriptUtils.javaScriptEscape(content); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.util.JavaScriptUtils; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.JSONP; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MediaType getContentType() { |
||||||
|
return new MediaType("application", "javascript", Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PollingHttpServerSession createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config) { |
||||||
|
return new PollingHttpServerSession(sessionId, handler, config); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
String callback = request.getQueryParams().getFirst("c"); |
||||||
|
if (! StringUtils.hasText(callback)) { |
||||||
|
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
response.getBody().write("\"callback\" parameter required".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
super.handleRequest(request, response, session); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FrameFormat getFrameFormat(ServerHttpRequest request) { |
||||||
|
|
||||||
|
// we already validated the parameter..
|
||||||
|
String callback = request.getQueryParams().getFirst("c"); |
||||||
|
|
||||||
|
return new SockJsFrame.DefaultFrameFormat(callback + "(\"%s\");\r\n") { |
||||||
|
@Override |
||||||
|
protected String preProcessContent(String content) { |
||||||
|
return JavaScriptUtils.javaScriptEscape(content); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
|
||||||
|
public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.JSONP_SEND; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
SockJsSessionSupport sockJsSession) throws Exception { |
||||||
|
|
||||||
|
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { |
||||||
|
if (request.getQueryParams().getFirst("d") == null) { |
||||||
|
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
response.getBody().write("Payload expected.".getBytes("UTF-8")); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
super.handleRequest(request, response, sockJsSession); |
||||||
|
|
||||||
|
response.getBody().write("ok".getBytes("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String[] readMessages(ServerHttpRequest request) throws IOException { |
||||||
|
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { |
||||||
|
String d = request.getQueryParams().getFirst("d"); |
||||||
|
return getObjectMapper().readValue(d, String[].class); |
||||||
|
} |
||||||
|
else { |
||||||
|
return getObjectMapper().readValue(request.getBody(), String[].class); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected HttpStatus getResponseStatus() { |
||||||
|
return HttpStatus.OK; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
|
||||||
|
|
||||||
|
public class PollingHttpServerSession extends AbstractHttpServerSession { |
||||||
|
|
||||||
|
public PollingHttpServerSession(String sessionId, SockJsHandler delegate, SockJsConfiguration sockJsConfig) { |
||||||
|
super(sessionId, delegate, sockJsConfig); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void flush() { |
||||||
|
cancelHeartbeat(); |
||||||
|
String[] messages = getMessageCache().toArray(new String[getMessageCache().size()]); |
||||||
|
getMessageCache().clear(); |
||||||
|
writeFrame(SockJsFrame.messageFrame(messages)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void writeFrame(SockJsFrame frame) { |
||||||
|
super.writeFrame(frame); |
||||||
|
resetRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,72 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.OutputStream; |
||||||
|
|
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame; |
||||||
|
|
||||||
|
|
||||||
|
public class StreamingHttpServerSession extends AbstractHttpServerSession { |
||||||
|
|
||||||
|
private int byteCount; |
||||||
|
|
||||||
|
|
||||||
|
public StreamingHttpServerSession(String sessionId, SockJsHandler delegate, SockJsConfiguration sockJsConfig) { |
||||||
|
super(sessionId, delegate, sockJsConfig); |
||||||
|
} |
||||||
|
|
||||||
|
protected void flush() { |
||||||
|
|
||||||
|
cancelHeartbeat(); |
||||||
|
|
||||||
|
do { |
||||||
|
String message = getMessageCache().poll(); |
||||||
|
SockJsFrame frame = SockJsFrame.messageFrame(message); |
||||||
|
writeFrame(frame); |
||||||
|
|
||||||
|
this.byteCount += frame.getContentBytes().length + 1; |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace(this.byteCount + " bytes written, " + getMessageCache().size() + " more messages"); |
||||||
|
} |
||||||
|
if (this.byteCount >= getSockJsConfig().getStreamBytesLimit()) { |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Streamed bytes limit reached. Recycling current request"); |
||||||
|
} |
||||||
|
resetRequest(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} while (!getMessageCache().isEmpty()); |
||||||
|
|
||||||
|
scheduleHeartbeat(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected synchronized void resetRequest() { |
||||||
|
super.resetRequest(); |
||||||
|
this.byteCount = 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void writeFrame(OutputStream outputStream, SockJsFrame frame) throws IOException { |
||||||
|
super.writeFrame(outputStream, frame); |
||||||
|
outputStream.flush(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.SockJsSessionSupport; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsWebSocketSessionAdapter; |
||||||
|
import org.springframework.sockjs.server.TransportHandler; |
||||||
|
import org.springframework.sockjs.server.WebSocketSockJsHandlerAdapter; |
||||||
|
import org.springframework.websocket.server.HandshakeRequestHandler; |
||||||
|
import org.springframework.websocket.server.endpoint.EndpointHandshakeRequestHandler; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class WebSocketTransportHandler implements TransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.WEBSOCKET; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SockJsSessionSupport createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config) { |
||||||
|
return new SockJsWebSocketSessionAdapter(sessionId, handler, config); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, SockJsSessionSupport session) |
||||||
|
throws Exception { |
||||||
|
|
||||||
|
SockJsWebSocketSessionAdapter sockJsSession = (SockJsWebSocketSessionAdapter) session; |
||||||
|
WebSocketSockJsHandlerAdapter webSocketHandler = new WebSocketSockJsHandlerAdapter(sockJsSession); |
||||||
|
HandshakeRequestHandler handshakeRequestHandler = new EndpointHandshakeRequestHandler(webSocketHandler); |
||||||
|
handshakeRequestHandler.doHandshake(request, response); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.sockjs.SockJsHandler; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsConfiguration; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.DefaultFrameFormat; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class XhrPollingTransportHandler extends AbstractHttpSendingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.XHR; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MediaType getContentType() { |
||||||
|
return new MediaType("application", "javascript", Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FrameFormat getFrameFormat(ServerHttpRequest request) { |
||||||
|
return new DefaultFrameFormat("%s\n"); |
||||||
|
} |
||||||
|
|
||||||
|
public PollingHttpServerSession createSession(String sessionId, SockJsHandler handler, SockJsConfiguration config) { |
||||||
|
return new PollingHttpServerSession(sessionId, handler, config); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.DefaultFrameFormat; |
||||||
|
import org.springframework.sockjs.server.SockJsFrame.FrameFormat; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* TODO |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.0 |
||||||
|
*/ |
||||||
|
public class XhrStreamingTransportHandler extends AbstractStreamingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.XHR_STREAMING; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MediaType getContentType() { |
||||||
|
return new MediaType("application", "javascript", Charset.forName("UTF-8")); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void writePrelude(ServerHttpRequest request, ServerHttpResponse response) throws IOException { |
||||||
|
for (int i=0; i < 2048; i++) { |
||||||
|
response.getBody().write('h'); |
||||||
|
} |
||||||
|
response.getBody().write('\n'); |
||||||
|
response.getBody().flush(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FrameFormat getFrameFormat(ServerHttpRequest request) { |
||||||
|
return new DefaultFrameFormat("%s\n"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2013 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.sockjs.server.transport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.sockjs.TransportType; |
||||||
|
|
||||||
|
public class XhrTransportHandler extends AbstractHttpReceivingTransportHandler { |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public TransportType getTransportType() { |
||||||
|
return TransportType.XHR_SEND; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String[] readMessages(ServerHttpRequest request) throws IOException { |
||||||
|
return getObjectMapper().readValue(request.getBody(), String[].class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected HttpStatus getResponseStatus() { |
||||||
|
return HttpStatus.NO_CONTENT; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue