| /* |
| * 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; |
| } |
| } |
| |
| } |