diff --git a/google-api-client-servlet/pom.xml b/google-api-client-servlet/pom.xml
index 8d0818237..c70df8ac2 100644
--- a/google-api-client-servlet/pom.xml
+++ b/google-api-client-servlet/pom.xml
@@ -84,6 +84,12 @@
javax.servlet
servlet-api
+ provided
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
diff --git a/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/NotificationServlet.java b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/NotificationServlet.java
new file mode 100644
index 000000000..8aa66ad93
--- /dev/null
+++ b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/NotificationServlet.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.api.client.googleapis.extensions.servlet.notifications.jakarta;
+
+import com.google.api.client.googleapis.notifications.StoredChannel;
+import com.google.api.client.util.Beta;
+import com.google.api.client.util.store.DataStore;
+import com.google.api.client.util.store.DataStoreFactory;
+import com.google.api.client.util.store.MemoryDataStoreFactory;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * {@link Beta}
+ * Thread-safe Webhook Servlet to receive notifications using the {@code jakarta.servlet} namespace.
+ *
+ *
In order to use this servlet you should create a class inheriting from {@link
+ * NotificationServlet} and register the servlet in your web.xml.
+ *
+ *
It is a simple wrapper around {@link WebhookUtils#processWebhookNotification}, so if you you
+ * may alternatively call that method instead from your {@link HttpServlet#doPost} with no loss of
+ * functionality. Example usage:
+ *
+ *
{@code
+ * public class MyNotificationServlet extends NotificationServlet {
+ *
+ * private static final long serialVersionUID = 1L;
+ *
+ * public MyNotificationServlet() throws IOException {
+ * super(new SomeDataStoreFactory());
+ * }
+ * }
+ * }
+ *
+ * Sample web.xml setup:
+ *
+ * {@code
+ * {@literal <}servlet{@literal >}
+ * {@literal <}servlet-name{@literal >}MyNotificationServlet{@literal <}/servlet-name{@literal >}
+ * {@literal <}servlet-class{@literal >}
+ * com.mypackage.MyNotificationServlet
+ * {@literal <}/servlet-class{@literal >}
+ * {@literal <}/servlet{@literal >}
+ * {@literal <}servlet-mapping{@literal >}
+ * {@literal <}servlet-name{@literal >}MyNotificationServlet{@literal <}/servlet-name{@literal >}
+ * {@literal <}url-pattern{@literal >}/notifications{@literal <}/url-pattern{@literal >}
+ * {@literal <}/servlet-mapping{@literal >}
+ * }
+ *
+ * WARNING: by default it uses {@link MemoryDataStoreFactory#getDefaultInstance()} which means it
+ * will NOT persist the notification channels when the servlet process dies, so it is a BAD CHOICE
+ * for a production application. But it is a convenient choice when testing locally, in which case
+ * you don't need to override it, and can simply reference it directly in your web.xml file. For
+ * example:
+ *
+ *
{@code
+ * {@literal <}servlet{@literal >}
+ * {@literal <}servlet-name{@literal >}NotificationServlet{@literal <}/servlet-name{@literal >}
+ * {@literal <}servlet-class{@literal >}
+ * com.google.api.client.googleapis.extensions.servlet.notificationsNotificationServlet
+ * {@literal <}/servlet-class{@literal >}
+ * {@literal <}/servlet{@literal >}
+ * {@literal <}servlet-mapping{@literal >}
+ * {@literal <}servlet-name{@literal >}NotificationServlet{@literal <}/servlet-name{@literal >}
+ * {@literal <}url-pattern{@literal >}/notifications{@literal <}/url-pattern{@literal >}
+ * {@literal <}/servlet-mapping{@literal >}
+ * }
+ *
+ * @since 2.6.0
+ */
+@Beta
+public class NotificationServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Notification channel data store. */
+ private final transient DataStore channelDataStore;
+
+ /**
+ * Constructor to be used for testing and demo purposes that uses {@link
+ * MemoryDataStoreFactory#getDefaultInstance()} which means it will NOT persist the notification
+ * channels when the servlet process dies, so it is a bad choice for a production application.
+ */
+ public NotificationServlet() throws IOException {
+ this(MemoryDataStoreFactory.getDefaultInstance());
+ }
+
+ /**
+ * Constructor which uses {@link StoredChannel#getDefaultDataStore(DataStoreFactory)} on the given
+ * data store factory, which is the normal use case.
+ *
+ * @param dataStoreFactory data store factory
+ */
+ protected NotificationServlet(DataStoreFactory dataStoreFactory) throws IOException {
+ this(StoredChannel.getDefaultDataStore(dataStoreFactory));
+ }
+
+ /**
+ * Constructor that allows a specific notification data store to be specified.
+ *
+ * @param channelDataStore notification channel data store
+ */
+ protected NotificationServlet(DataStore channelDataStore) {
+ this.channelDataStore = channelDataStore;
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ WebhookUtils.processWebhookNotification(req, resp, channelDataStore);
+ }
+}
diff --git a/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/WebhookUtils.java b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/WebhookUtils.java
new file mode 100644
index 000000000..be209c78e
--- /dev/null
+++ b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/WebhookUtils.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.api.client.googleapis.extensions.servlet.notifications.jakarta;
+
+import com.google.api.client.googleapis.extensions.servlet.notifications.WebhookHeaders;
+import com.google.api.client.googleapis.notifications.StoredChannel;
+import com.google.api.client.googleapis.notifications.UnparsedNotification;
+import com.google.api.client.googleapis.notifications.UnparsedNotificationCallback;
+import com.google.api.client.util.Beta;
+import com.google.api.client.util.LoggingInputStream;
+import com.google.api.client.util.Preconditions;
+import com.google.api.client.util.StringUtils;
+import com.google.api.client.util.store.DataStore;
+import com.google.api.client.util.store.DataStoreFactory;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * {@link Beta}
+ * Utilities for Webhook notifications using the {@code jakarta.servlet} namespace.
+ *
+ * @since 2.6.0
+ */
+@Beta
+public final class WebhookUtils {
+
+ static final Logger LOGGER = Logger.getLogger(WebhookUtils.class.getName());
+
+ /** Webhook notification channel type to use in the watch request. */
+ public static final String TYPE = "web_hook";
+
+ /**
+ * Utility method to process the webhook notification from {@link HttpServlet#doPost} by finding
+ * the notification channel in the given data store factory.
+ *
+ * It is a wrapper around {@link #processWebhookNotification(HttpServletRequest,
+ * HttpServletResponse, DataStore)} that uses the data store from {@link
+ * StoredChannel#getDefaultDataStore(DataStoreFactory)}.
+ *
+ * @param req an {@link HttpServletRequest} object that contains the request the client has made
+ * of the servlet
+ * @param resp an {@link HttpServletResponse} object that contains the response the servlet sends
+ * to the client
+ * @param dataStoreFactory data store factory
+ * @exception IOException if an input or output error is detected when the servlet handles the
+ * request
+ * @exception ServletException if the request for the POST could not be handled
+ */
+ public static void processWebhookNotification(
+ HttpServletRequest req, HttpServletResponse resp, DataStoreFactory dataStoreFactory)
+ throws ServletException, IOException {
+ processWebhookNotification(req, resp, StoredChannel.getDefaultDataStore(dataStoreFactory));
+ }
+
+ /**
+ * Utility method to process the webhook notification from {@link HttpServlet#doPost}.
+ *
+ *
The {@link HttpServletRequest#getInputStream()} is closed in a finally block inside this
+ * method. If it is not detected to be a webhook notification, an {@link
+ * HttpServletResponse#SC_BAD_REQUEST} error will be displayed. If the notification channel is
+ * found in the given notification channel data store, it will call {@link
+ * UnparsedNotificationCallback#onNotification} for the registered notification callback method.
+ *
+ * @param req an {@link HttpServletRequest} object that contains the request the client has made
+ * of the servlet
+ * @param resp an {@link HttpServletResponse} object that contains the response the servlet sends
+ * to the client
+ * @param channelDataStore notification channel data store
+ * @exception IOException if an input or output error is detected when the servlet handles the
+ * request
+ * @exception ServletException if the request for the POST could not be handled
+ */
+ public static void processWebhookNotification(
+ HttpServletRequest req, HttpServletResponse resp, DataStore channelDataStore)
+ throws ServletException, IOException {
+ Preconditions.checkArgument("POST".equals(req.getMethod()));
+ InputStream contentStream = req.getInputStream();
+ try {
+ // log headers
+ if (LOGGER.isLoggable(Level.CONFIG)) {
+ StringBuilder builder = new StringBuilder();
+ Enumeration> e = req.getHeaderNames();
+ if (e != null) {
+ while (e.hasMoreElements()) {
+ Object nameObj = e.nextElement();
+ if (nameObj instanceof String) {
+ String name = (String) nameObj;
+ Enumeration> ev = req.getHeaders(name);
+ if (ev != null) {
+ while (ev.hasMoreElements()) {
+ builder
+ .append(name)
+ .append(": ")
+ .append(ev.nextElement())
+ .append(StringUtils.LINE_SEPARATOR);
+ }
+ }
+ }
+ }
+ }
+ LOGGER.config(builder.toString());
+ contentStream = new LoggingInputStream(contentStream, LOGGER, Level.CONFIG, 0x4000);
+ // TODO(yanivi): allow to override logging content limit
+ }
+ // parse the relevant headers, and create a notification
+ Long messageNumber;
+ try {
+ messageNumber = Long.valueOf(req.getHeader(WebhookHeaders.MESSAGE_NUMBER));
+ } catch (NumberFormatException e) {
+ messageNumber = null;
+ }
+ String resourceState = req.getHeader(WebhookHeaders.RESOURCE_STATE);
+ String resourceId = req.getHeader(WebhookHeaders.RESOURCE_ID);
+ String resourceUri = req.getHeader(WebhookHeaders.RESOURCE_URI);
+ String channelId = req.getHeader(WebhookHeaders.CHANNEL_ID);
+ String channelExpiration = req.getHeader(WebhookHeaders.CHANNEL_EXPIRATION);
+ String channelToken = req.getHeader(WebhookHeaders.CHANNEL_TOKEN);
+ String changed = req.getHeader(WebhookHeaders.CHANGED);
+ if (messageNumber == null
+ || resourceState == null
+ || resourceId == null
+ || resourceUri == null
+ || channelId == null) {
+ resp.sendError(
+ HttpServletResponse.SC_BAD_REQUEST,
+ "Notification did not contain all required information.");
+ return;
+ }
+ UnparsedNotification notification =
+ new UnparsedNotification(messageNumber, resourceState, resourceId, resourceUri, channelId)
+ .setChannelExpiration(channelExpiration)
+ .setChannelToken(channelToken)
+ .setChanged(changed)
+ .setContentType(req.getContentType())
+ .setContentStream(contentStream);
+ // check if we know about the channel, hand over the notification to the notification callback
+ StoredChannel storedChannel = channelDataStore.get(notification.getChannelId());
+ if (storedChannel != null) {
+ storedChannel.getNotificationCallback().onNotification(storedChannel, notification);
+ }
+ } finally {
+ contentStream.close();
+ }
+ }
+
+ private WebhookUtils() {}
+}
diff --git a/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/package-info.java b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/package-info.java
new file mode 100644
index 000000000..796005fec
--- /dev/null
+++ b/google-api-client-servlet/src/main/java/com/google/api/client/googleapis/extensions/servlet/notifications/jakarta/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * 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.
+ */
+
+/**
+ * {@link com.google.api.client.util.Beta}
+ * Support for subscribing to topics and receiving notifications on servlet-based platforms using
+ * {@code jakarta.servlet} namespace.
+ *
+ * @since 2.6.0
+ */
+@com.google.api.client.util.Beta
+package com.google.api.client.googleapis.extensions.servlet.notifications.jakarta;
diff --git a/pom.xml b/pom.xml
index 6918eba68..34bdf9f36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,6 +130,11 @@
jsr305
${project.jsr305.version}
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${project.jakarta-servlet-api.version}
+
javax.jdo
jdo2-api
@@ -525,6 +530,8 @@
3.2.1
4.0.3
2.5
+
+ 5.0.0
false
2.10.1