/* * 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.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.lang.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.models.impl.model.ModelClass; import org.apache.sling.models.spi.ImplementationPicker; import org.apache.sling.models.spi.injectorspecific.StaticInjectAnnotationProcessorFactory; import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Collects alternative adapter implementations that may be defined in a @Model.adapters attribute. * If multiple models implement the same adapter they are all collected and can be chose via a ImplementationPicker. * Additionally it acts as a cache for model classes without adapter definitions, where adapter and implementation type is the same. * The implementation is thread-safe. */ final class AdapterImplementations { private static final Logger log = LoggerFactory.getLogger(AdapterImplementations.class); private final ConcurrentMap<String,ConcurrentNavigableMap<String,ModelClass<?>>> adapterImplementations = new ConcurrentHashMap<String,ConcurrentNavigableMap<String,ModelClass<?>>>(); private final ConcurrentMap<String,ModelClass<?>> modelClasses = new ConcurrentHashMap<String,ModelClass<?>>(); private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForResources = new ConcurrentHashMap<String, Class<?>>(); private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForRequests = new ConcurrentHashMap<String, Class<?>>(); private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForResources = new ConcurrentHashMap<Bundle, List<String>>(); private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForRequests = new ConcurrentHashMap<Bundle, List<String>>(); private volatile ImplementationPicker[] sortedImplementationPickers = new ImplementationPicker[0]; private volatile StaticInjectAnnotationProcessorFactory[] sortedStaticInjectAnnotationProcessorFactories = new StaticInjectAnnotationProcessorFactory[0]; public void setImplementationPickers(Collection<ImplementationPicker> implementationPickers) { this.sortedImplementationPickers = implementationPickers.toArray(new ImplementationPicker[implementationPickers.size()]); } public ImplementationPicker[] getImplementationPickers() { return this.sortedImplementationPickers; } public StaticInjectAnnotationProcessorFactory[] getStaticInjectAnnotationProcessorFactories() { return sortedStaticInjectAnnotationProcessorFactories; } public void setStaticInjectAnnotationProcessorFactories( Collection<StaticInjectAnnotationProcessorFactory> factories) { this.sortedStaticInjectAnnotationProcessorFactories = factories.toArray(new StaticInjectAnnotationProcessorFactory[factories.size()]); updateProcessorFactoriesInModelClasses(); } /** * Updates all {@link ModelClass} instances with updates list of static inject annotation processor factories. */ private void updateProcessorFactoriesInModelClasses() { Iterator<ModelClass<?>> items = modelClasses.values().iterator(); updateProcessorFactoriesInModelClasses(items); Iterator<ConcurrentNavigableMap<String,ModelClass<?>>> mapItems = adapterImplementations.values().iterator(); while (mapItems.hasNext()) { ConcurrentNavigableMap<String,ModelClass<?>> mapItem = mapItems.next(); updateProcessorFactoriesInModelClasses(mapItem.values().iterator()); } } private void updateProcessorFactoriesInModelClasses(Iterator<ModelClass<?>> items) { while (items.hasNext()) { ModelClass<?> item = items.next(); item.updateProcessorFactories(sortedStaticInjectAnnotationProcessorFactories); } } /** Add implementation mapping for the given model class (implementation is the model class itself). * Only used for testing purposes. Use {@link #addAll(Class, Class...)} in case you want to register a different implementation. * @param modelClasses the model classes to register */ protected void addClassesAsAdapterAndImplementation(Class<?>... modelClasses) { for (Class<?> modelClass : modelClasses) { addAll(modelClass, modelClass); } } /** * Add implementation mapping for the given adapter types. * @param implType Implementation type * @param adapterTypes Adapter types * @result true if adapters were successfully added */ @SuppressWarnings("unchecked") boolean addAll(Class<?> implType, Class<?>... adapterTypes) { ModelClass<?> modelClass = null; try { modelClass = new ModelClass(implType, sortedStaticInjectAnnotationProcessorFactories); } catch (Exception e) { log.warn("Unable to reflect on " + implType.getName(), e); return false; } catch (NoClassDefFoundError e) { log.warn("Unable to reflect on " + implType.getName(), e); return false; } for (Class<?> adapterType : adapterTypes) { String key = adapterType.getName(); if (adapterType == implType) { modelClasses.put(key, modelClass); } else { // although we already use a ConcurrentMap synchronize explicitly because we apply non-atomic operations on it synchronized (adapterImplementations) { ConcurrentNavigableMap<String, ModelClass<?>> implementations = adapterImplementations.get(key); if (implementations == null) { // to have a consistent ordering independent of bundle loading use a ConcurrentSkipListMap that sorts by class name implementations = new ConcurrentSkipListMap<String, ModelClass<?>>(); adapterImplementations.put(key, implementations); } implementations.put(implType.getName(), modelClass); } } } return true; } /** * Remove implementation mapping for the given adapter type. * @param adapterTypeName Adapter type name * @param implTypeName Implementation type name */ public void remove(String adapterTypeName, String implTypeName) { String key = adapterTypeName; if (StringUtils.equals(adapterTypeName, implTypeName)) { modelClasses.remove(key); } else { // although we already use a ConcurrentMap synchronize explicitly because we apply non-atomic operations on it synchronized (adapterImplementations) { ConcurrentNavigableMap<String,ModelClass<?>> implementations = adapterImplementations.get(key); if (implementations != null) { implementations.remove(implTypeName); if (implementations.isEmpty()) { adapterImplementations.remove(key); } } } } } /** * Remove all implementation mappings. */ public void removeAll() { modelClasses.clear(); adapterImplementations.clear(); } /** * Lookup the best-matching implementation for the given adapter type by enquiring the {@link ImplementationPicker} services. * @param adapterType Adapter type * @param adaptable Adaptable for reference * @return Implementation type or null if none detected */ @SuppressWarnings("unchecked") public <ModelType> ModelClass<ModelType> lookup(Class<ModelType> adapterType, Object adaptable) { String key = adapterType.getName(); // lookup in cache for models without adapter classes ModelClass<ModelType> modelClass = (ModelClass<ModelType>)modelClasses.get(key); if (modelClass!=null) { return modelClass; } // not found? look in cache with adapter classes ConcurrentNavigableMap<String,ModelClass<?>> implementations = adapterImplementations.get(key); if (implementations==null || implementations.isEmpty()) { return null; } Collection<ModelClass<?>> implementationsCollection = implementations.values(); ModelClass<?>[] implementationWrappersArray = implementationsCollection.toArray(new ModelClass<?>[implementationsCollection.size()]); // prepare array for implementation picker Class<?>[] implementationsArray = new Class<?>[implementationsCollection.size()]; for (int i=0; i<implementationWrappersArray.length; i++) { implementationsArray[i] = implementationWrappersArray[i].getType(); } for (ImplementationPicker picker : this.sortedImplementationPickers) { Class<?> implementation = picker.pick(adapterType, implementationsArray, adaptable); if (implementation != null) { for (int i=0; i<implementationWrappersArray.length; i++) { if (implementation==implementationWrappersArray[i].getType()) { return (ModelClass<ModelType>)implementationWrappersArray[i]; } } } } return null; } /** * @param adapterType the type to check * @return {@code true} in case the given type is a model (may be with a different adapter class) */ @SuppressWarnings("unchecked") public <ModelType> boolean isModelClass(Class<ModelType> adapterType) { String key = adapterType.getName(); // lookup in cache for models without adapter classes ModelClass<ModelType> modelClass = (ModelClass<ModelType>)modelClasses.get(key); if (modelClass!=null) { return true; } // not found? look in cache with adapter classes ConcurrentNavigableMap<String,ModelClass<?>> implementations = adapterImplementations.get(key); if (implementations==null || implementations.isEmpty()) { return false; } return true; } public void registerModelToResourceType(final Bundle bundle, final String resourceType, final Class<?> adaptableType, final Class<?> clazz) { if (resourceType.startsWith("/")) { log.warn("Registering model class {} for adaptable {} with absolute resourceType {}." , new Object[] { clazz, adaptableType, resourceType }); } ConcurrentMap<String, Class<?>> map; ConcurrentMap<Bundle, List<String>> resourceTypeRemovalLists; if (adaptableType == Resource.class) { map = resourceTypeMappingsForResources; resourceTypeRemovalLists = resourceTypeRemovalListsForResources; } else if (adaptableType == SlingHttpServletRequest.class) { map = resourceTypeMappingsForRequests; resourceTypeRemovalLists = resourceTypeRemovalListsForRequests; } else { log.warn("Found model class {} with resource type {} for adaptable {}. Unsupported type for resourceType binding.", new Object[] { clazz, resourceType, adaptableType }); return; } Class<?> existingMapping = map.putIfAbsent(resourceType, clazz); if (existingMapping == null) { resourceTypeRemovalLists.putIfAbsent(bundle, new CopyOnWriteArrayList<String>()); resourceTypeRemovalLists.get(bundle).add(resourceType); } else { log.warn("Skipped registering {} for resourceType {} under adaptable {} because of existing mapping to {}", new Object[] { clazz, resourceType, adaptableType, existingMapping }); } } public void removeResourceTypeBindings(final Bundle bundle) { List<String> registeredResourceTypes = resourceTypeRemovalListsForResources.remove(bundle); if (registeredResourceTypes != null) { for (String resourceType : registeredResourceTypes) { resourceTypeMappingsForResources.remove(resourceType); } } registeredResourceTypes = resourceTypeRemovalListsForRequests.remove(bundle); if (registeredResourceTypes != null) { for (String resourceType : registeredResourceTypes) { resourceTypeMappingsForRequests.remove(resourceType); } } } public Class<?> getModelClassForRequest(final SlingHttpServletRequest request) { return getModelClassForResource(request.getResource(), resourceTypeMappingsForRequests); } public Class<?> getModelClassForResource(final Resource resource) { return getModelClassForResource(resource, resourceTypeMappingsForResources); } protected static Class<?> getModelClassForResource(final Resource resource, final Map<String, Class<?>> map) { if (resource == null) { return null; } ResourceResolver resolver = resource.getResourceResolver(); final String originalResourceType = resource.getResourceType(); Class<?> modelClass = getClassFromResourceTypeMap(originalResourceType, map, resolver); if (modelClass != null) { return modelClass; } else { String resourceType = resolver.getParentResourceType(resource); while (resourceType != null) { modelClass = getClassFromResourceTypeMap(resourceType, map, resolver); if (modelClass != null) { return modelClass; } else { resourceType = resolver.getParentResourceType(resourceType); } } Resource resourceTypeResource = resolver.getResource(originalResourceType); return getModelClassForResource(resourceTypeResource, map); } } private static Class<?> getClassFromResourceTypeMap(final String resourceType, final Map<String, Class<?>> map, final ResourceResolver resolver) { if (resourceType == null) { return null; } Class<?> modelClass = map.get(resourceType); if (modelClass == null) { for (String searchPath : resolver.getSearchPath()) { if (resourceType.startsWith("/")) { if (resourceType.startsWith(searchPath)) { modelClass = map.get(resourceType.substring(searchPath.length())); if (modelClass != null) { break; } } } else { modelClass = map.get(searchPath + resourceType); if (modelClass != null) { break; } } } } return modelClass; } Map<String, Class<?>> getResourceTypeMappingsForRequests() { return Collections.unmodifiableMap(resourceTypeMappingsForRequests); } Map<String, Class<?>> getResourceTypeMappingsForResources() { return Collections.unmodifiableMap(resourceTypeMappingsForResources); } }