blob: c3b1757475afca16654b7a418a34d039845d1131 [file] [log] [blame]
/*
* 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.models.impl;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.servlet.Servlet;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.adapter.AdapterFactory;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.ExporterOption;
import org.apache.sling.models.annotations.Exporters;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.scripting.api.BindingsValuesProvidersByContext;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ModelPackageBundleListener implements BundleTrackerCustomizer<ServiceRegistration[]> {
static final String PACKAGE_HEADER = "Sling-Model-Packages";
static final String CLASSES_HEADER = "Sling-Model-Classes";
static final String PROP_EXPORTER_SERVLET_CLASS = "sling.models.exporter.servlet.class";
static final String PROP_EXPORTER_SERVLET_NAME = "sling.models.exporter.servlet.name";
/**
* Service registration property for the adapter condition.
*/
private static final String PROP_ADAPTER_CONDITION = "adapter.condition";
/**
* The model implementation class that initiated the service registration.
*/
private static final String PROP_IMPLEMENTATION_CLASS = "models.adapter.implementationClass";
/**
* Service registration property letting the Adapter Manager the adapter is OK to be in a private package
*/
public static final String PROP_ALLOWED_IN_PRIVATE = "adapter.allowed.in.private.package";
private static final Logger log = LoggerFactory.getLogger(ModelPackageBundleListener.class);
private final BundleContext bundleContext;
private final BundleTracker bundleTracker;
private final ModelAdapterFactory factory;
private final AdapterImplementations adapterImplementations;
private final BindingsValuesProvidersByContext bindingsValuesProvidersByContext;
private final SlingModelsScriptEngineFactory scriptEngineFactory;
public ModelPackageBundleListener(BundleContext bundleContext,
ModelAdapterFactory factory,
AdapterImplementations adapterImplementations,
BindingsValuesProvidersByContext bindingsValuesProvidersByContext,
SlingModelsScriptEngineFactory scriptEngineFactory) {
this.bundleContext = bundleContext;
this.factory = factory;
this.adapterImplementations = adapterImplementations;
this.bindingsValuesProvidersByContext = bindingsValuesProvidersByContext;
this.scriptEngineFactory = scriptEngineFactory;
this.bundleTracker = new BundleTracker<>(bundleContext, Bundle.ACTIVE, this);
this.bundleTracker.open();
}
@Override
public ServiceRegistration[] addingBundle(Bundle bundle, BundleEvent event) {
List<ServiceRegistration> regs = new ArrayList<>();
Dictionary<?, ?> headers = bundle.getHeaders();
String packageList = PropertiesUtil.toString(headers.get(PACKAGE_HEADER), null);
if (packageList != null) {
packageList = StringUtils.deleteWhitespace(packageList);
String[] packages = packageList.split(",");
for (String singlePackage : packages) {
@SuppressWarnings("unchecked")
Enumeration<URL> classUrls = bundle.findEntries("/" + singlePackage.replace('.', '/'), "*.class",
true);
if (classUrls == null) {
log.warn("No adaptable classes found in package {}, ignoring", singlePackage);
continue;
}
while (classUrls.hasMoreElements()) {
URL url = classUrls.nextElement();
String className = toClassName(url);
analyzeClass(bundle, className, regs);
}
}
}
String classesList = PropertiesUtil.toString(headers.get(CLASSES_HEADER), null);
if (classesList != null) {
classesList = StringUtils.deleteWhitespace(classesList);
String[] classes = classesList.split(",");
for (String className : classes) {
analyzeClass(bundle, className, regs);
}
}
return regs.toArray(new ServiceRegistration[0]);
}
private void analyzeClass(Bundle bundle, String className, List<ServiceRegistration> regs) {
try {
Class<?> implType = bundle.loadClass(className);
Model annotation = implType.getAnnotation(Model.class);
if (annotation != null) {
// get list of adapters from annotation - if not given use annotated class itself
Class<?>[] adapterTypes = annotation.adapters();
if (adapterTypes.length == 0) {
adapterTypes = new Class<?>[] { implType };
} else if (!ArrayUtils.contains(adapterTypes, implType)) {
adapterTypes = ArrayUtils.add(adapterTypes, implType);
}
// register adapter only if given adapters are valid
if (validateAdapterClasses(implType, adapterTypes)) {
if (adapterImplementations.addAll(implType, adapterTypes)) {
ServiceRegistration reg = registerAdapterFactory(adapterTypes, annotation.adaptables(), implType, annotation.condition());
regs.add(reg);
String[] resourceTypes = annotation.resourceType();
for (String resourceType : resourceTypes) {
if (StringUtils.isNotEmpty(resourceType)) {
for (Class<?> adaptable : annotation.adaptables()) {
adapterImplementations.registerModelToResourceType(bundle, resourceType, adaptable, implType);
ExportServlet.ExportedObjectAccessor accessor = null;
if (adaptable == Resource.class) {
accessor = new ExportServlet.ResourceAccessor(implType);
} else if (adaptable == SlingHttpServletRequest.class) {
accessor = new ExportServlet.RequestAccessor(implType);
}
Exporter exporterAnnotation = implType.getAnnotation(Exporter.class);
if (exporterAnnotation != null) {
registerExporter(bundle, implType, resourceType, exporterAnnotation, regs, accessor);
}
Exporters exportersAnnotation = implType.getAnnotation(Exporters.class);
if (exportersAnnotation != null) {
for (Exporter ann : exportersAnnotation.value()) {
registerExporter(bundle, implType, resourceType, ann, regs, accessor);
}
}
}
}
}
}
}
}
} catch (ClassNotFoundException e) {
log.warn("Unable to load class", e);
}
}
@Override
public void modifiedBundle(Bundle bundle, BundleEvent event, ServiceRegistration[] object) {
}
@Override
public void removedBundle(Bundle bundle, BundleEvent event, ServiceRegistration[] object) {
for (ServiceRegistration reg : object) {
ServiceReference ref = reg.getReference();
String[] adapterTypeNames = PropertiesUtil.toStringArray(ref.getProperty(AdapterFactory.ADAPTER_CLASSES));
if (adapterTypeNames != null) {
String implTypeName = PropertiesUtil.toString(ref.getProperty(PROP_IMPLEMENTATION_CLASS), null);
for (String adapterTypeName : adapterTypeNames) {
adapterImplementations.remove(adapterTypeName, implTypeName);
}
}
reg.unregister();
}
adapterImplementations.removeResourceTypeBindings(bundle);
}
public synchronized void unregisterAll() {
this.bundleTracker.close();
}
/** Convert class URL to class name */
private String toClassName(URL url) {
final String f = url.getFile();
final String cn = f.substring(1, f.length() - ".class".length());
return cn.replace('/', '.');
}
private String[] toStringArray(Class<?>[] classes) {
String[] arr = new String[classes.length];
for (int i = 0; i < classes.length; i++) {
arr[i] = classes[i].getName();
}
return arr;
}
/**
* Validate list of adapter classes. Make sure all given are either the annotated class itself,
* or an interface or superclass of it.
* A warning is written if this it not the case, and false is returned.
* @param clazz Annotated class
* @param adapterClasses Adapter classes
* @return true if validation was successful
*/
private boolean validateAdapterClasses(Class<?> clazz, Class<?>[] adapterClasses) {
for (Class<?> adapterClass : adapterClasses) {
if (!adapterClass.isAssignableFrom(clazz)) {
log.warn("Unable to register model class {} because adapter class {} is not valid.",
clazz.getName(), adapterClass.getName());
return false;
}
}
return true;
}
/**
* Registers an adapter factory for a annotated sling models class.
* @param adapterTypes Adapter (either the class itself, or interface or superclass of it)
* @param adaptableTypes Classes to adapt from
* @param implType Type of the implementation class
* @param condition Condition (optional)
* @return Service registration
*/
private ServiceRegistration registerAdapterFactory(Class<?>[] adapterTypes, Class<?>[] adaptableTypes, Class<?> implType, String condition) {
Dictionary<String, Object> registrationProps = new Hashtable<>();
registrationProps.put(AdapterFactory.ADAPTER_CLASSES, toStringArray(adapterTypes));
registrationProps.put(AdapterFactory.ADAPTABLE_CLASSES, toStringArray(adaptableTypes));
registrationProps.put(PROP_IMPLEMENTATION_CLASS, implType.getName());
registrationProps.put(PROP_ALLOWED_IN_PRIVATE, true);
if (StringUtils.isNotBlank(condition)) {
registrationProps.put(PROP_ADAPTER_CONDITION, condition);
}
return bundleContext.registerService(AdapterFactory.SERVICE_NAME, factory, registrationProps);
}
private void registerExporter(Bundle bundle, Class<?> annotatedClass, String resourceType, Exporter exporterAnnotation,
List<ServiceRegistration> regs, ExportServlet.ExportedObjectAccessor accessor) {
if (accessor != null) {
Map<String, String> baseOptions = getOptions(exporterAnnotation);
ExportServlet servlet = new ExportServlet(bundle.getBundleContext(), factory, bindingsValuesProvidersByContext,
scriptEngineFactory, annotatedClass, exporterAnnotation.selector(), exporterAnnotation.name(), accessor, baseOptions);
Dictionary<String, Object> registrationProps = new Hashtable<>();
registrationProps.put("sling.servlet.resourceTypes", resourceType);
registrationProps.put("sling.servlet.selectors", exporterAnnotation.selector());
registrationProps.put("sling.servlet.extensions", exporterAnnotation.extensions());
registrationProps.put(PROP_EXPORTER_SERVLET_CLASS, annotatedClass.getName());
registrationProps.put(PROP_EXPORTER_SERVLET_NAME, exporterAnnotation.name());
log.debug("registering servlet for {}, {}, {}", new Object[]{resourceType, exporterAnnotation.selector(), exporterAnnotation.extensions()});
ServiceRegistration reg = bundleContext.registerService(Servlet.class.getName(), servlet, registrationProps);
regs.add(reg);
}
}
private Map<String, String> getOptions(Exporter annotation) {
ExporterOption[] options = annotation.options();
if (options.length == 0) {
return Collections.emptyMap();
} else {
Map<String, String> map = new HashMap<>(options.length);
for (ExporterOption option : options) {
map.put(option.name(), option.value());
}
return map;
}
}
}