/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * 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 uk.q3c.krail.core.navigate.sitemap;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.i18n.CurrentLocale;
import uk.q3c.krail.core.i18n.I18NKey;
import uk.q3c.krail.core.view.KrailView;
import uk.q3c.util.CycleDetectedException;
import uk.q3c.util.DynamicDAG;
import uk.q3c.util.MessageFormat;
import javax.annotation.Nonnull;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Checks the Sitemap for inconsistencies after it has been loaded. The following are considered:
* <ol>
* <li>Missing views are not allowed unless the page is redirected
* <li>Missing enums (label keys) are not allowed unless the page is redirected
* <li>Redirects from within the {@link MasterSitemap} have their pageAccessControl attribute set to the
* pageAccessControl of the redirect target.
* <li>Redirects to a child (for example from 'private' to 'private/home' must have a label key
* <p>
* </ol>
*
* @author David Sowerby
*/
public class DefaultSitemapFinisher implements SitemapFinisher {
private static Logger log = LoggerFactory.getLogger(DefaultSitemapFinisher.class);
private final Set<String> missingViewClasses;
private final Set<String> missingLabelKeys;
private final Set<String> missingPageAccessControl;
private final Set<String> redirectLoops;
private Set<String> annotationSources;
private I18NKey defaultKey;
private Class<? extends KrailView> defaultView;
private StringBuilder report;
private Set<String> sourceModuleNames;
@Inject
protected DefaultSitemapFinisher(CurrentLocale currentLocale) {
super();
missingViewClasses = new HashSet<>();
missingLabelKeys = new HashSet<>();
missingPageAccessControl = new HashSet<>();
redirectLoops = new HashSet<>();
}
@Override
public void check(@Nonnull MasterSitemap sitemap) {
checkNotNull(sitemap);
// do this first, because a redirection loop will cause the main check to fail
redirectCheck(sitemap);
replaceMissingViews(sitemap);
replaceMissingKeys(sitemap);
for (MasterSitemapNode node : sitemap.getAllNodes()) {
String nodeUri = sitemap.uri(node);
log.debug("Checking {}", nodeUri);
// If no redirect, must have a label key, pageAccessControl and view
if (!sitemap.getRedirects()
.containsKey(nodeUri)) {
if (node.getViewClass() == null) {
missingViewClasses.add(nodeUri);
}
if (node.getLabelKey() == null) {
missingLabelKeys.add(nodeUri);
}
if (node.getPageAccessControl() == null) {
missingPageAccessControl.add(nodeUri);
}
} else {
// if redirected, take the accessControlPermission from the redirect target
// note: Sitemap allows for multiple levels of redirect
MasterSitemapNode targetNode = sitemap.nodeFor(sitemap.getRedirectPageFor(nodeUri));
MasterSitemapNode newNode = node.modifyPageAccessControl(targetNode.getPageAccessControl());
sitemap.replaceNode(node, newNode);
// if redirect is from parent to child, the parent must have a label key, or it cannot display, in a
// UserNavigationTree for example. Easiest way to check is to take the target node, get the chain
// of nodes 'above' it, then ensure they all have a label key
List<MasterSitemapNode> nodeChainForTarget = sitemap.nodeChainFor(targetNode);
for (MasterSitemapNode n : nodeChainForTarget) {
if (n.getLabelKey() == null) {
missingLabelKeys.add(sitemap.uri(n));
}
}
}
}
// if there are no missing keys or views, return
if (missingViewClasses.isEmpty() && missingLabelKeys.isEmpty() && missingPageAccessControl.isEmpty() &&
redirectLoops.isEmpty()) {
return;
}
report = new StringBuilder();
report.append("\n================ Sitemap Check ===============\n\n");
report.append("Direct Modules\n\n");
if (sourceModuleNames != null) {
for (String s : sourceModuleNames) {
report.append(s);
report.append('\n');
}
} else {
report.append("No direct modules identified\n");
}
report.append("-----------------------------------------------\n");
report.append("Annotation Sources\n\n");
if (annotationSources != null) {
for (String s : annotationSources) {
report.append(s);
report.append('\n');
}
} else {
report.append("No annotation sources identified\n");
}
report.append("-----------------------------------------------\n");
if (!missingViewClasses.isEmpty()) {
report.append("------------ URIs with missing Views -----------\n");
for (String view : missingViewClasses) {
report.append(view);
report.append('\n');
}
}
if (!missingLabelKeys.isEmpty()) {
report.append("--------- URIs with missing label keys -----------\n");
for (String key : missingLabelKeys) {
report.append(key);
report.append('\n');
}
}
if (!missingPageAccessControl.isEmpty()) {
report.append("--------- URIs with missing page access control -----------\n");
for (String key : missingPageAccessControl) {
report.append(key);
report.append('\n');
}
}
if (!redirectLoops.isEmpty()) {
report.append("--------- redirect loops -----------\n");
for (String key : redirectLoops) {
report.append(key);
report.append('\n');
}
}
log.info("{}", report.toString());
// otherwise print a report and throw an exception
throw new SitemapException("Sitemap check failed, see log for failed items");
}
private void replaceMissingViews(MasterSitemap sitemap) {
//there's nothing to replace missing items with
if (defaultView == null) {
return;
}
for (MasterSitemapNode node : sitemap.getAllNodes()) {
if (node.getViewClass() == null) {
MasterSitemapNode newNode = node.modifyView(defaultView);
sitemap.replaceNode(node, newNode);
}
}
}
private void replaceMissingKeys(MasterSitemap sitemap) {
//there's nothing to replace missing items with
if (defaultKey == null) {
return;
}
for (MasterSitemapNode node : sitemap.getAllNodes()) {
if (node.getLabelKey() == null) {
MasterSitemapNode newNode = node.modifyLabelKey(defaultKey);
sitemap.replaceNode(node, newNode);
}
}
}
private void redirectCheck(MasterSitemap sitemap) {
DynamicDAG<String> dag = new DynamicDAG<>();
ImmutableMap<String, String> redirectMap = sitemap.getRedirects();
for (Entry<String, String> entry : redirectMap.entrySet()) {
try {
dag.addChild(entry.getKey(), entry.getValue());
} catch (CycleDetectedException cde) {
String msg = MessageFormat.format("Redirecting {0} to {1} would cause a loop", entry.getKey(), entry.getValue());
redirectLoops.add(msg);
// throw new CycleDetectedException(msg);
}
}
}
@Override
public SitemapFinisher replaceMissingViewWith(@Nonnull Class<? extends KrailView> defaultView) {
checkNotNull(defaultView);
this.defaultView = defaultView;
return this;
}
@Override
public SitemapFinisher replaceMissingKeyWith(@Nonnull I18NKey defaultKey) {
checkNotNull(defaultKey);
this.defaultKey = defaultKey;
return this;
}
@Override
public void setSourceModuleNames(@Nonnull Set<String> names) {
checkNotNull(names);
this.sourceModuleNames = names;
}
@Override
public void setAnnotationSources(@Nonnull Set<String> sources) {
checkNotNull(sources);
this.annotationSources = sources;
}
public Set<String> getMissingViewClasses() {
return missingViewClasses;
}
public Set<String> getMissingLabelKeys() {
return missingLabelKeys;
}
public StringBuilder getReport() {
return report;
}
public Set<String> getMissingPageAccessControl() {
return missingPageAccessControl;
}
}