You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2024/09/24 13:23:49 UTC

(sling-samples) branch master updated: feat(oauth): add a GitHub demo page

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-samples.git


The following commit(s) were added to refs/heads/master by this push:
     new baa6f52  feat(oauth): add a GitHub demo page
baa6f52 is described below

commit baa6f521eb9ad4c4e43abc9d9619a2653754d9c7
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Sep 24 15:23:10 2024 +0200

    feat(oauth): add a GitHub demo page
---
 .../oauth_demo/GithubRepositoriesModel.java        | 72 ++++++++++++++++++++++
 .../GithubRepositoriesSearchResponse.java          | 26 ++++++++
 .../sling/samples/oauth_demo/GithubRepository.java | 25 ++++++++
 .../samples/oauth_demo/impl/GithubHttpClient.java  | 57 +++++++++++++++++
 ...SearchServlet.java => GithubSearchServlet.java} | 59 +++++++++---------
 .../oauth_demo/impl/YoutubeSearchServlet.java      |  4 +-
 oauth/launcher/Makefile                            |  9 +++
 oauth/launcher/src/main/features/launcher.json     |  8 +++
 .../components/github-repos/.content.xml           | 22 +++++++
 .../components/github-repos/github-repos.html      | 36 +++++++++++
 .../oauth-demo/components/welcome/welcome.html     |  1 +
 .../content/oauth-demo/repositories/.content.xml   | 24 ++++++++
 12 files changed, 311 insertions(+), 32 deletions(-)

diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesModel.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesModel.java
new file mode 100644
index 0000000..28e0f0d
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesModel.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.extensions.oauth_client.ClientConnection;
+import org.apache.sling.extensions.oauth_client.OAuthToken;
+import org.apache.sling.extensions.oauth_client.OAuthTokenStore;
+import org.apache.sling.extensions.oauth_client.OAuthUris;
+import org.apache.sling.extensions.oauth_client.TokenState;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.OSGiService;
+import org.apache.sling.models.annotations.injectorspecific.SlingObject;
+import org.apache.sling.samples.oauth_demo.impl.GithubHttpClient;
+
+@Model(adaptables = SlingHttpServletRequest.class)
+public class GithubRepositoriesModel {
+    
+    @SlingObject
+    private SlingHttpServletRequest request;
+    
+    // get a reference to an OSGi service
+    @OSGiService(filter = "(name=github)")
+    private ClientConnection connection;
+    
+    @OSGiService
+    private OAuthTokenStore tokenStore;
+    
+    public boolean needsLogin() {
+        return getAccessToken().getState() != TokenState.VALID;
+    }
+
+    public boolean hasSearch() {
+        String searchParam = request.getParameter("search");
+        
+        return searchParam != null && searchParam.trim().length() > 0;
+    }
+    
+    private OAuthToken getAccessToken() {
+        return tokenStore.getAccessToken(connection, request.getResourceResolver());
+    }
+    
+    public GithubRepositoriesSearchResponse getSearchResponse() throws IOException, InterruptedException {
+        if ( hasSearch() ) {
+            return new GithubHttpClient().searchRepositories(request.getParameter("search"), getAccessToken().getValue());
+        }
+        
+        return null;
+    }
+    
+    public URI getGithubLoginUrl() {
+        return OAuthUris.getOidcEntryPointUri(connection, request, request.getRequestURI());
+    }
+
+}
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesSearchResponse.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesSearchResponse.java
new file mode 100644
index 0000000..69d91b7
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepositoriesSearchResponse.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record GithubRepositoriesSearchResponse(@JsonProperty("total_count") int totalCount,
+        List<GithubRepository> items) {
+
+}
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepository.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepository.java
new file mode 100644
index 0000000..31bfd61
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/GithubRepository.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record GithubRepository(String name,
+        @JsonProperty("full_name") String fullName,
+        @JsonProperty("html_url") String htmlUrl,
+        String description) {
+}
diff --git a/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/GithubHttpClient.java b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/GithubHttpClient.java
new file mode 100644
index 0000000..a81cbe3
--- /dev/null
+++ b/oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/GithubHttpClient.java
@@ -0,0 +1,57 @@
+/*
+ * 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 java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sling.samples.oauth_demo.GithubRepositoriesSearchResponse;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class GithubHttpClient {
+    public static final String API_BASE = "https://api.github.com";
+    
+    public GithubRepositoriesSearchResponse searchRepositories(String query, String accessToken) throws IOException, InterruptedException {
+        
+        try ( HttpClient client = HttpClient.newHttpClient() ) {
+            query = URLEncoder.encode(query.trim(), StandardCharsets.UTF_8);
+            HttpRequest searchRequest = HttpRequest.newBuilder()
+                .uri(URI.create(API_BASE + "/search/repositories?q=" + query))
+                .header("Authorization", "Bearer " + accessToken)
+                .header("Accept", "application/vnd.github+json")
+                .header("X-GitHub-Api-Version", "2022-11-28")
+                .build();
+            
+            HttpResponse<String> response = client.send(searchRequest, BodyHandlers.ofString());
+    
+            String body = response.body();
+            
+            ObjectMapper mapper = new ObjectMapper();
+            // client mapping is still incomplete
+            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+            return mapper.reader().readValue(body, GithubRepositoriesSearchResponse.class);
+        }
+    }   
+}
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/GithubSearchServlet.java
similarity index 52%
copy from oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/YoutubeSearchServlet.java
copy to oauth/core/src/main/java/org/apache/sling/samples/oauth_demo/impl/GithubSearchServlet.java
index 363e251..202b230 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/GithubSearchServlet.java
@@ -24,55 +24,54 @@ import javax.servlet.http.HttpServletResponse;
 
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.SlingHttpServletResponse;
-import org.apache.sling.api.request.RequestDispatcherOptions;
+import org.apache.sling.extensions.oauth_client.ClientConnection;
 import org.apache.sling.extensions.oauth_client.OAuthToken;
 import org.apache.sling.extensions.oauth_client.OAuthTokenRefresher;
 import org.apache.sling.extensions.oauth_client.OAuthTokenStore;
-import org.apache.sling.extensions.oauth_client.OidcConnection;
 import org.apache.sling.extensions.oauth_client.support.OAuthEnabledSlingServlet;
-import org.apache.sling.samples.oauth_demo.YoutubeSearchResponse;
-import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
+import org.apache.sling.samples.oauth_demo.GithubRepositoriesSearchResponse;
+import org.apache.sling.servlets.annotations.SlingServletPaths;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 
 @Component(service = { Servlet.class })
-@SlingServletResourceTypes(resourceTypes = "oauth-demo/components/youtube-videos", methods = "GET", extensions = "html")
-public class YoutubeSearchServlet extends OAuthEnabledSlingServlet {
+@SlingServletPaths("/bin/github/search")
+public class GithubSearchServlet extends OAuthEnabledSlingServlet {
 
-    private static final long serialVersionUID = 1L;
-    
     @Activate
-    public YoutubeSearchServlet(@Reference OidcConnection connection, @Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher oidcClient) {
+    public GithubSearchServlet(@Reference(target = "(name=github)") ClientConnection connection,
+            @Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher oidcClient) {
         super(connection, tokenStore, oidcClient);
     }
 
-    @Override
-    protected @NotNull String getRedirectPath(@NotNull SlingHttpServletRequest request) {
-        return request.getRequestURI();
-    }
-
     @Override
     protected void doGetWithToken(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response,
-            OAuthToken token) throws IOException, ServletException {
+            OAuthToken token) throws ServletException, IOException {
 
-        try {
-            String search = request.getParameter("search");
-            if (search != null && !search.isBlank()) {
-                YoutubeHttpClient client = new YoutubeHttpClient();
-                YoutubeSearchResponse youtubeResponse = client.searchVideos(search, token.getValue());
-                request.setAttribute("ytResponse", youtubeResponse);
+        String search = request.getParameter("search");
+
+        if (search != null && !search.isBlank()) {
+            try {
+                GithubHttpClient client = new GithubHttpClient();
+                GithubRepositoriesSearchResponse repositories = client.searchRepositories(search, token.getValue());
+                
+                response.setContentType("text/plain");
+                response.getWriter().write("Repositories found: " + repositories.totalCount() + "\n\n");
+                repositories.items().forEach(repo -> {
+                    try {
+                        response.getWriter().write(repo.fullName() + "\n");
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Please retry later");
             }
-            
-            RequestDispatcherOptions opts = new RequestDispatcherOptions();
-            opts.setReplaceSelectors("show");
-            
-            response.setContentType("text/html");
-            request.getRequestDispatcher(request.getResource(), opts).include(request, response);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Please retry later");
         }
+
     }
+
 }
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 363e251..4f9e218 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
@@ -25,10 +25,10 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.SlingHttpServletResponse;
 import org.apache.sling.api.request.RequestDispatcherOptions;
+import org.apache.sling.extensions.oauth_client.ClientConnection;
 import org.apache.sling.extensions.oauth_client.OAuthToken;
 import org.apache.sling.extensions.oauth_client.OAuthTokenRefresher;
 import org.apache.sling.extensions.oauth_client.OAuthTokenStore;
-import org.apache.sling.extensions.oauth_client.OidcConnection;
 import org.apache.sling.extensions.oauth_client.support.OAuthEnabledSlingServlet;
 import org.apache.sling.samples.oauth_demo.YoutubeSearchResponse;
 import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
@@ -44,7 +44,7 @@ public class YoutubeSearchServlet extends OAuthEnabledSlingServlet {
     private static final long serialVersionUID = 1L;
     
     @Activate
-    public YoutubeSearchServlet(@Reference OidcConnection connection, @Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher oidcClient) {
+    public YoutubeSearchServlet(@Reference(target = "(name=google)") ClientConnection connection, @Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher oidcClient) {
         super(connection, tokenStore, oidcClient);
     }
 
diff --git a/oauth/launcher/Makefile b/oauth/launcher/Makefile
index dc1e5d2..5a11b0f 100644
--- a/oauth/launcher/Makefile
+++ b/oauth/launcher/Makefile
@@ -13,12 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+DEBUG_FLAGS := -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
+export JAVA_OPTS := $(DEBUG_FLAGS)
+
 secrets/google/clientId:
 	@echo [WARN] $@ missing, OIDC config needs to be created manually
 
 secrets/google/clientSecret:
 	@echo [WARN] $@ missing, OIDC config needs to be created manually
 
+secrets/github/clientId:
+	@echo [WARN] $@ missing, OAuth config needs to be created manually
+
+secrets/github/clientSecret:
+	@echo [WARN] $@ missing, OAuth config needs to be created manually
+
 run: target/dependency/org.apache.sling.feature.launcher/bin/launcher secrets/google/clientId secrets/google/clientSecret
 	@echo '-------------------------------------------------------------------------------------------'
 	@echo '[NOTE] Launching application, this will fail if you did not build the project at least once'
diff --git a/oauth/launcher/src/main/features/launcher.json b/oauth/launcher/src/main/features/launcher.json
index 6fd3196..11e450a 100644
--- a/oauth/launcher/src/main/features/launcher.json
+++ b/oauth/launcher/src/main/features/launcher.json
@@ -22,6 +22,14 @@
         "clientSecret": "$[secret:google/clientSecret]",
         "scopes": ["openid", "https://www.googleapis.com/auth/youtube.force-ssl"]
     },
+    "org.apache.sling.extensions.oauth_client.impl.OAuthConnectionImpl~github": {
+        "name": "github",
+        "authorizationEndpoint": "https://github.com/login/oauth/authorize",
+        "tokenEndpoint": "https://github.com/login/oauth/access_token",
+        "clientId": "$[secret:github/clientId]",
+        "clientSecret": "$[secret:github/clientSecret]",
+        "scopes": ["user:email"]
+    },
     "org.apache.sling.extensions.oauth_client.impl.JcrUserHomeOAuthTokenStore" : {
     }
   }
diff --git a/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/.content.xml b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/.content.xml
new file mode 100644
index 0000000..20e138a
--- /dev/null
+++ b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/.content.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
+          jcr:primaryType="sling:Folder"
+/>
diff --git a/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/github-repos.html b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/github-repos.html
new file mode 100644
index 0000000..f9ebca5
--- /dev/null
+++ b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/github-repos/github-repos.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link href="/oauth-demo/static/css/output.css" rel="stylesheet">
+</head>
+<body data-sly-use.github="org.apache.sling.samples.oauth_demo.GithubRepositoriesModel">
+    <main>
+        <header class="flex flex-row justify-between">
+            <h1>GitHub Repositories</h1>
+            <form>
+                <input type="text" id="search" name="search" value="${youtube.search}">
+                <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Search</button>
+            </form>
+        </header>
+        <div data-sly-test="${github.searchResponse}">
+            <p>Repository count: ${github.searchResponse.totalCount}</p>
+            <div data-sly-list.item="${github.searchResponse.items}"
+                class="flex flex-row flex-wrap">
+                <div class="basis-1/3">
+                    <div class="px-4 bg-white rounded overflow-hidden shadow-lg m-2">
+                        <a href="${item.htmlUrl}" target="_blank">
+                            <h2>${item.fullName}</h2>
+                            <p>${item.description}</p>
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div data-sly-test="${github.needsLogin}">
+            <p>Please <a href="${github.githubLoginUrl}">connect your account to github</a> to view the repositories.</p>
+        </div>
+    </main>
+</body>
+</html>
\ No newline at end of file
diff --git a/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/welcome/welcome.html b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/welcome/welcome.html
index d424700..df5f709 100644
--- a/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/welcome/welcome.html
+++ b/oauth/ui.apps/src/main/content/jcr_root/apps/oauth-demo/components/welcome/welcome.html
@@ -9,6 +9,7 @@
     <main>
         <h1>Sling OAuth Demo</h1>
         <p>Access the <a class="underline" href="oauth-demo/videos.html">YouTube videos</a> page.</p>
+        <p>Access the <a class="underline" href="oauth-demo/repositories.html">GitHub repositories</a> page.</p>
     </main>
 </body>
 </html>
\ No newline at end of file
diff --git a/oauth/ui.apps/src/main/content/jcr_root/content/oauth-demo/repositories/.content.xml b/oauth/ui.apps/src/main/content/jcr_root/content/oauth-demo/repositories/.content.xml
new file mode 100644
index 0000000..4ca959b
--- /dev/null
+++ b/oauth/ui.apps/src/main/content/jcr_root/content/oauth-demo/repositories/.content.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+          xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
+          jcr:primaryType="sling:Folder"
+          sling:resourceType="oauth-demo/components/github-repos"
+/>
\ No newline at end of file