Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WebHookUtil classes using the jakarta namespace #2484

Merged
merged 2 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions google-api-client-servlet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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} <br>
* Thread-safe Webhook Servlet to receive notifications using the {@code jakarta.servlet} namespace.
*
* <p>In order to use this servlet you should create a class inheriting from {@link
* NotificationServlet} and register the servlet in your web.xml.
*
* <p>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. <b>Example usage:</b>
*
* <pre>{@code
* public class MyNotificationServlet extends NotificationServlet {
*
* private static final long serialVersionUID = 1L;
*
* public MyNotificationServlet() throws IOException {
* super(new SomeDataStoreFactory());
* }
* }
* }</pre>
*
* <b>Sample web.xml setup:</b>
*
* <pre>{@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 >}
* }</pre>
*
* <p>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:
*
* <pre>{@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 >}
* }</pre>
*
* @since 2.6.0
*/
@Beta
public class NotificationServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

/** Notification channel data store. */
private final transient DataStore<StoredChannel> 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<StoredChannel> channelDataStore) {
this.channelDataStore = channelDataStore;
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
WebhookUtils.processWebhookNotification(req, resp, channelDataStore);
}
}
Original file line number Diff line number Diff line change
@@ -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} <br>
* 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.
*
* <p>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}.
*
* <p>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<StoredChannel> 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() {}
}
Original file line number Diff line number Diff line change
@@ -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} <br>
* 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;
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
<artifactId>jsr305</artifactId>
<version>${project.jsr305.version}</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${project.jakarta-servlet-api.version}</version>
</dependency>
<dependency>
<groupId>javax.jdo</groupId>
<artifactId>jdo2-api</artifactId>
Expand Down Expand Up @@ -525,6 +530,8 @@
<project.datanucleus-api-jdo.version>3.2.1</project.datanucleus-api-jdo.version>
<project.datanucleus-maven-plugin.version>4.0.3</project.datanucleus-maven-plugin.version>
<project.servlet-api.version>2.5</project.servlet-api.version>
<!-- jakarta-servlet-api 5.0.0 is the last version that works with Java 8 -->
<project.jakarta-servlet-api.version>5.0.0</project.jakarta-servlet-api.version>
<deploy.autorelease>false</deploy.autorelease>
<gson.version>2.10.1</gson.version>
</properties>
Expand Down
Loading