feat(oauth): support clearing tokens for YouTube
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/Error.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/Error.java
index 3e3cec3..cb96d39 100644
--- a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/Error.java
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/Error.java
@@ -5,5 +5,6 @@
 public record Error (
     int code,
     String message,
+    String status,
     List<ErrorDetail> errors
 ) {}
\ No newline at end of file
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/ErrorDetail.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/ErrorDetail.java
index 7cb0bc7..5a3e84a 100644
--- a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/ErrorDetail.java
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/ErrorDetail.java
@@ -3,5 +3,7 @@
 public record ErrorDetail (
     String message,
     String domain,
-    String reason
+    String reason,
+    String location,
+    String locationType
 ) {}
\ No newline at end of file
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/InvalidAccessTokenException.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/InvalidAccessTokenException.java
new file mode 100644
index 0000000..0136527
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/InvalidAccessTokenException.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.samples.oauth_demo.impl;
+
+public class InvalidAccessTokenException extends YoutubeApiException {
+
+    private static final long serialVersionUID = 1L;
+
+    public InvalidAccessTokenException(String message) {
+        super(message);
+    }
+
+    public InvalidAccessTokenException(Throwable cause) {
+        super(cause);
+    }
+
+    public InvalidAccessTokenException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeApiException.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeApiException.java
new file mode 100644
index 0000000..1191340
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeApiException.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.samples.oauth_demo.impl;
+
+import org.apache.sling.samples.oauth_demo.Error;
+
+public class YoutubeApiException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public static YoutubeApiException fromError(Error error) {
+        
+        boolean invalidToken = error.errors().stream()
+            .anyMatch( ed -> "authError".equals(ed.reason()));
+        
+        String message = error.message();
+        
+        
+        return invalidToken ? new InvalidAccessTokenException(message) : 
+            new YoutubeApiException(message);
+    }
+    
+    public YoutubeApiException(String message) {
+        super(message);
+    }
+
+    public YoutubeApiException(Throwable cause) {
+        super(cause);
+    }
+
+    public YoutubeApiException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeHttpClient.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeHttpClient.java
index 54cbc56..a85c362 100644
--- a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeHttpClient.java
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeHttpClient.java
@@ -33,7 +33,7 @@
     
     public static final String API_BASE = "https://youtube.googleapis.com/youtube/v3";
     
-    public YoutubeSearchResponse searchVideos(String query, String accessToken) throws IOException, InterruptedException {
+    public YoutubeSearchResponse searchVideos(String query, String accessToken) throws IOException, InterruptedException, YoutubeApiException {
         
         try ( HttpClient client = HttpClient.newHttpClient() ) {
             query = URLEncoder.encode(query.trim(), StandardCharsets.UTF_8);
@@ -48,7 +48,14 @@
             String body = response.body();
             
             ObjectMapper mapper = new ObjectMapper();
-            return mapper.reader().readValue(body, YoutubeSearchResponse.class);
+            
+            YoutubeSearchResponse apiResponse = mapper.reader().readValue(body, YoutubeSearchResponse.class);
+
+            if ( response.statusCode() == 401 ) {
+                throw YoutubeApiException.fromError(apiResponse.error());
+            }
+            
+            return apiResponse;
         }
     }        
 }
\ No newline at end of file
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeSearchServlet.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeSearchServlet.java
index 4f9e218..aecca52 100644
--- a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeSearchServlet.java
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeSearchServlet.java
@@ -73,6 +73,13 @@
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Please retry later");
+        } catch (YoutubeApiException e) {
+            throw new ServletException(e);
         }
     }
+    
+    @Override
+    protected boolean isInvalidAccessTokenException(Exception e) {
+        return e.getCause() instanceof InvalidAccessTokenException;
+    }
 }