/**
* Copyright 2013 the original author or authors.
*
* Licensed 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 io.neba.core.resourcemodels.registration;
import io.neba.core.blueprint.EventhandlingBarrier;
import io.neba.core.util.ConcurrentDistinctMultiValueMap;
import io.neba.core.util.Key;
import io.neba.core.util.MatchedBundlesPredicate;
import io.neba.core.util.OsgiBeanSource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.sling.api.resource.Resource;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static io.neba.core.resourcemodels.registration.MappableTypeHierarchy.mappableTypeHierarchyOf;
import static io.neba.core.util.BundleUtil.displayNameOf;
import static java.util.Collections.unmodifiableCollection;
/**
* Contains {@link OsgiBeanSource model sources} associated to
* {@link MappableTypeHierarchy mappable types} and the corresponding logic to
* lookup these relationships.
*
* @author Olaf Otto
*/
@Service
public class ModelRegistry {
private static final Object NULL_VALUE = new Object();
private static final long EVERY_30_SECONDS = 30 * 1000;
/**
* Generate a {@link Key} representing both the
* {@link org.apache.sling.api.resource.Resource#getResourceType() sling resource type},
* {@link javax.jcr.Node#getPrimaryNodeType() primary node type} and the {@link Node#getMixinNodeTypes() mixin types}
* of the resource, if any. Rationale: Resources may have the same <code>sling:resourceType</code>, but different primary or mixin types,
* thus potentially producing different results when mapped. The cache must thus use these
* types as a key for cached adaptation results.<br />
*
* @param resource must not be <code>null</code>.
* @param furtherKeyElements can be <code>null</code>
* @return never <code>null</code>.
*/
private static Key key(Resource resource, Object... furtherKeyElements) {
Key key;
final Key furtherElementsKey = furtherKeyElements == null ? null : new Key(furtherKeyElements);
Node node = resource.adaptTo(Node.class);
if (node != null) {
try {
key = new Key(
resource.getResourceType(),
resource.getResourceSuperType(),
node.getPrimaryNodeType().getName(),
Arrays.toString(node.getMixinNodeTypes()),
furtherElementsKey);
} catch (RepositoryException e) {
throw new RuntimeException("Unable to retrieve the primary type of " + resource + ".", e);
}
} else {
key = new Key(resource.getResourceType(), furtherElementsKey);
}
return key;
}
/**
* @param source can be <code>null</code>.
* @param <T> the collection type.
* @return the collection, or <code>null</code> if the collection
* id <code>null</code> or empty.
*/
private static <T> Collection<T> nullIfEmpty(Collection<T> source) {
return source == null || source.isEmpty() ? null : source;
}
/**
* @param sources can be <code>null</code>.
* @param compatibleType can be <code>null</code>.
* @return the original collection if sources or compatibleType are
* <code>null</code>, or a collection representing the models
* compatible to the given type.
*/
private static Collection<OsgiBeanSource<?>> filter(Collection<OsgiBeanSource<?>> sources, Class<?> compatibleType) {
Collection<OsgiBeanSource<?>> compatibleSources = sources;
if (sources != null && compatibleType != null) {
compatibleSources = new ArrayList<>(sources.size());
for (OsgiBeanSource<?> source : sources) {
if (compatibleType.isAssignableFrom(source.getBeanType())) {
compatibleSources.add(source);
}
}
}
return compatibleSources;
}
/**
* @param sources can be <code>null</code>.
* @param beanName can be <code>null</code>.
* @return the original collection if sources or beanName are
* <code>null</code>, or a collection representing the models
* who's {@link io.neba.core.util.OsgiBeanSource#getBeanName()} bean name}
* is equal to the given bean name.
*/
private static Collection<OsgiBeanSource<?>> filter(Collection<OsgiBeanSource<?>> sources, String beanName) {
Collection<OsgiBeanSource<?>> sourcesWithBeanName = sources;
if (sources != null && beanName != null) {
sourcesWithBeanName = new ArrayList<>(sources.size());
for (OsgiBeanSource<?> source : sources) {
if (beanName.equals(source.getBeanName())) {
sourcesWithBeanName.add(source);
}
}
}
return sourcesWithBeanName;
}
private final ConcurrentDistinctMultiValueMap<String, OsgiBeanSource<?>>
typeNameToBeanSourcesMap = new ConcurrentDistinctMultiValueMap<>();
private final ConcurrentDistinctMultiValueMap<Key, LookupResult>
lookupCache = new ConcurrentDistinctMultiValueMap<>();
private final Map<Key, Object> unmappedTypesCache = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(getClass());
private final AtomicInteger state = new AtomicInteger(0);
/**
* Finds the most specific models for the given {@link Resource}. The model's bean
* name must match the provided bean name.
* @param resource must not be <code>null</code>.
* @param beanName must not be <code>null</code>.
* @return the resolved models, or <code>null</code> if no such models exist.
*/
public Collection<LookupResult> lookupMostSpecificModels(Resource resource, String beanName) {
if (resource == null) {
throw new IllegalArgumentException("Method argument resource must not be null.");
}
if (beanName == null) {
throw new IllegalArgumentException("Method argument beanName must not be null.");
}
if (isUnmapped(resource)) {
return null;
}
Key key = key(resource, beanName);
if (isUnmapped(key)) {
return null;
}
Collection<LookupResult> matchingModels = lookupFromCache(key);
if (matchingModels == null) {
final int currentStateId = this.state.get();
matchingModels = resolveMostSpecificBeanSources(resource, beanName);
if (matchingModels.isEmpty()) {
markAsUnmapped(key, currentStateId);
} else {
cache(key, matchingModels, currentStateId);
}
}
return nullIfEmpty(matchingModels);
}
/**
* Finds the most specific models for the given {@link Resource}, i.e. the
* first model(s) found when traversing the resource's
* {@link MappableTypeHierarchy mappable hierarchy}.
*
* @param resource must not be <code>null</code>.
* @return the model sources, or <code>null</code> if no models exist for the resource.
*/
public Collection<LookupResult> lookupMostSpecificModels(Resource resource) {
if (resource == null) {
throw new IllegalArgumentException("Method argument resource must not be null.");
}
final Key key = key(resource);
if (isUnmapped(key)) {
return null;
}
Collection<LookupResult> sources = lookupFromCache(key);
if (sources == null) {
final int currentStateId = this.state.get();
sources = resolveMostSpecificBeanSources(resource);
if (sources.isEmpty()) {
markAsUnmapped(key, currentStateId);
} else {
cache(key, sources, currentStateId);
}
}
return nullIfEmpty(sources);
}
/**
* Finds the all models for the given {@link Resource}, i.e. the
* all model(s) found when traversing the resource's
* {@link MappableTypeHierarchy mappable hierarchy}.
*
* @param resource must not be <code>null</code>.
* @return the model sources, or <code>null</code> if no models exist for the resource.
*/
public Collection<LookupResult> lookupAllModels(Resource resource) {
if (resource == null) {
throw new IllegalArgumentException("Method argument resource must not be null.");
}
if (isUnmapped(resource)) {
return null;
}
final Key key = key(resource, "allModels");
if (isUnmapped(key)) {
return null;
}
Collection<LookupResult> sources = lookupFromCache(key);
if (sources == null) {
final int currentStateId = this.state.get();
sources = resolveBeanSources(resource, null, false);
if (sources.isEmpty()) {
markAsUnmapped(key, currentStateId);
} else {
cache(key, sources, currentStateId);
}
}
return nullIfEmpty(sources);
}
/**
* Finds the most specific models for the given resource which are
* {@link Class#isAssignableFrom(Class) assignable to} the target type.
*
* @param resource must not be <code>null</code>.
* @param targetType must not be <code>null</code>.
* @return the sources of the models, or <code>null</code> if no such model exists.
*/
public Collection<LookupResult> lookupMostSpecificModels(Resource resource, Class<?> targetType) {
if (resource == null) {
throw new IllegalArgumentException("Method argument resource must not be null.");
}
if (targetType == null) {
throw new IllegalArgumentException("Method argument targetType must not be null.");
}
if (isUnmapped(resource)) {
return null;
}
final Key key = key(resource, targetType);
if (isUnmapped(key)) {
return null;
}
Collection<LookupResult> matchingModels = lookupFromCache(key);
if (matchingModels == null) {
final int currentStateId = this.state.get();
matchingModels = resolveMostSpecificBeanSources(resource, targetType);
if (matchingModels.isEmpty()) {
markAsUnmapped(key, currentStateId);
} else {
cache(key, matchingModels, currentStateId);
}
}
return nullIfEmpty(matchingModels);
}
/**
* Clears the registry upon shutdown.
*/
@PreDestroy
public void shutdown() {
this.logger.info("The model registry is shutting down.");
clearRegisteredModels();
clearLookupCaches();
}
/**
* Removes all resource models originating from the given bundle from this registry.
*
* @param bundle must not be <code>null</code>.
*/
public void removeResourceModels(final Bundle bundle) {
this.logger.info("Removing resource models of bundle " + displayNameOf(bundle) + "...");
MatchedBundlesPredicate sourcesWithBundles = new MatchedBundlesPredicate(bundle);
for (Collection<OsgiBeanSource<?>> values : this.typeNameToBeanSourcesMap.values()) {
CollectionUtils.filter(values, sourcesWithBundles);
}
clearLookupCaches();
this.logger.info("Removed " + sourcesWithBundles.getFilteredElements()
+ " resource models of bundle " + displayNameOf(bundle) + "...");
}
/**
* @return a shallow copy of all registered sources for models, never
* <code>null</code> but rather an empty list.
*/
public List<OsgiBeanSource<?>> getBeanSources() {
Collection<Collection<OsgiBeanSource<?>>> sources = this.typeNameToBeanSourcesMap.values();
List<OsgiBeanSource<?>> linearizedSources = new LinkedList<>();
sources.forEach(linearizedSources::addAll);
return linearizedSources;
}
/**
* Adds the type[] -&t; model relationship to the registry.
*
* @param types must not be <code>null</code>.
* @param source must not be <code>null</code>.
*/
public void add(String[] types, OsgiBeanSource<?> source) {
for (String resourceType : types) {
this.typeNameToBeanSourcesMap.put(resourceType, source);
}
clearLookupCaches();
}
/**
* @return all type -> model mappings.
*/
public Map<String, Collection<OsgiBeanSource<?>>> getTypeMappings() {
return this.typeNameToBeanSourcesMap.getContents();
}
/**
* Checks whether the {@link OsgiBeanSource sources} of all resource models
* are still {@link io.neba.core.util.OsgiBeanSource#isValid() valid}. If not,
* the corresponding model(s) are removed from the registry.
*/
@Scheduled(fixedRate = EVERY_30_SECONDS)
public void removeInvalidReferences() {
if (EventhandlingBarrier.tryBegin()) {
this.logger.debug("Checking for references to beans from inactive bundles...");
try {
for (Collection<OsgiBeanSource<?>> values : this.typeNameToBeanSourcesMap.values()) {
for (Iterator<OsgiBeanSource<?>> it = values.iterator(); it.hasNext(); ) {
final OsgiBeanSource<?> source = it.next();
if (!source.isValid()) {
this.logger.info("Reference to " + source + " is invalid, removing.");
it.remove();
clearLookupCaches();
}
}
}
} finally {
EventhandlingBarrier.end();
}
this.logger.debug("Completed checking for references to beans from inactive bundles.");
}
}
/**
* Clears all quick lookup caches for resource models, but
* not the registry itself.
*/
public synchronized void clearLookupCaches() {
this.state.incrementAndGet();
this.lookupCache.clear();
this.unmappedTypesCache.clear();
this.logger.debug("Cache cleared.");
}
private boolean isUnmapped(Resource resource) {
return this.unmappedTypesCache.containsKey(key(resource));
}
private boolean isUnmapped(Key key) {
return this.unmappedTypesCache.containsKey(key);
}
private Collection<LookupResult> lookupFromCache(Key key) {
return this.lookupCache.get(key);
}
private void clearRegisteredModels() {
this.typeNameToBeanSourcesMap.clear();
this.logger.debug("Registry cleared.");
}
/**
* @see #resolveMostSpecificBeanSources(org.apache.sling.api.resource.Resource, Class)
*/
private Collection<LookupResult> resolveMostSpecificBeanSources(Resource resource) {
return resolveMostSpecificBeanSources(resource, (Class<?>) null);
}
/**
* @see #resolveBeanSources(org.apache.sling.api.resource.Resource, Class, boolean)
*/
private Collection<LookupResult> resolveMostSpecificBeanSources(
Resource resource,
Class<?> compatibleType) {
return resolveBeanSources(resource, compatibleType, true);
}
/**
* Finds all {@link OsgiBeanSource bean sources} representing models for the given
* {@link Resource}.
*
* @param resource must not be <code>null</code>.
* @param compatibleType can be <code>null</code>. If provided, only models
* compatible to the given type are returned.
* @param resolveMostSpecific whether to resolve only the most specific models.
*
* @return never <code>null</code> but rather an empty collection.
*/
private Collection<LookupResult> resolveBeanSources(Resource resource, Class<?> compatibleType, boolean resolveMostSpecific) {
Collection<LookupResult> sources = new ArrayList<>(64);
for (final String resourceType : mappableTypeHierarchyOf(resource)) {
Collection<OsgiBeanSource<?>> allSourcesForType = this.typeNameToBeanSourcesMap.get(resourceType);
Collection<OsgiBeanSource<?>> sourcesForCompatibleType = filter(allSourcesForType, compatibleType);
if (sourcesForCompatibleType != null && !sourcesForCompatibleType.isEmpty()) {
sources.addAll(sourcesForCompatibleType.stream().map(source -> new LookupResult(source, resourceType)).collect(Collectors.toList()));
if (resolveMostSpecific) {
break;
}
}
}
return unmodifiableCollection(sources);
}
/**
* Finds all {@link OsgiBeanSource bean sources} representing models for the given
* {@link Resource} whos {@link io.neba.core.util.OsgiBeanSource#getBeanName() bean name}
* matches the given bean name.
*
* @param resource must not be <code>null</code>.
* @param beanName can be <code>null</code>.
* @return never <code>null</code> but rather an empty collection.
*/
private Collection<LookupResult> resolveMostSpecificBeanSources(Resource resource, String beanName) {
Collection<LookupResult> sources = new ArrayList<>();
for (final String resourceType : mappableTypeHierarchyOf(resource)) {
Collection<OsgiBeanSource<?>> allSourcesForType = this.typeNameToBeanSourcesMap.get(resourceType);
Collection<OsgiBeanSource<?>> sourcesWithMatchingBeanName = filter(allSourcesForType, beanName);
if (sourcesWithMatchingBeanName != null && !sourcesWithMatchingBeanName.isEmpty()) {
sources.addAll(sourcesWithMatchingBeanName.stream().map(source -> new LookupResult(source, resourceType)).collect(Collectors.toList()));
break;
}
}
return unmodifiableCollection(sources);
}
/**
* A mapping might apply to any type somewhere within a resource's
* {@link MappableTypeHierarchy}. This cache saves the registrar from searching
* this entire hierarchy each time the model is resolved by remembering a found
* resource type -> model relationship.
*/
private void cache(final Key key, final Collection<LookupResult> sources, final int stateId) {
synchronized (this) {
if (stateId == this.state.get()) {
this.lookupCache.put(key, sources);
}
}
}
private void markAsUnmapped(final Key key, final int stateId) {
synchronized (this) {
if (stateId == this.state.get()) {
this.unmappedTypesCache.put(key, NULL_VALUE);
}
}
}
}