/* | |
* 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; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.SortedMap; | |
import java.util.TreeMap; | |
import org.apache.sling.api.resource.LoginException; | |
import org.apache.sling.api.resource.ModifiableValueMap; | |
import org.apache.sling.api.resource.PersistenceException; | |
import org.apache.sling.api.resource.Resource; | |
import org.apache.sling.api.resource.ResourceResolver; | |
import org.apache.sling.api.resource.ResourceResolverFactory; | |
import org.apache.sling.api.resource.ResourceUtil; | |
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.TenantManager; | |
import org.apache.sling.tenant.TenantProvider; | |
import org.apache.sling.tenant.internal.console.WebConsolePlugin; | |
import org.apache.sling.tenant.spi.TenantCustomizer; | |
import org.apache.sling.tenant.spi.TenantManagerHook; | |
import org.osgi.framework.BundleContext; | |
import org.osgi.framework.Constants; | |
import org.osgi.framework.Filter; | |
import org.osgi.framework.FrameworkUtil; | |
import org.osgi.framework.InvalidSyntaxException; | |
import org.osgi.service.component.annotations.Activate; | |
import org.osgi.service.component.annotations.Component; | |
import org.osgi.service.component.annotations.Deactivate; | |
import org.osgi.service.component.annotations.Reference; | |
import org.osgi.service.component.annotations.ReferenceCardinality; | |
import org.osgi.service.component.annotations.ReferencePolicy; | |
import org.osgi.service.metatype.annotations.AttributeDefinition; | |
import org.osgi.service.metatype.annotations.Designate; | |
import org.osgi.service.metatype.annotations.ObjectClassDefinition; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* Resource based Tenant Provider implementation. | |
*/ | |
@Component( | |
service = {TenantProvider.class, TenantManager.class}, | |
property = { | |
Constants.SERVICE_VENDOR + "=The Apache Software Foundation", | |
Constants.SERVICE_DESCRIPTION + "=Apache Sling Tenant Provider" | |
}, | |
immediate = true, | |
reference = { | |
@Reference( | |
name = "tenantSetup", | |
service = TenantCustomizer.class, | |
cardinality = ReferenceCardinality.MULTIPLE, | |
policy = ReferencePolicy.DYNAMIC), | |
@Reference( | |
name = "hook", | |
service = TenantManagerHook.class, | |
cardinality = ReferenceCardinality.MULTIPLE, | |
policy = ReferencePolicy.DYNAMIC) | |
} | |
) | |
@Designate(ocd = TenantProviderImpl.Configuration.class) | |
public class TenantProviderImpl implements TenantProvider, TenantManager { | |
/** default log */ | |
private final Logger log = LoggerFactory.getLogger(getClass()); | |
/** | |
* Root path for tenant | |
*/ | |
private static final String RESOURCE_TENANT_ROOT = "/etc/tenants"; | |
@ObjectClassDefinition(name = "Apache Sling Tenant Provider", | |
description = "Service responsible for providing Tenants.") | |
public @interface Configuration { | |
@AttributeDefinition( | |
name = "Tenants Root Path", | |
description = "Defines tenants root path", | |
defaultValue = RESOURCE_TENANT_ROOT | |
) | |
String tenant_root(); | |
@AttributeDefinition( | |
name = "Tenants Path Matcher", | |
description = "Defines tenants path matcher i.e. /content/sample/([^/]+)/*, used while resolving path to tenant" | |
) | |
String[] tenant_path_matcher(); | |
} | |
private static final String[] DEFAULT_PATH_MATCHER = {}; | |
private SortedMap<Comparable<Object>, TenantCustomizer> registeredTenantHandlers = new TreeMap<Comparable<Object>, TenantCustomizer>( | |
Collections.reverseOrder()); | |
private SortedMap<Comparable<Object>, TenantManagerHook> registeredHooks = new TreeMap<Comparable<Object>, TenantManagerHook>( | |
Collections.reverseOrder()); | |
private String tenantRootPath = RESOURCE_TENANT_ROOT; | |
@Reference | |
private ResourceResolverFactory factory; | |
private TenantAdapterFactory adapterFactory; | |
private WebConsolePlugin plugin; | |
@Activate | |
protected void activate(final BundleContext bundleContext, final Configuration configuration) { | |
this.tenantRootPath = PropertiesUtil.toString(configuration.tenant_root(), RESOURCE_TENANT_ROOT); | |
this.adapterFactory = new TenantAdapterFactory(bundleContext, this, PropertiesUtil.toStringArray(configuration.tenant_path_matcher(), DEFAULT_PATH_MATCHER)); | |
this.plugin = new WebConsolePlugin(bundleContext, this); | |
} | |
@Deactivate | |
protected void deactivate() { | |
if (this.adapterFactory != null) { | |
this.adapterFactory.dispose(); | |
this.adapterFactory = null; | |
} | |
if (this.plugin != null) { | |
this.plugin.dispose(); | |
this.plugin = null; | |
} | |
} | |
@SuppressWarnings("unused") | |
private synchronized void bindTenantSetup(TenantCustomizer action, Map<String, Object> config) { | |
registeredTenantHandlers.put(ServiceUtil.getComparableForServiceRanking(config), action); | |
} | |
@SuppressWarnings("unused") | |
private synchronized void unbindTenantSetup(TenantCustomizer action, Map<String, Object> config) { | |
registeredTenantHandlers.remove(ServiceUtil.getComparableForServiceRanking(config)); | |
} | |
private synchronized Collection<TenantCustomizer> getTenantHandlers() { | |
return registeredTenantHandlers.values(); | |
} | |
@SuppressWarnings("unused") | |
private synchronized void bindHook(TenantManagerHook action, Map<String, Object> config) { | |
registeredHooks.put(ServiceUtil.getComparableForServiceRanking(config), action); | |
} | |
@SuppressWarnings("unused") | |
private synchronized void unbindHook(TenantManagerHook action, Map<String, Object> config) { | |
registeredHooks.remove(ServiceUtil.getComparableForServiceRanking(config)); | |
} | |
private synchronized Collection<TenantManagerHook> getHooks() { | |
return registeredHooks.values(); | |
} | |
@Override | |
public Tenant getTenant(final String tenantId) { | |
if (tenantId != null && tenantId.length() > 0) { | |
return call(new ResourceResolverTask<Tenant>() { | |
@Override | |
public Tenant call(ResourceResolver resolver) { | |
Resource tenantRes = getTenantResource(resolver, tenantId); | |
return (tenantRes != null) ? new TenantImpl(tenantRes) : null; | |
} | |
}); | |
} | |
// in case of some problem | |
return null; | |
} | |
@Override | |
public Iterator<Tenant> getTenants() { | |
return getTenants(null); | |
} | |
@Override | |
public Iterator<Tenant> getTenants(final String tenantFilter) { | |
final Filter filter; | |
if (tenantFilter != null && tenantFilter.length() > 0) { | |
try { | |
filter = FrameworkUtil.createFilter(tenantFilter); | |
} catch (InvalidSyntaxException e) { | |
throw new IllegalArgumentException(e.getMessage(), e); | |
} | |
} else { | |
filter = null; | |
} | |
Iterator<Tenant> result = call(new ResourceResolverTask<Iterator<Tenant>>() { | |
@SuppressWarnings("unchecked") | |
@Override | |
public Iterator<Tenant> call(ResourceResolver resolver) { | |
Resource tenantRootRes = resolver.getResource(tenantRootPath); | |
if ( tenantRootRes != null ) { | |
List<Tenant> tenantList = new ArrayList<Tenant>(); | |
Iterator<Resource> tenantResourceList = tenantRootRes.listChildren(); | |
while (tenantResourceList.hasNext()) { | |
Resource tenantRes = tenantResourceList.next(); | |
if (filter == null || filter.matches(ResourceUtil.getValueMap(tenantRes))) { | |
TenantImpl tenant = new TenantImpl(tenantRes); | |
tenantList.add(tenant); | |
} | |
} | |
return tenantList.iterator(); | |
} | |
return Collections.EMPTY_LIST.iterator(); | |
} | |
}); | |
if (result == null) { | |
// no filter or no resource resolver for calling | |
result = Collections.<Tenant> emptyList().iterator(); | |
} | |
return result; | |
} | |
@Override | |
public Tenant create(final String tenantId, final Map<String, Object> properties) { | |
return call(new ResourceResolverTask<Tenant>() { | |
@Override | |
public Tenant call(ResourceResolver adminResolver) { | |
try { | |
// create the tenant | |
Resource tenantRes = createTenantResource(adminResolver, tenantId, properties); | |
TenantImpl tenant = new TenantImpl(tenantRes); | |
customizeTenant(tenantRes, tenant, true); | |
adminResolver.commit(); | |
// refresh tenant instance, as it copies property from | |
// resource | |
tenant.loadProperties(tenantRes); | |
return tenant; | |
} catch (PersistenceException e) { | |
log.error("create: Failed creating Tenant {}", tenantId, e); | |
} finally { | |
adminResolver.close(); | |
} | |
// no new tenant in case of problems | |
return null; | |
} | |
}); | |
} | |
@Override | |
public void remove(final Tenant tenant) { | |
call(new ResourceResolverTask<Void>() { | |
@Override | |
public Void call(ResourceResolver resolver) { | |
try { | |
Resource tenantRes = getTenantResource(resolver, tenant.getId()); | |
if (tenantRes != null) { | |
// call tenant setup handler | |
for (TenantCustomizer ts : getTenantHandlers()) { | |
try { | |
ts.remove(tenant, resolver); | |
} catch (Exception e) { | |
log.info("removeTenant: Unexpected problem calling TenantCustomizer " + ts, e); | |
} | |
} | |
// call tenant hooks | |
for (TenantManagerHook ts : getHooks()) { | |
try { | |
ts.remove(tenant); | |
} catch (Exception e) { | |
log.info("removeTenant: Unexpected problem calling TenantManagerHook " + ts, e); | |
} | |
} | |
resolver.delete(tenantRes); | |
resolver.commit(); | |
} | |
} catch (PersistenceException e) { | |
log.error("remove({}): Cannot persist Tenant removal", tenant.getId(), e); | |
} | |
return null; | |
} | |
}); | |
} | |
@Override | |
public void setProperty(final Tenant tenant, final String name, final Object value) { | |
updateProperties(tenant, new PropertiesUpdater() { | |
@Override | |
public void update(ModifiableValueMap properties) { | |
if (value != null) { | |
properties.put(name, value); | |
} else { | |
properties.remove(name); | |
} | |
} | |
}); | |
} | |
@Override | |
public void setProperties(final Tenant tenant, final Map<String, Object> properties) { | |
updateProperties(tenant, new PropertiesUpdater() { | |
@Override | |
public void update(ModifiableValueMap vm) { | |
for (Entry<String, Object> entry : properties.entrySet()) { | |
if (entry.getValue() != null) { | |
vm.put(entry.getKey(), entry.getValue()); | |
} else { | |
vm.remove(entry.getKey()); | |
} | |
} | |
} | |
}); | |
} | |
@Override | |
public void removeProperties(final Tenant tenant, final String... propertyNames) { | |
updateProperties(tenant, new PropertiesUpdater() { | |
@Override | |
public void update(ModifiableValueMap properties) { | |
for (String name : propertyNames) { | |
properties.remove(name); | |
} | |
} | |
}); | |
} | |
@SuppressWarnings("serial") | |
private Resource createTenantResource(final ResourceResolver resolver, final String tenantId, | |
final Map<String, Object> properties) throws PersistenceException { | |
// check for duplicate first | |
if (getTenantResource(resolver, tenantId) != null) { | |
throw new PersistenceException("Tenant '" + tenantId + "' already exists"); | |
} | |
Resource tenantRoot = resolver.getResource(tenantRootPath); | |
if (tenantRoot == null) { | |
Resource current = resolver.getResource("/"); | |
if (current == null) { | |
throw new PersistenceException("Cannot get root Resource"); | |
} | |
String[] segments = this.tenantRootPath.split("/"); | |
for (String segment : segments) { | |
Resource child = current.getChild(segment); | |
if (child == null) { | |
child = resolver.create(current, segment, new HashMap<String, Object>() { | |
{ | |
put("jcr:primaryType", "sling:Folder"); | |
} | |
}); | |
} | |
current = child; | |
} | |
tenantRoot = current; | |
} | |
return resolver.create(tenantRoot, tenantId, properties); | |
} | |
private Resource getTenantResource(final ResourceResolver resolver, final String tenantId) { | |
return resolver.getResource(tenantRootPath + "/" + tenantId); | |
} | |
private void customizeTenant(final Resource tenantRes, final Tenant tenant, boolean isCreate) { | |
// call tenant setup handler | |
Map<String, Object> tenantProps = tenantRes.adaptTo(ModifiableValueMap.class); | |
if (tenantProps == null) { | |
log.warn( | |
"create({}): Cannot get ModifiableValueMap for new tenant; will not store changed properties of TenantCustomizers", | |
tenant.getId()); | |
tenantProps = new HashMap<String, Object>(); | |
} | |
for (TenantCustomizer ts : getTenantHandlers()) { | |
try { | |
Map<String, Object> props = ts.setup(tenant, tenantRes.getResourceResolver()); | |
if (props != null) { | |
tenantProps.putAll(props); | |
} | |
} catch (Exception e) { | |
log.info("addTenant/updateTenant: Unexpected problem calling TenantCustomizer " + ts, e); | |
} | |
} | |
// call tenant hooks | |
for (TenantManagerHook ts : getHooks()) { | |
try { | |
Map<String, Object> props = (isCreate ? ts.setup(tenant) : ts.change(tenant)); | |
if (props != null) { | |
tenantProps.putAll(props); | |
} | |
} catch (Exception e) { | |
log.info("addTenant/updateTenant: Unexpected problem calling TenantManagerHook " + ts, e); | |
} | |
} | |
} | |
private <T> T call(ResourceResolverTask<T> task) { | |
ResourceResolver resolver = null; | |
T result = null; | |
try { | |
resolver = factory.getServiceResourceResolver(null); | |
result = task.call(resolver); | |
} catch (LoginException le) { | |
// unexpected, thus ignore | |
} finally { | |
if (resolver != null) { | |
resolver.close(); | |
} | |
} | |
return result; | |
} | |
private void updateProperties(final Tenant tenant, final PropertiesUpdater updater) { | |
call(new ResourceResolverTask<Void>() { | |
@Override | |
public Void call(ResourceResolver resolver) { | |
try { | |
Resource tenantRes = getTenantResource(resolver, tenant.getId()); | |
if (tenantRes != null) { | |
updater.update(tenantRes.adaptTo(ModifiableValueMap.class)); | |
//refresh so that customizer gets a refreshed tenant instance | |
if (tenant instanceof TenantImpl) { | |
((TenantImpl) tenant).loadProperties(tenantRes); | |
} | |
customizeTenant(tenantRes, tenant, false); | |
resolver.commit(); | |
if (tenant instanceof TenantImpl) { | |
((TenantImpl) tenant).loadProperties(tenantRes); | |
} | |
} | |
} catch (PersistenceException pe) { | |
log.error("setProperty({}): Cannot persist Tenant removal", tenant.getId(), pe); | |
} | |
return null; | |
} | |
}); | |
} | |
private static interface ResourceResolverTask<T> { | |
T call(ResourceResolver resolver); | |
} | |
private static interface PropertiesUpdater { | |
void update(ModifiableValueMap properties); | |
} | |
} |