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 2017/10/18 23:25:46 UTC

[sling-org-apache-sling-tenant] 05/44: SLING-2676 Simple tenant administration through the Web Console (thanks Amit Gupta for providing the patch).

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-org-apache-sling-tenant.git

commit d12fcf0d29bf981370d48f0e1b70017a7dc9cce9
Author: Felix Meschberger <fm...@apache.org>
AuthorDate: Thu Nov 29 13:14:21 2012 +0000

    SLING-2676 Simple tenant administration through the Web Console (thanks Amit Gupta for providing the patch).
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1415151 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  11 ++
 .../sling/tenant/internal/TenantProviderImpl.java  | 144 +++++++++++++--
 .../tenant/internal/console/WebConsolePlugin.java  | 197 +++++++++++++++++++++
 .../apache/sling/tenant/spi/TenantCustomizer.java  |  71 ++++++++
 .../org/apache/sling/tenant/spi/package-info.java  |  24 +++
 5 files changed, 437 insertions(+), 10 deletions(-)

diff --git a/pom.xml b/pom.xml
index 8d68c85..f4b7c7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,6 +98,12 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr-commons</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>commons-lang</groupId>
             <artifactId>commons-lang</artifactId>
             <version>2.0</version>
@@ -118,6 +124,11 @@
             <artifactId>bndlib</artifactId>
             <scope>provided</scope>
         </dependency>
+        <!-- Webconsole -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
 
         <!-- Testing -->
         <dependency>
diff --git a/src/main/java/org/apache/sling/tenant/internal/TenantProviderImpl.java b/src/main/java/org/apache/sling/tenant/internal/TenantProviderImpl.java
index 4ab2c47..45a7933 100644
--- a/src/main/java/org/apache/sling/tenant/internal/TenantProviderImpl.java
+++ b/src/main/java/org/apache/sling/tenant/internal/TenantProviderImpl.java
@@ -19,10 +19,13 @@
 package org.apache.sling.tenant.internal;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -38,8 +41,12 @@ import org.apache.felix.scr.annotations.Properties;
 import org.apache.felix.scr.annotations.Property;
 import org.apache.felix.scr.annotations.PropertyUnbounded;
 import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
 import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.commons.JcrUtils;
 import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistableValueMap;
 import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
@@ -47,8 +54,11 @@ import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.api.resource.ResourceUtil;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.commons.osgi.ServiceUtil;
 import org.apache.sling.tenant.Tenant;
 import org.apache.sling.tenant.TenantProvider;
+import org.apache.sling.tenant.internal.console.WebConsolePlugin;
+import org.apache.sling.tenant.spi.TenantCustomizer;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.Filter;
@@ -61,11 +71,17 @@ import org.osgi.framework.InvalidSyntaxException;
 @Component(
         metatype = true,
         label = "Apache Sling JCR Tenant Provider",
-        description = "Service responsible for providing Tenants")
+        description = "Service responsible for providing Tenants",
+        immediate = true)
 @Service
 @Properties(value = {
     @Property(name = Constants.SERVICE_DESCRIPTION, value = "Apache Sling JCR Tenant Provider")
 })
