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 <[email protected]> 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("&", "&").replace("<", "<").replace(">", ">"); + } + + 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 "[email protected]" <[email protected]>.
