ConfigurationReferenceProvider.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2017 wcm.io
* %%
* 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.
* #L%
*/
package io.wcm.caconfig.extensions.references.impl;
import static com.day.cq.dam.api.DamConstants.ACTIVITY_TYPE_ASSET;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.management.ConfigurationManager;
import org.apache.sling.caconfig.management.ConfigurationResourceResolverConfig;
import org.apache.sling.caconfig.management.multiplexer.ConfigurationResourceResolvingStrategyMultiplexer;
import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.api.reference.ReferenceProvider;
/**
* This implementation of {@link ReferenceProvider} allows to resolve references of a given {@link Resource} to
* context-aware configurations.
*
* <p>
* This is for example used by ActivationReferenceSearchServlet to resolve referenced content of pages during activation
* of a page using AEM sites. Returning the configurations and (if enabled) asset references allows the editor to activate
* them along with the page referring to them.
* </p>
*
* <p>
* This component can be disabled by configuration, but its enabled by default.
* </p>
*/
@Component(service = ReferenceProvider.class)
@Designate(ocd = ConfigurationReferenceProvider.Config.class)
public class ConfigurationReferenceProvider implements ReferenceProvider {
@ObjectClassDefinition(name = "wcm.io Context-Aware Configuration Reference Provider",
description = "Allows to resolve references from resources to their Context-Aware configuration pages "
+ "and referenced assets, for example during page activation.")
@interface Config {
@AttributeDefinition(name = "Enabled",
description = "Enable this reference provider.")
boolean enabled() default true;
@AttributeDefinition(name = "Asset References",
description = "Check for asset references within the context-aware configurations, and add them to the list of references.")
boolean assetReferences() default false;
}
static final String REFERENCE_TYPE = "caconfig";
@Reference
private ConfigurationManager configurationManager;
@Reference
private ConfigurationResourceResolvingStrategyMultiplexer configurationResourceResolvingStrategy;
@Reference
private ConfigurationResourceResolverConfig configurationResourceResolverConfig;
private boolean enabled;
private boolean assetReferencesEnabled;
private static final Logger log = LoggerFactory.getLogger(ConfigurationReferenceProvider.class);
@Reference
private PageManagerFactory pageManagerFactory;
@Activate
protected void activate(Config config) {
enabled = config.enabled();
assetReferencesEnabled = config.assetReferences();
}
@Deactivate
protected void deactivate() {
enabled = false;
}
@Override
public List<com.day.cq.wcm.api.reference.Reference> findReferences(Resource resource) {
if (!enabled) {
return Collections.emptyList();
}
PageManager pageManager = pageManagerFactory.getPageManager(resource.getResourceResolver());
if (pageManager == null) {
throw new RuntimeException("No page manager.");
}
Page contextPage = pageManager.getContainingPage(resource);
if (contextPage == null) {
return Collections.emptyList();
}
Map<String, ConfigurationMetadata> configurationMetadatas = new TreeMap<>(configurationManager.getConfigurationNames().stream()
.collect(Collectors.toMap(configName -> configName, configName -> configurationManager.getConfigurationMetadata(configName))));
List<com.day.cq.wcm.api.reference.Reference> references = new ArrayList<>();
Map<String, Asset> referencedAssets = new TreeMap<>();
Set<String> configurationBuckets = new LinkedHashSet<>(configurationResourceResolverConfig.configBucketNames());
for (String configurationName : configurationMetadatas.keySet()) {
Iterator<Resource> configurationInheritanceChain = configurationResourceResolvingStrategy.getResourceInheritanceChain(resource, configurationBuckets,
configurationName);
// get all configuration pages from inheritance chain
Collection<Page> referencePages = getReferencePages(configurationInheritanceChain, pageManager);
// generate references for each page (but not if the context page itself is included as well)
referencePages.stream()
.filter(configPage -> !StringUtils.equals(contextPage.getPath(), configPage.getPath()))
.forEach(configPage -> {
references.add(toReference(resource, configPage, configurationMetadatas, configurationBuckets));
// collect asset references
if (assetReferencesEnabled && configPage.getContentResource() != null) {
AssetRefereneDetector detector = new AssetRefereneDetector(configPage);
detector.getReferencedAssets().stream().forEach(asset -> referencedAssets.put(asset.getPath(), asset));
}
});
}
if (!referencedAssets.isEmpty()) {
// collect asset references detected in configuration pages (de-duplicated by using a map)
referencedAssets.values().forEach(asset -> references.add(toReference(resource, asset)));
}
log.debug("Found {} references for resource {}", references.size(), resource.getPath());
return references;
}
private Collection<Page> getReferencePages(@Nullable Iterator<Resource> configurationInheritanceChain, @NotNull PageManager pageManager) {
Map<String, Page> referencePages = new LinkedHashMap<>();
while (configurationInheritanceChain != null && configurationInheritanceChain.hasNext()) {
Resource configurationResource = configurationInheritanceChain.next();
// get page for configuration resource - and all children (e.g. for config collections)
// collect in map to eliminate duplicate pages
Page configPage = pageManager.getContainingPage(configurationResource);
if (configPage != null) {
referencePages.put(configPage.getPath(), configPage);
Iterator<Page> deepChildren = configPage.listChildren(new PageFilter(false, true), true);
while (deepChildren.hasNext()) {
Page configChildPage = deepChildren.next();
referencePages.put(configChildPage.getPath(), configChildPage);
}
}
}
return referencePages.values();
}
private com.day.cq.wcm.api.reference.Reference toReference(Resource resource, Page configPage,
Map<String, ConfigurationMetadata> configurationMetadatas, Set<String> configurationBuckets) {
log.trace("Found configuration reference {} for resource {}", configPage.getPath(), resource.getPath());
return new com.day.cq.wcm.api.reference.Reference(REFERENCE_TYPE,
getReferenceName(configPage, configurationMetadatas, configurationBuckets),
configPage.adaptTo(Resource.class),
getLastModifiedOf(configPage));
}
private com.day.cq.wcm.api.reference.Reference toReference(Resource resource, Asset asset) {
log.trace("Found asset reference {} for resource {}", asset.getPath(), resource.getPath());
return new com.day.cq.wcm.api.reference.Reference(ACTIVITY_TYPE_ASSET,
asset.getName(), asset.adaptTo(Resource.class), asset.getLastModified());
}
/**
* Build reference display name from path with:
* - translating configuration names to labels
* - omitting configuration bucket names
* - insert additional spaces so long paths may wrap on multiple lines
*/
private static String getReferenceName(Page configPage,
Map<String, ConfigurationMetadata> configurationMetadatas, Set<String> configurationBuckets) {
List<String> pathParts = Arrays.asList(StringUtils.split(configPage.getPath(), "/"));
return pathParts.stream()
.filter(name -> !configurationBuckets.contains(name))
.map(name -> {
ConfigurationMetadata configMetadata = configurationMetadatas.get(name);
if (configMetadata != null && configMetadata.getLabel() != null) {
return configMetadata.getLabel();
}
else {
return name;
}
})
.collect(Collectors.joining(" / "));
}
private static long getLastModifiedOf(Page page) {
Calendar lastModified = page.getLastModified();
return lastModified != null ? lastModified.getTimeInMillis() : 0;
}
}