Skip to content

Commit e928cfa

Browse files
feat: WebHookUtil classes using the jakarta namespace (#2484)
* feat: WebHookUtil classes using the jakarta namespace Fixes #2260 Copied the implementation with the jakarta.servlet namespace using the same methology as googleapis/google-oauth-java-client#1115. - Replaced javax with jakarta and declared the dependency with provided scope. - Keeping the classes as `Beta` annotated. - No tests exist for the classes. - Added `@since 2.6.0` as the last release was 2.5.1. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 3ba13b4 commit e928cfa

File tree

5 files changed

+329
-0
lines changed

5 files changed

+329
-0
lines changed

google-api-client-servlet/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@
8484
<dependency>
8585
<groupId>javax.servlet</groupId>
8686
<artifactId>servlet-api</artifactId>
87+
<scope>provided</scope>
88+
</dependency>
89+
<dependency>
90+
<groupId>jakarta.servlet</groupId>
91+
<artifactId>jakarta.servlet-api</artifactId>
92+
<scope>provided</scope>
8793
</dependency>
8894
</dependencies>
8995
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2013 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
15+
package com.google.api.client.googleapis.extensions.servlet.notifications.jakarta;
16+
17+
import com.google.api.client.googleapis.notifications.StoredChannel;
18+
import com.google.api.client.util.Beta;
19+
import com.google.api.client.util.store.DataStore;
20+
import com.google.api.client.util.store.DataStoreFactory;
21+
import com.google.api.client.util.store.MemoryDataStoreFactory;
22+
import jakarta.servlet.ServletException;
23+
import jakarta.servlet.http.HttpServlet;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import java.io.IOException;
27+
28+
/**
29+
* {@link Beta} <br>
30+
* Thread-safe Webhook Servlet to receive notifications using the {@code jakarta.servlet} namespace.
31+
*
32+
* <p>In order to use this servlet you should create a class inheriting from {@link
33+
* NotificationServlet} and register the servlet in your web.xml.
34+
*
35+
* <p>It is a simple wrapper around {@link WebhookUtils#processWebhookNotification}, so if you you
36+
* may alternatively call that method instead from your {@link HttpServlet#doPost} with no loss of
37+
* functionality. <b>Example usage:</b>
38+
*
39+
* <pre>{@code
40+
* public class MyNotificationServlet extends NotificationServlet {
41+
*
42+
* private static final long serialVersionUID = 1L;
43+
*
44+
* public MyNotificationServlet() throws IOException {
45+
* super(new SomeDataStoreFactory());
46+
* }
47+
* }
48+
* }</pre>
49+
*
50+
* <b>Sample web.xml setup:</b>
51+
*
52+
* <pre>{@code
53+
* {@literal <}servlet{@literal >}
54+
* {@literal <}servlet-name{@literal >}MyNotificationServlet{@literal <}/servlet-name{@literal >}
55+
* {@literal <}servlet-class{@literal >}
56+
* com.mypackage.MyNotificationServlet
57+
* {@literal <}/servlet-class{@literal >}
58+
* {@literal <}/servlet{@literal >}
59+
* {@literal <}servlet-mapping{@literal >}
60+
* {@literal <}servlet-name{@literal >}MyNotificationServlet{@literal <}/servlet-name{@literal >}
61+
* {@literal <}url-pattern{@literal >}/notifications{@literal <}/url-pattern{@literal >}
62+
* {@literal <}/servlet-mapping{@literal >}
63+
* }</pre>
64+
*
65+
* <p>WARNING: by default it uses {@link MemoryDataStoreFactory#getDefaultInstance()} which means it
66+
* will NOT persist the notification channels when the servlet process dies, so it is a BAD CHOICE
67+
* for a production application. But it is a convenient choice when testing locally, in which case
68+
* you don't need to override it, and can simply reference it directly in your web.xml file. For
69+
* example:
70+
*
71+
* <pre>{@code
72+
* {@literal <}servlet{@literal >}
73+
* {@literal <}servlet-name{@literal >}NotificationServlet{@literal <}/servlet-name{@literal >}
74+
* {@literal <}servlet-class{@literal >}
75+
* com.google.api.client.googleapis.extensions.servlet.notificationsNotificationServlet
76+
* {@literal <}/servlet-class{@literal >}
77+
* {@literal <}/servlet{@literal >}
78+
* {@literal <}servlet-mapping{@literal >}
79+
* {@literal <}servlet-name{@literal >}NotificationServlet{@literal <}/servlet-name{@literal >}
80+
* {@literal <}url-pattern{@literal >}/notifications{@literal <}/url-pattern{@literal >}
81+
* {@literal <}/servlet-mapping{@literal >}
82+
* }</pre>
83+
*
84+
* @since 2.6.0
85+
*/
86+
@Beta
87+
public class NotificationServlet extends HttpServlet {
88+
89+
private static final long serialVersionUID = 1L;
90+
91+
/** Notification channel data store. */
92+
private final transient DataStore<StoredChannel> channelDataStore;
93+
94+
/**
95+
* Constructor to be used for testing and demo purposes that uses {@link
96+
* MemoryDataStoreFactory#getDefaultInstance()} which means it will NOT persist the notification
97+
* channels when the servlet process dies, so it is a bad choice for a production application.
98+
*/
99+
public NotificationServlet() throws IOException {
100+
this(MemoryDataStoreFactory.getDefaultInstance());
101+
}
102+
103+
/**
104+
* Constructor which uses {@link StoredChannel#getDefaultDataStore(DataStoreFactory)} on the given
105+
* data store factory, which is the normal use case.
106+
*
107+
* @param dataStoreFactory data store factory
108+
*/
109+
protected NotificationServlet(DataStoreFactory dataStoreFactory) throws IOException {
110+
this(StoredChannel.getDefaultDataStore(dataStoreFactory));
111+
}
112+
113+
/**
114+
* Constructor that allows a specific notification data store to be specified.
115+
*
116+
* @param channelDataStore notification channel data store
117+
*/
118+
protected NotificationServlet(DataStore<StoredChannel> channelDataStore) {
119+
this.channelDataStore = channelDataStore;
120+
}
121+
122+
@Override
123+
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
124+
throws ServletException, IOException {
125+
WebhookUtils.processWebhookNotification(req, resp, channelDataStore);
126+
}
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2013 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
15+
package com.google.api.client.googleapis.extensions.servlet.notifications.jakarta;
16+
17+
import com.google.api.client.googleapis.extensions.servlet.notifications.WebhookHeaders;
18+
import com.google.api.client.googleapis.notifications.StoredChannel;
19+
import com.google.api.client.googleapis.notifications.UnparsedNotification;
20+
import com.google.api.client.googleapis.notifications.UnparsedNotificationCallback;
21+
import com.google.api.client.util.Beta;
22+
import com.google.api.client.util.LoggingInputStream;
23+
import com.google.api.client.util.Preconditions;
24+
import com.google.api.client.util.StringUtils;
25+
import com.google.api.client.util.store.DataStore;
26+
import com.google.api.client.util.store.DataStoreFactory;
27+
import jakarta.servlet.ServletException;
28+
import jakarta.servlet.http.HttpServlet;
29+
import jakarta.servlet.http.HttpServletRequest;
30+
import jakarta.servlet.http.HttpServletResponse;
31+
import java.io.IOException;
32+
import java.io.InputStream;
33+
import java.util.Enumeration;
34+
import java.util.logging.Level;
35+
import java.util.logging.Logger;
36+
37+
/**
38+
* {@link Beta} <br>
39+
* Utilities for Webhook notifications using the {@code jakarta.servlet} namespace.
40+
*
41+
* @since 2.6.0
42+
*/
43+
@Beta
44+
public final class WebhookUtils {
45+
46+
static final Logger LOGGER = Logger.getLogger(WebhookUtils.class.getName());
47+
48+
/** Webhook notification channel type to use in the watch request. */
49+
public static final String TYPE = "web_hook";
50+
51+
/**
52+
* Utility method to process the webhook notification from {@link HttpServlet#doPost} by finding
53+
* the notification channel in the given data store factory.
54+
*
55+
* <p>It is a wrapper around {@link #processWebhookNotification(HttpServletRequest,
56+
* HttpServletResponse, DataStore)} that uses the data store from {@link
57+
* StoredChannel#getDefaultDataStore(DataStoreFactory)}.
58+
*
59+
* @param req an {@link HttpServletRequest} object that contains the request the client has made
60+
* of the servlet
61+
* @param resp an {@link HttpServletResponse} object that contains the response the servlet sends
62+
* to the client
63+
* @param dataStoreFactory data store factory
64+
* @exception IOException if an input or output error is detected when the servlet handles the
65+
* request
66+
* @exception ServletException if the request for the POST could not be handled
67+
*/
68+
public static void processWebhookNotification(
69+
HttpServletRequest req, HttpServletResponse resp, DataStoreFactory dataStoreFactory)
70+
throws ServletException, IOException {
71+
processWebhookNotification(req, resp, StoredChannel.getDefaultDataStore(dataStoreFactory));
72+
}
73+
74+
/**
75+
* Utility method to process the webhook notification from {@link HttpServlet#doPost}.
76+
*
77+
* <p>The {@link HttpServletRequest#getInputStream()} is closed in a finally block inside this
78+
* method. If it is not detected to be a webhook notification, an {@link
79+
* HttpServletResponse#SC_BAD_REQUEST} error will be displayed. If the notification channel is
80+
* found in the given notification channel data store, it will call {@link
81+
* UnparsedNotificationCallback#onNotification} for the registered notification callback method.
82+
*
83+
* @param req an {@link HttpServletRequest} object that contains the request the client has made
84+
* of the servlet
85+
* @param resp an {@link HttpServletResponse} object that contains the response the servlet sends
86+
* to the client
87+
* @param channelDataStore notification channel data store
88+
* @exception IOException if an input or output error is detected when the servlet handles the
89+
* request
90+
* @exception ServletException if the request for the POST could not be handled
91+
*/
92+
public static void processWebhookNotification(
93+
HttpServletRequest req, HttpServletResponse resp, DataStore<StoredChannel> channelDataStore)
94+
throws ServletException, IOException {
95+
Preconditions.checkArgument("POST".equals(req.getMethod()));
96+
InputStream contentStream = req.getInputStream();
97+
try {
98+
// log headers
99+
if (LOGGER.isLoggable(Level.CONFIG)) {
100+
StringBuilder builder = new StringBuilder();
101+
Enumeration<?> e = req.getHeaderNames();
102+
if (e != null) {
103+
while (e.hasMoreElements()) {
104+
Object nameObj = e.nextElement();
105+
if (nameObj instanceof String) {
106+
String name = (String) nameObj;
107+
Enumeration<?> ev = req.getHeaders(name);
108+
if (ev != null) {
109+
while (ev.hasMoreElements()) {
110+
builder
111+
.append(name)
112+
.append(": ")
113+
.append(ev.nextElement())
114+
.append(StringUtils.LINE_SEPARATOR);
115+
}
116+
}
117+
}
118+
}
119+
}
120+
LOGGER.config(builder.toString());
121+
contentStream = new LoggingInputStream(contentStream, LOGGER, Level.CONFIG, 0x4000);
122+
// TODO(yanivi): allow to override logging content limit
123+
}
124+
// parse the relevant headers, and create a notification
125+
Long messageNumber;
126+
try {
127+
messageNumber = Long.valueOf(req.getHeader(WebhookHeaders.MESSAGE_NUMBER));
128+
} catch (NumberFormatException e) {
129+
messageNumber = null;
130+
}
131+
String resourceState = req.getHeader(WebhookHeaders.RESOURCE_STATE);
132+
String resourceId = req.getHeader(WebhookHeaders.RESOURCE_ID);
133+
String resourceUri = req.getHeader(WebhookHeaders.RESOURCE_URI);
134+
String channelId = req.getHeader(WebhookHeaders.CHANNEL_ID);
135+
String channelExpiration = req.getHeader(WebhookHeaders.CHANNEL_EXPIRATION);
136+
String channelToken = req.getHeader(WebhookHeaders.CHANNEL_TOKEN);
137+
String changed = req.getHeader(WebhookHeaders.CHANGED);
138+
if (messageNumber == null
139+
|| resourceState == null
140+
|| resourceId == null
141+
|| resourceUri == null
142+
|| channelId == null) {
143+
resp.sendError(
144+
HttpServletResponse.SC_BAD_REQUEST,
145+
"Notification did not contain all required information.");
146+
return;
147+
}
148+
UnparsedNotification notification =
149+
new UnparsedNotification(messageNumber, resourceState, resourceId, resourceUri, channelId)
150+
.setChannelExpiration(channelExpiration)
151+
.setChannelToken(channelToken)
152+
.setChanged(changed)
153+
.setContentType(req.getContentType())
154+
.setContentStream(contentStream);
155+
// check if we know about the channel, hand over the notification to the notification callback
156+
StoredChannel storedChannel = channelDataStore.get(notification.getChannelId());
157+
if (storedChannel != null) {
158+
storedChannel.getNotificationCallback().onNotification(storedChannel, notification);
159+
}
160+
} finally {
161+
contentStream.close();
162+
}
163+
}
164+
165+
private WebhookUtils() {}
166+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2024 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
15+
/**
16+
* {@link com.google.api.client.util.Beta} <br>
17+
* Support for subscribing to topics and receiving notifications on servlet-based platforms using
18+
* {@code jakarta.servlet} namespace.
19+
*
20+
* @since 2.6.0
21+
*/
22+
@com.google.api.client.util.Beta
23+
package com.google.api.client.googleapis.extensions.servlet.notifications.jakarta;

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@
130130
<artifactId>jsr305</artifactId>
131131
<version>${project.jsr305.version}</version>
132132
</dependency>
133+
<dependency>
134+
<groupId>jakarta.servlet</groupId>
135+
<artifactId>jakarta.servlet-api</artifactId>
136+
<version>${project.jakarta-servlet-api.version}</version>
137+
</dependency>
133138
<dependency>
134139
<groupId>javax.jdo</groupId>
135140
<artifactId>jdo2-api</artifactId>
@@ -525,6 +530,8 @@
525530
<project.datanucleus-api-jdo.version>3.2.1</project.datanucleus-api-jdo.version>
526531
<project.datanucleus-maven-plugin.version>4.0.3</project.datanucleus-maven-plugin.version>
527532
<project.servlet-api.version>2.5</project.servlet-api.version>
533+
<!-- jakarta-servlet-api 5.0.0 is the last version that works with Java 8 -->
534+
<project.jakarta-servlet-api.version>5.0.0</project.jakarta-servlet-api.version>
528535
<deploy.autorelease>false</deploy.autorelease>
529536
<gson.version>2.10.1</gson.version>
530537
</properties>

0 commit comments

Comments
 (0)