+@Reference(
+        name = "tenantSetup",
+        referenceInterface = TenantCustomizer.class,
+        cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
+        policy = ReferencePolicy.DYNAMIC)
 public class TenantProviderImpl implements TenantProvider {
     /**
      * Root path for tenant
@@ -77,6 +93,9 @@ public class TenantProviderImpl implements TenantProvider {
 
     private static final String[] DEFAULT_PATH_MATCHER = {};
 
+    private SortedMap<Comparable<Object>, TenantCustomizer> registeredTenantHandlers = new TreeMap<Comparable<Object>, TenantCustomizer>(
+        Collections.reverseOrder());
+
     @Property(
             value = {},
             unbounded = PropertyUnbounded.ARRAY,
@@ -95,10 +114,15 @@ public class TenantProviderImpl implements TenantProvider {
 
     private TenantAdapterFactory adapterFactory;
 
+    private WebConsolePlugin plugin;
+
+    private BundleContext bundleContext;
+
     @Activate
     private void activate(final BundleContext bundleContext, final Map<String, Object> properties) {
         this.tenantRootPath = PropertiesUtil.toString(properties.get(TENANT_ROOT), JCR_TENANT_ROOT);
         this.pathMatchers = PropertiesUtil.toStringArray(properties.get(TENANT_PATH_MATCHER), DEFAULT_PATH_MATCHER);
+        this.bundleContext = bundleContext;
 
         this.pathPatterns.clear();
         for (String matcherStr : this.pathMatchers) {
@@ -106,6 +130,7 @@ public class TenantProviderImpl implements TenantProvider {
         }
 
         this.adapterFactory = new TenantAdapterFactory(bundleContext, this);
+        this.plugin = new WebConsolePlugin(bundleContext, this);
     }
 
     @Deactivate
@@ -114,6 +139,23 @@ public class TenantProviderImpl implements TenantProvider {
             this.adapterFactory.dispose();
             this.adapterFactory = null;
         }
+
+        if (this.plugin != null) {
+            this.plugin.dispose();
+            this.plugin = null;
+        }
+    }
+
+    private synchronized void bindTenantSetup(TenantCustomizer action, Map<String, Object> config) {
+        registeredTenantHandlers.put(ServiceUtil.getComparableForServiceRanking(config), action);
+    }
+
+    private synchronized void unbindTenantSetup(TenantCustomizer action, Map<String, Object> config) {
+        registeredTenantHandlers.remove(ServiceUtil.getComparableForServiceRanking(config));
+    }
+
+    private synchronized Collection<TenantCustomizer> getTenantHandlers() {
+        return registeredTenantHandlers.values();
     }
 
     public Tenant getTenant(String tenantId) {
@@ -145,13 +187,15 @@ public class TenantProviderImpl implements TenantProvider {
             try {
                 Resource tenantRootRes = adminResolver.getResource(tenantRootPath);
 
-                List<Tenant> tenantList = new ArrayList<Tenant>();
-                Iterator<Resource> tenantResourceList = tenantRootRes.listChildren();
-                while (tenantResourceList.hasNext()) {
-                    Resource tenantRes = tenantResourceList.next();
-                    tenantList.add(new TenantImpl(tenantRes));
+                if (tenantRootRes != null) {
+                    List<Tenant> tenantList = new ArrayList<Tenant>();
+                    Iterator<Resource> tenantResourceList = tenantRootRes.listChildren();
+                    while (tenantResourceList.hasNext()) {
+                        Resource tenantRes = tenantResourceList.next();
+                        tenantList.add(new TenantImpl(tenantRes));
+                    }
+                    return tenantList.iterator();
                 }
-                return tenantList.iterator();
             } finally {
                 adminResolver.close();
             }
@@ -161,11 +205,27 @@ public class TenantProviderImpl implements TenantProvider {
         return Collections.<Tenant> emptyList().iterator();
     }
 
-    public Tenant addTenant(String name, String tenantId) throws PersistenceException {
+    /**
+     * Creates a new tenant (not exposed as part of the api)
+     *
+     * @param name
+     * @param tenantId
+     * @param description
+     * @return
+     * @throws PersistenceException
+     */
+    public Tenant addTenant(String name, String tenantId, String description) throws PersistenceException {
         final ResourceResolver adminResolver = getAdminResolver();
         if (adminResolver != null) {
             try {
                 Resource tenantRootRes = adminResolver.getResource(tenantRootPath);
+                Session adminSession = adminResolver.adaptTo(Session.class);
+
+                if (tenantRootRes == null) {
+                    // create the root path
+                    JcrUtils.getOrCreateByPath(tenantRootPath, null, adminSession);
+                    tenantRootRes = adminResolver.getResource(tenantRootPath);
+                }
 
                 // check if tenantId already exists
                 Resource child = tenantRootRes.getChild(tenantId);
@@ -177,8 +237,27 @@ public class TenantProviderImpl implements TenantProvider {
                     Node rootNode = tenantRootRes.adaptTo(Node.class);
                     Node tenantNode = rootNode.addNode(tenantId);
                     tenantNode.setProperty(Tenant.PROP_NAME, name);
-                    adminResolver.adaptTo(Session.class).save();
-                    return new TenantImpl(adminResolver.getResource(tenantNode.getPath()));
+                    tenantNode.setProperty(Tenant.PROP_DESCRIPTION, description);
+
+                    Resource resource = adminResolver.getResource(tenantNode.getPath());
+                    Tenant tenant = new TenantImpl(resource);
+                    PersistableValueMap tenantProps = resource.adaptTo(PersistableValueMap.class);
+                    // call tenant setup handler
+                    for (TenantCustomizer ts : getTenantHandlers()) {
+                        Map<String, Object> props = ts.setup(tenant, adminResolver);
+                        if (props != null) {
+                            tenantProps.putAll(props);
+                        }
+                    }
+                    // save the properties
+                    tenantProps.save();
+
+                    // save the session
+                    adminSession.save();
+                    // refersh tenant instance, as it copies property from
+                    // resource
+                    tenant = new TenantImpl(resource);
+                    return tenant;
                 }
             } catch (RepositoryException e) {
                 throw new PersistenceException("Unexpected RepositoryException while adding tenant", e);
@@ -190,6 +269,51 @@ public class TenantProviderImpl implements TenantProvider {
         throw new PersistenceException("Cannot create the tenant");
     }
 
+    /**
+     * Removes the tenant (not exposed as part of the api)
+     *
+     * @param tenantId tenant identifier
+     * @return
+     * @throws PersistenceException
+     */
+    public void removeTenant(String tenantId) throws PersistenceException {
+        final ResourceResolver adminResolver = getAdminResolver();
+        if (adminResolver != null) {
+            try {
+                Resource tenantRootRes = adminResolver.getResource(tenantRootPath);
+
+                if (tenantRootRes == null) {
+                    // if tenant home is null just return
+                    return;
+                }
+
+                // check if tenantId already exists
+                Resource tenantRes = tenantRootRes.getChild(tenantId);
+
+                if (tenantRes != null) {
+                    Node tenantNode = tenantRes.adaptTo(Node.class);
+                    Tenant tenant = new TenantImpl(tenantRes);
+                    // call tenant setup handler
+                    for (TenantCustomizer ts : getTenantHandlers()) {
+                        ts.remove(tenant, adminResolver);
+                    }
+
+                    tenantNode.remove();
+                    adminResolver.adaptTo(Session.class).save();
+                    return;
+                }
+                // if there was no tenant found, just return
+                return;
+            } catch (RepositoryException e) {
+                throw new PersistenceException("Unexpected RepositoryException while removing tenant", e);
+            } finally {
+                adminResolver.close();
+            }
+        }
+
+        throw new PersistenceException("Cannot remove the tenant");
+    }
+
     public Iterator<Tenant> getTenants(String tenantFilter) {
         if (StringUtils.isBlank(tenantFilter)) {
             return null;
diff --git a/src/main/java/org/apache/sling/tenant/internal/console/WebConsolePlugin.java b/src/main/java/org/apache/sling/tenant/internal/console/WebConsolePlugin.java
new file mode 100644
index 0000000..f3eb471
--- /dev/null
+++ b/src/main/java/org/apache/sling/tenant/internal/console/WebConsolePlugin.java
@@ -0,0 +1,197 @@
+/*
+ * 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.tenant.internal.console;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.tenant.Tenant;
+import org.apache.sling.tenant.internal.TenantProviderImpl;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * This is a webconsole plugin displaying the active queues, some statistics and
+ * the configurations.
+ */
+public class WebConsolePlugin extends HttpServlet {
+
+    private static final long serialVersionUID = -6983227434841706385L;
+
+    private static final String LABEL = "tenants";
+
+    private static final String TITLE = "Tenant Administration";
+
+    private static final String CATEGORY = "Sling";
+
+    /** tenant name parameter */
+    private static final String REQ_PRM_TENANT_NAME = "tenantName";
+
+    /** tenant id parameter */
+    private static final String REQ_PRM_TENANT_ID = "tenantId";
+
+    /** tenant description parameter */
+    private static final String REQ_PRM_TENANT_DESC = "tenantDesc";
+
+    private TenantProviderImpl tenantProvider;
+
+    private final ServiceRegistration<?> service;
+
+    /** Escape the output for html. */
+    private String escape(final String text) {
+        if (text == null) {
+            return "";
+        }
+        return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
+    }
+
+    public WebConsolePlugin(final BundleContext bundleContext, final TenantProviderImpl tenantProvider) {
+        this.tenantProvider = tenantProvider;
+
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Tenant Management Console");
+        props.put("felix.webconsole.label", LABEL);
+        props.put("felix.webconsole.title", TITLE);
+        props.put("felix.webconsole.category", CATEGORY);
+        // props.put("felix.webconsole.configprinter.modes", new String[]{"zip",
+        // "txt"});
+
+        this.service = bundleContext.registerService(Servlet.class.getCanonicalName(), this, props);
+    }
+
+    public void dispose() {
+        if (this.service != null) {
+            this.service.unregister();
+        }
+    }
+
+    @Override
+    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+        String msg = null;
+        final String cmd = req.getParameter("action");
+        if ("create".equals(cmd)) {
+            try {
+                Tenant t = this.createTenant(req);
+                msg = String.format("Created Tenant %s (%s)", t.getName(), t.getDescription());
+            } catch (PersistenceException pe) {
+                msg = "Cannot create tenant: " + pe.getMessage();
+            }
+        } else if ("remove".equals(cmd)) {
+            this.removeTenant(req);
+        } else {
+            msg = "Unknown command";
+        }
+
+        final String path = LABEL;
+        final String redirectTo;
+        if (msg == null) {
+            redirectTo = path;
+        } else {
+            redirectTo = path + "?message=" + msg;
+        }
+
+        resp.sendRedirect(redirectTo);
+    }
+
+    private void removeTenant(HttpServletRequest request) throws PersistenceException {
+        String tenantId = request.getParameter(REQ_PRM_TENANT_ID);
+        tenantProvider.removeTenant(tenantId);
+    }
+
+    private void printForm(final PrintWriter pw, final Tenant t, final String buttonLabel, final String cmd) {
+        pw.printf("<button class='ui-state-default ui-corner-all' onclick='javascript:cmdsubmit(\"%s\", \"%s\");'>"
+            + "%s</button>", cmd, (t != null ? t.getId() : ""), buttonLabel);
+    }
+
+    private Tenant createTenant(HttpServletRequest request) throws PersistenceException {
+        String tenantName = request.getParameter(REQ_PRM_TENANT_NAME);
+        String tenantId = request.getParameter(REQ_PRM_TENANT_ID);
+        String tenantDesc = request.getParameter(REQ_PRM_TENANT_DESC);
+
+        return tenantProvider.addTenant(tenantName, tenantId, tenantDesc);
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
+        final PrintWriter pw = res.getWriter();
+
+        pw.println("<form method='POST' name='cmd'>" + "<input type='hidden' name='action' value=''/>"
+            + "<input type='hidden' name='tenantId' value=''/>" + "</form>");
+        pw.println("<script type='text/javascript'>");
+        pw.println("function cmdsubmit(action, tenantId) {" + " document.forms['cmd'].action.value = action;"
+            + " document.forms['cmd'].tenantId.value = tenantId;" + " document.forms['cmd'].submit();" + "} "
+            + "function createsubmit() {" + " document.forms['editorForm'].submit();" + "} " + "</script>");
+        pw.printf("<p class='statline ui-state-highlight'>Apache Sling Tenant Support</p>");
+
+        pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>");
+        pw.println("<span style='float: left; margin-left: 1em'>Add New Tenant </span>");
+        pw.println("<button class='ui-state-default ui-corner-all' onclick='javascript:createsubmit();'> Create </button></div></td></tr>");
+        pw.println("</div>");
+        pw.println("<table id='editortable' class='nicetable'><tbody>");
+
+        pw.println("<tr width='100%'><td colspan='2'><form id='editorForm' method='POST'>");
+        pw.println("<input name='action' type='hidden' value='create' class='ui-state-default ui-corner-all'>");
+        pw.println("<table border='0' width='100%'><tbody>");
+        pw.println("<tr><td style='width: 30%;'>Identifier</td><td>");
+        pw.println("<div><input name='tenantId' type='text' value=''></div>");
+        pw.println("</td></tr>");
+        pw.println("<tr><td style='width: 30%;'>Name</td><td>");
+        pw.println("<div><input name='tenantName' type='text' value=''></div>");
+        pw.println("</td></tr>");
+        pw.println("<tr><td style='width: 30%;'>Description</td><td>");
+        pw.println("<div><input name='tenantDesc' type='text' value=''></div>");
+        pw.println("</td></tr>");
+        pw.println("</tbody></table></form>");
+        pw.println("</tbody></table>");
+
+        Iterator<Tenant> tenants = this.tenantProvider.getTenants();
+        int count = 0;
+        while (tenants.hasNext()) {
+            count++;
+            Tenant tenant = tenants.next();
+            if (count == 1) {
+                pw.printf("<p class='statline ui-state-highlight'>Registered Tenants</p>");
+            }
+            pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>");
+            pw.printf("<span style='float: left; margin-left: 1em'>Tenant : %s </span>", escape(tenant.getName()));
+            this.printForm(pw, tenant, "Remove", "remove");
+            pw.println("</div>");
+            pw.println("<table class='nicetable'><tbody>");
+
+            pw.printf("<tr><td style='width: 30%%;'>Identifier</td><td>%s</td></tr>", escape(tenant.getId()));
+            pw.printf("<tr><td style='width: 30%%;'>Name</td><td>%s</td></tr>", escape(tenant.getName()));
+            pw.printf("<tr><td style='width: 30%%;'>Description</td><td>%s</td></tr>", escape(tenant.getDescription()));
+            pw.println("</tbody></table>");
+        }
+        // no existing tenants
+        if (count == 0) {
+            pw.printf("<p class='statline ui-state-highlight'>There are not registered tenants</p>");
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/tenant/spi/TenantCustomizer.java b/src/main/java/org/apache/sling/tenant/spi/TenantCustomizer.java
new file mode 100644
index 0000000..f25edf6
--- /dev/null
+++ b/src/main/java/org/apache/sling/tenant/spi/TenantCustomizer.java
@@ -0,0 +1,71 @@
+/*
+ * 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.tenant.spi;
+
+import java.util.Map;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.tenant.Tenant;
+
+/**
+ * This is a service interface which services are called by the WebConsole
+ * plugin (or admin tool) to complete the Tenant setup.
+ */
+public interface TenantCustomizer {
+
+    /**
+     * Method called to create or update the given tenant. The method may return
+     * additional properties to be added to the Tenant's property list. The
+     * ResourceResolver allows for access to the persistence.
+     * <p>
+     * The {@code ResourceResolver.commit} method must not be called by this
+     * method.
+     * <p>
+     * This method is not expected to throw an exception. Any exception thrown
+     * is logged but otherwise ignored.
+     *
+     * @param tenant The {@link Tenant} to be configured by this call
+     * @param resolver The {@code ResourceResolver} providing access to the
+     *            persistence for further setup. Note, that this
+     *            {@code resolver} will have administrative privileges.
+     * @return Additional properties to be added to the tenant. These properties
+     *         may later be accessed through the {@linkplain Tenant tenant's}
+     *         property accessor methods. {@code null} or an empty map may be
+     *         returned to not add properties.
+     */
+    public Map<String, Object> setup(Tenant tenant, ResourceResolver resolver);
+
+    /**
+     * Called to remove the setup for the given Tenant. This reverts all changes
+     * done by the #setup method. The ResourceResolver allows for access to the
+     * persistence.
+     * <p>
+     * The {@code ResourceResolver.commit} method must not be called by this
+     * method.
+     * <p>
+     * This method is not expected to throw an exception. Any exception thrown
+     * is logged but otherwise ignored.
+     *
+     * @param tenant The {@link Tenant} about to be removed
+     * @param resolver The {@code ResourceResolver} providing access to the
+     *            persistence for further cleanup. Note, that this
+     *            {@code resolver} will have administrative privileges.
+     */
+    public void remove(Tenant tenant, ResourceResolver resolver);
+}
diff --git a/src/main/java/org/apache/sling/tenant/spi/package-info.java b/src/main/java/org/apache/sling/tenant/spi/package-info.java
new file mode 100644
index 0000000..fa458c7
--- /dev/null
+++ b/src/main/java/org/apache/sling/tenant/spi/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+@Version("1.0")
+package org.apache.sling.tenant.spi;
+
+import aQute.bnd.annotation.Version;
+

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.