| /* |
| * 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.lang3.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<>(); |
| |
| private final ConcurrentMap<String,ModelClass<?>> modelClasses |
| = new ConcurrentHashMap<>(); |
| |
| private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForResources = new ConcurrentHashMap<>(); |
| private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForRequests = new ConcurrentHashMap<>(); |
| private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForResources = new ConcurrentHashMap<>(); |
| private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForRequests = new ConcurrentHashMap<>(); |
| |
| 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<>(); |
| 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); |
| if (resourceTypeResource != null && !resourceTypeResource.getPath().equals(resource.getPath())) { |
| return getModelClassForResource(resourceTypeResource, map); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |