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