getReceivedMessages() {
+ return Collections.unmodifiableCollection(messages);
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java
new file mode 100644
index 00000000000..32358f14268
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/EchoServlet.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import javax.servlet.annotation.WebServlet;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+
+/*
+ * Server-side WebSocket upgraded on /echo servlet.
+ */
+@SuppressWarnings("serial")
+@WebServlet(
+ name = "Echo WebSocket Servlet",
+ urlPatterns = {"/echo"})
+public class EchoServlet extends WebSocketServlet implements WebSocketCreator {
+ @Override
+ public void configure(WebSocketServletFactory factory) {
+ factory.setCreator(this);
+ }
+
+ @Override
+ public Object createWebSocket(
+ ServletUpgradeRequest servletUpgradeRequest, ServletUpgradeResponse servletUpgradeResponse) {
+ return new ServerSocket();
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java
new file mode 100644
index 00000000000..357deac5ce0
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/Main.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import org.apache.tomcat.util.scan.StandardJarScanFilter;
+import org.apache.tomcat.util.scan.StandardJarScanner;
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
+import org.eclipse.jetty.jsp.JettyJspServlet;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.webapp.WebInfConfiguration;
+
+/**
+ * Starts up the server, including a DefaultServlet that handles static files, and any servlet
+ * classes annotated with the @WebServlet annotation.
+ */
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+
+ // Create a server that listens on port 8080.
+ Server server = new Server(8080);
+ WebAppContext webAppContext = new WebAppContext();
+ server.setHandler(webAppContext);
+
+ // Load static content from inside the jar file.
+ URL webAppDir = Main.class.getClassLoader().getResource("WEB-INF/");
+ System.out.println(webAppDir);
+ webAppContext.setResourceBase(webAppDir.toURI().toString());
+
+ // Enable annotations so the server sees classes annotated with @WebServlet.
+ webAppContext.setConfigurations(
+ new Configuration[] {
+ new AnnotationConfiguration(), new WebInfConfiguration(),
+ });
+
+ webAppContext.setAttribute(
+ "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
+ ".*/target/classes/|.*\\.jar");
+ enableEmbeddedJspSupport(webAppContext);
+
+ ServletHolder holderAltMapping = new ServletHolder();
+ holderAltMapping.setName("index.jsp");
+ holderAltMapping.setForcedPath("/index.jsp");
+ webAppContext.addServlet(holderAltMapping, "/");
+
+ // Start the server! 🚀
+ server.start();
+ System.out.println("Server started!");
+
+ // Keep the main thread alive while the server is running.
+ server.join();
+ }
+
+ private static void enableEmbeddedJspSupport(ServletContextHandler servletContextHandler)
+ throws IOException {
+ // Establish Scratch directory for the servlet context (used by JSP compilation)
+ File tempDir = new File(System.getProperty("java.io.tmpdir"));
+ File scratchDir = new File(tempDir.toString(), "embedded-jetty-jsp");
+
+ if (!scratchDir.exists()) {
+ if (!scratchDir.mkdirs()) {
+ throw new IOException("Unable to create scratch directory: " + scratchDir);
+ }
+ }
+ servletContextHandler.setAttribute("javax.servlet.context.tempdir", scratchDir);
+
+ // Set Classloader of Context to be sane (needed for JSTL)
+ // JSP requires a non-System classloader, this simply wraps the
+ // embedded System classloader in a way that makes it suitable
+ // for JSP to use
+ ClassLoader jspClassLoader = new URLClassLoader(new URL[0], Main.class.getClassLoader());
+ servletContextHandler.setClassLoader(jspClassLoader);
+
+ // Manually call JettyJasperInitializer on context startup
+ servletContextHandler.addBean(new JspStarter(servletContextHandler));
+
+ // Create / Register JSP Servlet (must be named "jsp" per spec)
+ ServletHolder holderJsp = new ServletHolder("jsp", JettyJspServlet.class);
+ holderJsp.setInitOrder(0);
+ holderJsp.setInitParameter("logVerbosityLevel", "DEBUG");
+ holderJsp.setInitParameter("fork", "false");
+ holderJsp.setInitParameter("xpoweredBy", "false");
+ holderJsp.setInitParameter("compilerTargetVM", "25");
+ holderJsp.setInitParameter("compilerSourceVM", "25");
+ holderJsp.setInitParameter("keepgenerated", "true");
+ servletContextHandler.addServlet(holderJsp, "*.jsp");
+ }
+
+ /**
+ * JspStarter for embedded ServletContextHandlers
+ *
+ * This is added as a bean that is a jetty LifeCycle on the ServletContextHandler. This bean's
+ * doStart method will be called as the ServletContextHandler starts, and will call the
+ * ServletContainerInitializer for the jsp engine.
+ */
+ public static class JspStarter extends AbstractLifeCycle
+ implements ServletContextHandler.ServletContainerInitializerCaller {
+ JettyJasperInitializer sci;
+ ServletContextHandler context;
+
+ public JspStarter(ServletContextHandler context) {
+ this.sci = new JettyJasperInitializer();
+ this.context = context;
+ String skip = "apache-*,ecj-*,jetty-*,asm-*,javax.servlet-*"
+ + "javax.annotation-*,taglibs-standard-spec-*,*.jar";
+ StandardJarScanner jarScanner = new StandardJarScanner();
+ StandardJarScanFilter jarScanFilter = new StandardJarScanFilter();
+ jarScanFilter.setTldSkip(skip);
+ jarScanner.setJarScanFilter(jarScanFilter);
+ this.context.setAttribute("org.apache.tomcat.JarScanner", jarScanner);
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(context.getClassLoader());
+ try {
+ sci.onStartup(null, context.getServletContext());
+ super.doStart();
+ } finally {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java
new file mode 100644
index 00000000000..0feab349ac2
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/SendServlet.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+
+@WebServlet("/send")
+/** Servlet that sends the message sent over POST to over a websocket connection. */
+public class SendServlet extends HttpServlet {
+
+ private Logger logger = Logger.getLogger(SendServlet.class.getName());
+
+ private static final String ENDPOINT = "/echo";
+ private static final String WEBSOCKET_PROTOCOL_PREFIX = "ws://";
+ private static final String WEBSOCKET_HTTPS_PROTOCOL_PREFIX = "wss://";
+ private static final String APPENGINE_HOST_SUFFIX = ".appspot.com";
+
+ // GAE_INSTANCE environment is used to detect App Engine Flexible Environment
+ private static final String GAE_INSTANCE_VAR = "GAE_INSTANCE";
+ // GOOGLE_CLOUD_PROJECT environment variable is set to the GCP project ID on App Engine Flexible.
+ private static final String GOOGLE_CLOUD_PROJECT_ENV_VAR = "GOOGLE_CLOUD_PROJECT";
+ // GAE_SERVICE environment variable is set to the GCP service name.
+ private static final String GAE_SERVICE_ENV_VAR = "GAE_SERVICE";
+
+ private final HttpClient httpClient;
+ private final WebSocketClient webSocketClient;
+ private final ClientSocket clientSocket;
+
+ public SendServlet() {
+ this.httpClient = createHttpClient();
+ this.webSocketClient = createWebSocketClient();
+ this.clientSocket = new ClientSocket();
+ }
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String message = request.getParameter("message");
+ try {
+ sendMessageOverWebSocket(message);
+ response.sendRedirect("/");
+ } catch (Exception e) {
+ logger.severe("Error sending message over socket: " + e.getMessage());
+ e.printStackTrace(response.getWriter());
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ }
+ }
+
+ private HttpClient createHttpClient() {
+ HttpClient httpClient;
+ if (System.getenv(GAE_INSTANCE_VAR) != null) {
+ // If on HTTPS, create client with SSL Context
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ httpClient = new HttpClient(sslContextFactory);
+ } else {
+ // local testing on HTTP
+ httpClient = new HttpClient();
+ }
+ return httpClient;
+ }
+
+ private WebSocketClient createWebSocketClient() {
+ return new WebSocketClient(this.httpClient);
+ }
+
+ private void sendMessageOverWebSocket(String message) throws Exception {
+ if (!httpClient.isRunning()) {
+ try {
+ httpClient.start();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+ if (!webSocketClient.isRunning()) {
+ try {
+ webSocketClient.start();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+ ClientUpgradeRequest request = new ClientUpgradeRequest();
+ // Attempt connection
+ Future future =
+ webSocketClient.connect(clientSocket, new URI(getWebSocketAddress()), request);
+ // Wait for Connect
+ Session session = future.get();
+ // Send a message
+ session.getRemote().sendString(message);
+ // Close session
+ session.close();
+ }
+
+ /**
+ * Returns the host:port/echo address a client needs to use to communicate with the server. On App
+ * engine Flex environments, result will be in the form wss://project-id.appspot.com/echo
+ */
+ public static String getWebSocketAddress() {
+ // Use ws://127.0.0.1:8080/echo when testing locally
+ String webSocketHost = "127.0.0.1:8080";
+ String webSocketProtocolPrefix = WEBSOCKET_PROTOCOL_PREFIX;
+
+ // On App Engine flexible environment, use wss://project-id.appspot.com/echo
+ if (System.getenv(GAE_INSTANCE_VAR) != null) {
+ String projectId = System.getenv(GOOGLE_CLOUD_PROJECT_ENV_VAR);
+ if (projectId != null) {
+ String serviceName = System.getenv(GAE_SERVICE_ENV_VAR);
+ webSocketHost = serviceName + "-dot-" + projectId + APPENGINE_HOST_SUFFIX;
+ }
+ Preconditions.checkNotNull(webSocketHost);
+ // Use wss:// instead of ws:// protocol when connecting over https
+ webSocketProtocolPrefix = WEBSOCKET_HTTPS_PROTOCOL_PREFIX;
+ }
+ return webSocketProtocolPrefix + webSocketHost + ENDPOINT;
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java
new file mode 100644
index 00000000000..07cfafe0f3e
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/java/com/example/flexible/websocket/jettynative/ServerSocket.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+/*
+ * Server-side WebSocket : echoes received message back to client.
+ */
+@WebSocket(maxTextMessageSize = 64 * 1024)
+public class ServerSocket {
+ private Logger logger = Logger.getLogger(SendServlet.class.getName());
+ private Session session;
+
+ @OnWebSocketConnect
+ public void onWebSocketConnect(Session session) {
+ this.session = session;
+ logger.fine("Socket Connected: " + session);
+ }
+
+ @OnWebSocketMessage
+ public void onWebSocketText(String message) {
+ logger.fine("Received message: " + message);
+ try {
+ // echo message back to client
+ this.session.getRemote().sendString(message);
+ } catch (IOException e) {
+ logger.severe("Error echoing message: " + e.getMessage());
+ }
+ }
+
+ @OnWebSocketClose
+ public void onWebSocketClose(int statusCode, String reason) {
+ logger.fine("Socket Closed: [" + statusCode + "] " + reason);
+ }
+
+ @OnWebSocketError
+ public void onWebSocketError(Throwable cause) {
+ logger.severe("Websocket error : " + cause.getMessage());
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp
new file mode 100644
index 00000000000..8730c529584
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/index.jsp
@@ -0,0 +1,33 @@
+
+
+<%@ page import="com.example.flexible.websocket.jettynative.ClientSocket" %>
+
+
+
+
+ Send a message
+
+ Publish a message
+
+ Last received messages
+ <%= ClientSocket.getReceivedMessages() %>
+
+
diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml
new file mode 100644
index 00000000000..475971850a9
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/jetty-web.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ true
+
+ -org.eclipse.jetty.
+
+
diff --git a/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp
new file mode 100644
index 00000000000..ef9d7051928
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/main/webapp/WEB-INF/js_client.jsp
@@ -0,0 +1,85 @@
+
+
+
+<%@ page import="com.example.flexible.websocket.jettynative.SendServlet" %>
+
+ Google App Engine Flexible Environment - WebSocket Echo
+
+
+
+ Echo demo
+
+
+
+
+
+
+
+
+
diff --git a/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java
new file mode 100644
index 00000000000..6b8636852ef
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/ClientSocketTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class ClientSocketTest {
+ ClientSocket socket;
+
+ @Before
+ public void setUp() {
+ socket = new ClientSocket();
+ }
+
+ @Test
+ public void testOnMessage() {
+ assertEquals(ClientSocket.getReceivedMessages().size(), 0);
+ socket.onMessage("test");
+ assertEquals(ClientSocket.getReceivedMessages().size(), 1);
+ }
+}
diff --git a/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java
new file mode 100644
index 00000000000..37916cb6a37
--- /dev/null
+++ b/flexible/java-25/websocket-jetty/src/test/java/com/example/flexible/websocket/jettynative/SendServletTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.example.flexible.websocket.jettynative;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class SendServletTest {
+ @Test
+ public void testGetWebSocketAddress() {
+ assertTrue(SendServlet.getWebSocketAddress().contains("/echo"));
+ }
+}