LinkHandlerImpl.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 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.handler.link.impl;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.adapter.Adaptable;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.wcm.api.Page;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.wcm.handler.link.Link;
import io.wcm.handler.link.LinkArgs;
import io.wcm.handler.link.LinkBuilder;
import io.wcm.handler.link.LinkHandler;
import io.wcm.handler.link.LinkRequest;
import io.wcm.handler.link.spi.LinkHandlerConfig;
import io.wcm.handler.link.spi.LinkMarkupBuilder;
import io.wcm.handler.link.spi.LinkProcessor;
import io.wcm.handler.link.spi.LinkType;
import io.wcm.handler.link.type.InvalidLinkType;
import io.wcm.sling.commons.adapter.AdaptTo;
import io.wcm.sling.models.annotations.AemObject;
import io.wcm.wcm.commons.component.ComponentPropertyResolverFactory;
/**
* Default implementation of a {@link LinkHandler}
*/
@Model(adaptables = {
SlingHttpServletRequest.class, Resource.class
}, adapters = LinkHandler.class)
public final class LinkHandlerImpl implements LinkHandler {
@Self
private Adaptable adaptable;
@Self
private LinkHandlerConfig linkHandlerConfig;
@AemObject(injectionStrategy = InjectionStrategy.OPTIONAL)
private Page currentPage;
@OSGiService
private ComponentPropertyResolverFactory componentPropertyResolverFactory;
private static final Logger log = LoggerFactory.getLogger(LinkHandlerImpl.class);
@Override
public @NotNull LinkBuilder get(@Nullable Resource resource) {
return new LinkBuilderImpl(resource, this, componentPropertyResolverFactory);
}
@Override
public @NotNull LinkBuilder get(@Nullable Page page) {
return new LinkBuilderImpl(page, this);
}
@Override
public @NotNull LinkBuilder get(@Nullable String reference) {
return new LinkBuilderImpl(reference, this);
}
@Override
public @NotNull LinkBuilder get(@NotNull LinkRequest linkRequest) {
return new LinkBuilderImpl(linkRequest, this, componentPropertyResolverFactory);
}
/**
* Resolves the link
* @param linkRequest Link request
* @return Link metadata (never null)
*/
@NotNull
@SuppressWarnings({
"null", "unused",
"java:S6541", "java:S3776", "java:S2589", // ignore complexity
"java:S112", // runtime exception
"java:S1192" // redundant string literals
})
@SuppressFBWarnings({ "CORRECTNESS", "STYLE" })
Link processRequest(@NotNull LinkRequest linkRequest) {
// detect link type - first accepting wins
LinkType linkType = null;
List<Class<? extends LinkType>> linkTypes = linkHandlerConfig.getLinkTypes();
if (linkTypes == null || linkTypes.isEmpty()) {
throw new RuntimeException("No link types defined.");
}
for (Class<? extends LinkType> candidateLinkTypeClass : linkTypes) {
LinkType candidateLinkType = AdaptTo.notNull(adaptable, candidateLinkTypeClass);
if (candidateLinkType.accepts(linkRequest)) {
linkType = candidateLinkType;
break;
}
}
if (linkType == null) {
linkType = AdaptTo.notNull(adaptable, InvalidLinkType.class);
}
Link link = new Link(linkType, linkRequest);
if (log.isTraceEnabled()) {
log.trace("Start processing link request (linkType={}): {}", linkType.getId(), linkRequest);
}
// preprocess link before resolving
List<Class<? extends LinkProcessor>> linkPreProcessors = linkHandlerConfig.getPreProcessors();
if (linkPreProcessors != null) {
for (Class<? extends LinkProcessor> processorClass : linkPreProcessors) {
log.trace("Apply pre processor ({}): {}", processorClass, linkRequest);
LinkProcessor processor = AdaptTo.notNull(adaptable, processorClass);
link = processor.process(link);
if (link == null) {
throw new RuntimeException("LinkPreProcessor '" + processor + "' returned null, page '" + (currentPage != null ? currentPage.getPath() : "-") + "'.");
}
}
}
// resolve link
link = linkType.resolveLink(link);
if (link == null) {
throw new RuntimeException("LinkType '" + linkType + "' returned null, page '" + (currentPage != null ? currentPage.getPath() : "-") + "'.");
}
// if link is invalid - check if a fallback link property is set and try resolution with it
if (!link.isValid()) {
LinkRequest fallbackLinkRequest = getFallbackLinkRequest(linkRequest);
if (fallbackLinkRequest != null) {
log.trace("Link is invalid ({}) - process fallback link request: {}", link, fallbackLinkRequest);
Link fallbackLink = processRequest(fallbackLinkRequest);
if (fallbackLink.isValid()) {
return fallbackLink;
}
}
}
// generate markup (if markup builder is available) - first accepting wins
List<Class<? extends LinkMarkupBuilder>> linkMarkupBuilders = linkHandlerConfig.getMarkupBuilders();
if (linkMarkupBuilders != null) {
link.setAnchorBuilder(l -> {
for (Class<? extends LinkMarkupBuilder> linkMarkupBuilderClass : linkMarkupBuilders) {
LinkMarkupBuilder linkMarkupBuilder = AdaptTo.notNull(adaptable, linkMarkupBuilderClass);
if (linkMarkupBuilder.accepts(l)) {
log.trace("Apply link markup builder ({}): {}", linkMarkupBuilderClass, linkRequest);
return linkMarkupBuilder.build(l);
}
}
return null;
});
}
// postprocess link after resolving
List<Class<? extends LinkProcessor>> linkPostProcessors = linkHandlerConfig.getPostProcessors();
if (linkPostProcessors != null) {
for (Class<? extends LinkProcessor> processorClass : linkPostProcessors) {
log.trace("Apply post processor ({}): {}", processorClass, linkRequest);
LinkProcessor processor = AdaptTo.notNull(adaptable, processorClass);
link = processor.process(link);
if (link == null) {
throw new RuntimeException("LinkPostProcessor '" + processor + "' returned null, page '" + (currentPage != null ? currentPage.getPath() : "-") + "'.");
}
}
}
log.debug("Finished link processing: {}", link);
return link;
}
@Override
public Link invalid() {
LinkType linkType = AdaptTo.notNull(adaptable, InvalidLinkType.class);
return new Link(linkType, new LinkRequest(null, null, null));
}
/**
* Checks if a link target URL is defined in a fallback property and prepare a link request
* to try to resolve this as link instead.
* @param linkRequest Original link request
* @return Fallback link request or null
*/
private @Nullable LinkRequest getFallbackLinkRequest(@NotNull LinkRequest linkRequest) {
Resource resource = linkRequest.getResource();
// works only when resolution based on a resource
if (resource == null) {
return null;
}
// check if a fallback property name was given
String[] linkTargetUrlFallbackProperty = linkRequest.getLinkArgs().getLinkTargetUrlFallbackProperty();
if (linkTargetUrlFallbackProperty == null || linkTargetUrlFallbackProperty.length == 0) {
return null;
}
// check if a link target URL is set in the fallback property
String linkTargetUrl = null;
for (String propertyName : linkTargetUrlFallbackProperty) {
linkTargetUrl = resource.getValueMap().get(propertyName, String.class);
if (StringUtils.isNotBlank(linkTargetUrl)) {
break;
}
}
if (StringUtils.isBlank(linkTargetUrl)) {
return null;
}
LinkArgs fallbackLinkArgs = linkRequest.getLinkArgs().clone();
@NotNull
String @Nullable [] nullArray = null;
fallbackLinkArgs.linkTargetUrlFallbackProperty(nullArray);
return new LinkRequest(null, null, linkTargetUrl, fallbackLinkArgs);
}
}