InternalLinkResolver.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.type.helpers;
import static com.day.cq.wcm.api.NameConstants.PN_REDIRECT_TARGET;
import static io.wcm.handler.link.LinkNameConstants.PN_LINK_FRAGMENT;
import static io.wcm.handler.link.LinkNameConstants.PN_LINK_QUERY_PARAM;
import static io.wcm.handler.link.LinkNameConstants.PN_LINK_TYPE;
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.api.resource.ValueMap;
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.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.WCMMode;
import io.wcm.handler.link.Link;
import io.wcm.handler.link.LinkArgs;
import io.wcm.handler.link.LinkHandler;
import io.wcm.handler.link.LinkRequest;
import io.wcm.handler.link.spi.LinkHandlerConfig;
import io.wcm.handler.url.UrlHandler;
import io.wcm.handler.url.spi.UrlHandlerConfig;
import io.wcm.sling.commons.adapter.AdaptTo;
import io.wcm.sling.models.annotations.AemObject;
import io.wcm.wcm.commons.contenttype.FileExtension;
import io.wcm.wcm.commons.instancetype.InstanceTypeService;
import io.wcm.wcm.commons.util.Path;
/**
* Implements resolving an internal link for link types. The primary goal is to support the implementation
* for {@link io.wcm.handler.link.type.InternalLinkType}, but it can be used by custom link type implementations as
* well.
* <p>
* The link resolving process can be customized by providing a customized {@link InternalLinkResolverOptions} object.
* </p>
*/
@Model(adaptables = {
SlingHttpServletRequest.class, Resource.class
})
@ProviderType
public final class InternalLinkResolver {
@Self
private LinkHandlerConfig linkHandlerConfig;
@Self
private UrlHandlerConfig urlHandlerConfig;
@Self
private LinkHandler linkHandler;
@Self
private UrlHandler urlHandler;
@SlingObject
private ResourceResolver resourceResolver;
@AemObject
private PageManager pageManager;
@AemObject(injectionStrategy = InjectionStrategy.OPTIONAL)
private Page currentPage;
@AemObject(injectionStrategy = InjectionStrategy.OPTIONAL)
private WCMMode wcmMode;
@OSGiService
private InstanceTypeService instanceTypeService;
/**
* Check if a given page is valid and acceptable to link upon.
* @param page Page
* @param options Options
* @return true if link is acceptable
*/
@SuppressWarnings({
"java:S1172", // options is unused, but may be needed in the future
"java:S1126" // keep separate if statements for better readability
})
public boolean acceptPage(@Nullable Page page, @NotNull InternalLinkResolverOptions options) {
if (page == null) {
return false;
}
// check for jcr:content node
if (!page.hasContent()) {
return false;
}
// check if page is valid concerning on/off-time (only in publish environment)
if (instanceTypeService.isPublish() && !page.isValid()) {
return false;
}
// check if page is acceptable based on link handler config
if (!linkHandlerConfig.isValidLinkTarget(page)) {
return false;
}
return true;
}
/**
* Resolves a link and stores the result in the link object.
* Primary it sets the property "linkReferenceInvalid" and the URL is link resolving was successful.
* @param link Link
* @param options Options to influence the link resolution process
* @return Resolved link object
*/
@SuppressWarnings("java:S3776") // ignore complexity
public @NotNull Link resolveLink(@NotNull Link link, @NotNull InternalLinkResolverOptions options) {
LinkRequest linkRequest = link.getLinkRequest();
ValueMap props = linkRequest.getResourceProperties();
// flag to indicate whether any link reference parameter set
boolean referenceSet = false;
// first try to get direct link target page
Page targetPage = link.getLinkRequest().getPage();
if (targetPage != null) {
referenceSet = true;
}
// if no target page is set get internal path that points to target page
if (targetPage == null) {
String targetPath = props.get(options.getPrimaryLinkRefProperty(), String.class);
if (StringUtils.isEmpty(targetPath)) {
targetPath = link.getLinkRequest().getReference();
}
if (StringUtils.isNotEmpty(targetPath)) {
referenceSet = true;
}
targetPage = getTargetPage(targetPath, options);
}
UrlHandlerConfig resolvingUrlHandlerConfig = urlHandlerConfig;
UrlHandler resolvingUrlHandler = urlHandler;
// use URL handler from target context for link URL building
if (targetPage != null && useTargetContext(options)) {
Resource resource = targetPage.getContentResource();
resolvingUrlHandlerConfig = AdaptTo.notNull(resource, UrlHandlerConfig.class);
resolvingUrlHandler = AdaptTo.notNull(resource, UrlHandler.class);
}
// if target page is a redirect or integrator page recursively resolve link to which the redirect points to
// (skip this redirection if edit mode is active)
if (targetPage != null
&& (linkHandlerConfig.isRedirect(targetPage) || resolvingUrlHandlerConfig.isIntegrator(targetPage))
&& wcmMode != WCMMode.EDIT) {
return recursiveResolveLink(targetPage, link);
}
// build link url
String linkUrl = null;
if (targetPage != null) {
link.setTargetPage(targetPage);
LinkArgs linkArgs = linkRequest.getLinkArgs();
String selectors = linkArgs.getSelectors();
String fileExtension = StringUtils.defaultString(linkArgs.getExtension(), FileExtension.HTML);
String suffix = linkArgs.getSuffix();
String queryString = linkArgs.getQueryString();
String fragment = linkArgs.getFragment();
// optionally override query parameters and fragment from link resource
if (queryString == null) {
queryString = props.get(PN_LINK_QUERY_PARAM, String.class);
}
else {
queryString = props.get(PN_LINK_QUERY_PARAM, queryString);
}
if (fragment == null) {
fragment = props.get(PN_LINK_FRAGMENT, String.class);
}
else {
fragment = props.get(PN_LINK_FRAGMENT, fragment);
}
// build link url
linkUrl = resolvingUrlHandler.get(targetPage)
.selectors(selectors)
.extension(fileExtension)
.suffix(suffix)
.queryString(queryString)
.fragment(fragment)
.urlMode(linkArgs.getUrlMode())
.vanityMode(linkArgs.getVanityMode())
.disableSuffixSelector(linkArgs.isDisableSuffixSelector())
.buildExternalLinkUrl(targetPage);
}
// mark link as invalid if a reference was set that could not be resolved
if (linkUrl == null && referenceSet) {
link.setLinkReferenceInvalid(true);
}
// set link url
link.setUrl(linkUrl);
return link;
}
/**
* Resolves link of redirect or integrator page. Those pages contain the link reference information in their
* content resource (jcr:content node). This information is used to resolve the link.
* @param redirectPage Redirect or integrator page
* @param link Link metadata
* @return Link metadata
*/
private Link recursiveResolveLink(Page redirectPage, Link link) {
LinkRequest linkRequest = link.getLinkRequest();
LinkRequest redirectLinkRequest;
String linkType = redirectPage.getProperties().get(PN_LINK_TYPE, String.class);
String cqRedirectTarget = redirectPage.getProperties().get(PN_REDIRECT_TARGET, String.class);
if (StringUtils.isBlank(linkType) && StringUtils.isNotBlank(cqRedirectTarget)) {
// detected cq-style cq:redirectTarget property, use it's value as reference
redirectLinkRequest = new LinkRequest(
null,
null,
cqRedirectTarget,
linkRequest.getLinkArgs());
}
else {
// set link reference to content resource of redirect page, keep other parameters
redirectLinkRequest = new LinkRequest(
redirectPage.getContentResource(),
null,
linkRequest.getLinkArgs());
}
// check of maximum recursive calls via threadlocal to avoid endless loops, return invalid link if one is detected
LinkResolveCounter linkResolveCounter = LinkResolveCounter.get();
try {
linkResolveCounter.increaseCount();
if (linkResolveCounter.isMaximumReached()) {
// endless loop detected - set link to invalid link
link.setUrl(null);
return link;
}
// resolve link by recursive call to link handler, track recursion count
Link resolvedLink = linkHandler.get(redirectLinkRequest).build();
resolvedLink.addRedirectPage(redirectPage);
return resolvedLink;
}
finally {
linkResolveCounter.decreaseCount();
}
}
/**
* Returns the target page for the given internal content link reference.
* Checks validity of page.
* @param targetPath Repository path
* @return Target page or null if target reference is invalid.
*/
private Page getTargetPage(String targetPath, InternalLinkResolverOptions options) {
if (StringUtils.isEmpty(targetPath)) {
return null;
}
// Rewrite target to current site context
String rewrittenPath;
if (options.isRewritePathToContext() && !useTargetContext(options)) {
rewrittenPath = urlHandler.rewritePathToContext(SyntheticNavigatableResource.get(targetPath, resourceResolver));
}
else {
rewrittenPath = targetPath;
}
if (StringUtils.isEmpty(rewrittenPath)) {
return null;
}
// Get target page referenced by target path and check for acceptance
Page targetPage = pageManager.getPage(rewrittenPath);
if (acceptPage(targetPage, options)) {
return targetPage;
}
else {
return null;
}
}
/**
* Checks if target context should be used.
* @param options Link resolver options
* @return true if target context should be used
*/
@SuppressWarnings("java:S1871")
private boolean useTargetContext(InternalLinkResolverOptions options) {
if (options.isUseTargetContext() && !options.isRewritePathToContext()) {
return true;
}
// even is use target context is not activated use it if current page is an experience fragment
// otherwise it will be always impossible to resolve internal links
else if (currentPage != null && Path.isExperienceFragmentPath(currentPage.getPath())) {
return true;
}
return false;
}
}