blob: 8bac535a2711b671198b0ee7e5113d4eb736165e [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.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);
}
}