com.sdl.webapp.tridion.navigation.DynamicNavigationProvider Maven / Gradle / Ivy
package com.sdl.webapp.tridion.navigation;
import com.google.common.base.Strings;
import com.sdl.dxa.api.datamodel.model.SitemapItemModelData;
import com.sdl.dxa.api.datamodel.model.TaxonomyNodeModelData;
import com.sdl.dxa.common.dto.DepthCounter;
import com.sdl.dxa.common.dto.SitemapRequestDto;
import com.sdl.dxa.common.util.PathUtils;
import com.sdl.dxa.performance.Performance;
import com.sdl.dxa.tridion.navigation.dynamic.NavigationModelProvider;
import com.sdl.dxa.tridion.navigation.dynamic.OnDemandNavigationModelProvider;
import com.sdl.webapp.common.api.content.LinkResolver;
import com.sdl.webapp.common.api.localization.Localization;
import com.sdl.webapp.common.api.model.entity.NavigationLinks;
import com.sdl.webapp.common.api.model.entity.SitemapItem;
import com.sdl.webapp.common.api.model.entity.TaxonomyNode;
import com.sdl.webapp.common.api.navigation.NavigationFilter;
import com.sdl.webapp.common.api.navigation.NavigationProvider;
import com.sdl.webapp.common.api.navigation.NavigationProviderException;
import com.sdl.webapp.common.api.navigation.OnDemandNavigationProvider;
import com.sdl.webapp.common.exceptions.DxaItemNotFoundException;
import com.sdl.webapp.tridion.fields.exceptions.TaxonomyNotFoundException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.sdl.dxa.common.util.PathUtils.isHomePath;
/**
* Navigation Provider implementation based on Taxonomies (Categories & Keywords).
* Falls back to {@link StaticNavigationProvider} when dynamic navigation is not available.
*/
@Slf4j
@Service
@Primary
@Profile("dynamic.navigation.provider")
public class DynamicNavigationProvider implements NavigationProvider, OnDemandNavigationProvider {
private final StaticNavigationProvider staticNavigationProvider;
private final NavigationModelProvider navigationModelProvider;
private final OnDemandNavigationModelProvider onDemandNavigationModelProvider;
private final LinkResolver linkResolver;
@Autowired
public DynamicNavigationProvider(StaticNavigationProvider staticNavigationProvider,
LinkResolver linkResolver,
NavigationModelProvider navigationModelProvider,
OnDemandNavigationModelProvider onDemandNavigationModelProvider) {
this.staticNavigationProvider = staticNavigationProvider;
this.linkResolver = linkResolver;
this.navigationModelProvider = navigationModelProvider;
this.onDemandNavigationModelProvider = onDemandNavigationModelProvider;
}
@Override
public SitemapItem getNavigationModel(Localization localization) throws NavigationProviderException {
try(Performance perf = new Performance(1_000L, "getNavigationModel")) {
Optional navigationModel = getNavigationModelInternal(localization);
if (!navigationModel.isPresent()) {
return staticNavigationProvider.getNavigationModel(localization);
}
return convert(navigationModel.get());
}
}
@Override
public NavigationLinks getTopNavigationLinks(String requestPath, Localization localization) throws NavigationProviderException {
try(Performance perf = new Performance(1_000L, "getTopNavigationLinks")) {
Optional navigationModel = getNavigationModelInternal(localization);
if (!navigationModel.isPresent()) {
return staticNavigationProvider.getTopNavigationLinks(requestPath, localization);
}
return toNavigationLinksInternal(navigationModel.get().getItems(), true, localization);
}
}
@Override
public NavigationLinks getContextNavigationLinks(String requestPath, Localization localization) throws NavigationProviderException {
try(Performance perf = new Performance(1_000L, "getContextNavigationLinks")) {
Optional navigationModel = getNavigationModelInternal(localization);
if (!navigationModel.isPresent()) {
return staticNavigationProvider.getContextNavigationLinks(requestPath, localization);
}
navigationModel.get().rebuildParentRelationships();
SitemapItemModelData currentLevel = navigationModel.get().findWithUrl(PathUtils.stripDefaultExtension(requestPath));
if (currentLevel != null && !(currentLevel instanceof TaxonomyNodeModelData)) {
currentLevel = currentLevel.getParent();
}
return toNavigationLinksInternal(currentLevel == null ? Collections.emptySet() : currentLevel.getItems(), true, localization);
}
}
@Override
public NavigationLinks getBreadcrumbNavigationLinks(String requestPath, Localization localization) throws NavigationProviderException {
try(Performance perf = new Performance(1_000L, "getBreadcrumbNavigationLinks")) {
Optional navigationModel = getNavigationModelInternal(localization);
if (!navigationModel.isPresent()) {
return staticNavigationProvider.getBreadcrumbNavigationLinks(requestPath, localization);
}
navigationModel.get().rebuildParentRelationships();
SitemapItemModelData currentLevel = navigationModel.get().findWithUrl(PathUtils.stripDefaultExtension(requestPath));
Collection items = currentLevel == null ? Collections.emptySet() : collectBreadcrumbsToLevel(currentLevel, localization);
return toNavigationLinksInternal(items,false, localization);
}
}
@Override
public Collection getNavigationSubtree(@Nullable String sitemapItemId, @NonNull NavigationFilter navigationFilter, @NonNull Localization localization) throws DxaItemNotFoundException {
try(Performance perf = new Performance(1_000L, "getNavigationSubtree")) {
Optional> subtree;
SitemapRequestDto requestDto = SitemapRequestDto
.builder(Integer.parseInt(localization.getId()))
.navigationFilter(navigationFilter)
.expandLevels(new DepthCounter(navigationFilter.getDescendantLevels()))
.sitemapId(sitemapItemId)
.uriType(localization.getCmUriScheme())
.build();
subtree = onDemandNavigationModelProvider.getNavigationSubtree(requestDto);
if (!subtree.isPresent()) {
throw new TaxonomyNotFoundException("Keyword '" + requestDto.getSitemapId() + "' in publication '" + requestDto.getLocalizationId() + "' was not found.");
}
return subtree.get().stream()
.map(this::convert)
.collect(Collectors.toList());
}
}
@NotNull
private NavigationLinks toNavigationLinksInternal(Collection items,
boolean onlyVisible, Localization localization) {
return items.stream()
.filter(model -> !onlyVisible || (model.isVisible() && !isNullOrEmpty(model.getUrl())))
.map(this::convert)
.map(item -> item.createLink(linkResolver, localization))
.collect(Collectors.collectingAndThen(Collectors.toList(), NavigationLinks::new));
}
@NotNull
private List collectBreadcrumbsToLevel(SitemapItemModelData currentLevel,
final Localization localization) {
List breadcrumbs = new LinkedList<>();
SitemapItemModelData model = currentLevel;
boolean hasHome = false;
while (model.getParent() != null) {
breadcrumbs.add(model);
hasHome = isHomePath(model.getUrl(), localization.getPath());
model = model.getParent();
}
// The Home TaxonomyNode/Keyword may be a top-level sibling instead of an ancestor
if (!hasHome) {
SitemapItemModelData item = model.getItems().stream()
.filter(input -> isHomePath(input.getUrl(), localization.getPath()))
.findFirst()
.orElse(null);
if (item != null) {
breadcrumbs.add(item);
}
}
Collections.reverse(breadcrumbs);
return breadcrumbs;
}
@NotNull
private Optional getNavigationModelInternal(Localization localization) {
SitemapRequestDto requestDto = SitemapRequestDto.wholeTree(Integer.parseInt(localization.getId())).build();
Optional navigationModel = navigationModelProvider.getNavigationModel(requestDto);
if (!navigationModel.isPresent()) {
log.warn("Taxonomy navigation is not available, fallback to static navigation is required, localizationId {}", localization.getId());
return Optional.empty();
}
Assert.isInstanceOf(TaxonomyNodeModelData.class, navigationModel.get(), "Navigation model should always be a taxonomy node");
return Optional.of(navigationModel.get());
}
@NotNull
private SitemapItem convert(@NotNull SitemapItemModelData model) {
SitemapItem item = instantiateSitemap(model);
item.setId(model.getId());
item.setVisible(model.isVisible());
if (!Strings.isNullOrEmpty(model.getUrl())) item.setUrl("/" + model.getUrl().replaceAll("^/?(.*)", "$1"));
item.setTitle(model.getTitle());
item.setOriginalTitle(model.getOriginalTitle());
item.setPublishedDate(model.getPublishedDate());
item.setType(model.getType());
model.getItems().forEach(modelData -> item.addItem(convert(modelData)));
return item;
}
@NotNull
private SitemapItem instantiateSitemap(@NotNull SitemapItemModelData model) {
if (model instanceof TaxonomyNodeModelData) {
TaxonomyNodeModelData taxonomyModel = (TaxonomyNodeModelData) model;
TaxonomyNode item = new TaxonomyNode();
item.setKey(taxonomyModel.getKey());
item.setClassifiedItemsCount(taxonomyModel.getClassifiedItemsCount());
item.setDescription(taxonomyModel.getDescription());
item.setTaxonomyAbstract(taxonomyModel.isTaxonomyAbstract());
item.setWithChildren(taxonomyModel.isWithChildren());
return item;
} else {
return new SitemapItem();
}
}
}