/*
*
* * 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.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.navigate.NavigationState;
import uk.q3c.krail.core.navigate.URIFragmentHandler;
import uk.q3c.krail.core.shiro.PagePermission;
import uk.q3c.util.BasicForest;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public abstract class DefaultSitemapBase<T extends SitemapNode> implements Sitemap<T> {
private static Logger log = LoggerFactory.getLogger(DefaultSitemapBase.class);
protected final URIFragmentHandler uriHandler;
protected final Map<String, T> uriMap = new LinkedHashMap<>();
protected final Map<StandardPageKey, T> standardPages = new HashMap<>();
protected final Map<String, StandardPageKey> uriStandardPages = new HashMap<>();
// Uses LinkedHashMap to retain insertion order
protected final Map<String, String> redirects = new LinkedHashMap<>();
protected BasicForest<T> forest;
private boolean loaded;
private boolean locked;
protected DefaultSitemapBase(URIFragmentHandler uriHandler) {
super();
this.uriHandler = uriHandler;
forest = new BasicForest<>();
}
@Override
public boolean isLocked() {
return locked;
}
@Override
public void lock() {
checkLock();
this.locked = true;
}
/**
* Delegates to {@link BasicForest#getRootFor(Object)}
*
* @param node
* @return
*/
@Override
public synchronized T getRootFor(T node) {
return forest.getRootFor(node);
}
/**
* Delegates to {@link BasicForest#containsNode(Object)}
*
* @param node the node to look for
* @return
*/
@Override
public synchronized boolean containsNode(T node) {
return forest.containsNode(node);
}
/**
* Returns the full URI for {@code node}
*
* @param node
* @return
*/
@Override
public synchronized String uri(T node) {
checkNotNull(node);
StringBuilder buf = new StringBuilder(node.getUriSegment());
prependParent(node, buf);
return buf.toString();
}
/**
* Recursively prepends the parent URI segment of {@code node}, until the full URI has been built
*/
protected void prependParent(T node, StringBuilder buf) {
T parentNode = forest.getParent(node);
if (parentNode != null) {
buf.insert(0, "/");
buf.insert(0, parentNode.getUriSegment());
prependParent(parentNode, buf);
}
}
/**
* Delegates to {@link BasicForest#getAllNodes()}
*
* @return
*/
@Override
public synchronized List<T> getAllNodes() {
return forest.getAllNodes();
}
@Override
public synchronized List<T> getRoots() {
return forest.getRoots();
}
/**
* Delegates to {@link BasicForest#getChildCount(Object)}
*
* @param node
* @return
*/
@Override
public synchronized int getChildCount(T node) {
try {
return forest.getChildCount(node);
} catch (NullPointerException npe) {
throw new SitemapException("Cannot count children of non-existent node", npe);
}
}
/**
* Returns true if the sitemap contains {@code uri}. Only the virtual page part of the URI is used, parameters are
* ignored
*
* @param uri
* @return
*/
@Override
public synchronized boolean hasUri(String uri) {
NavigationState navigationState = uriHandler.navigationState(uri);
return hasUri(navigationState);
}
/**
* Returns true if the sitemap contains the URI represented by virtual page part of {@code navigationState}.
*
* @param navigationState the NavigationState which contains the uri to check
* @return
*/
@Override
public synchronized boolean hasUri(NavigationState navigationState) {
return uriMap.containsKey(navigationState.getVirtualPage());
}
/**
* Returns a {@link NavigationState} object representing the URI for the {@code node}
*
* @param node
* @return
*/
@Override
public synchronized NavigationState navigationState(T node) {
return uriHandler.navigationState(uri(node));
}
@Override
public synchronized String toString() {
return forest.toString();
}
/**
* Returns a {@link PagePermission} object for {@code node}
*
* @param node
* @return
*/
@Override
public synchronized PagePermission pagePermission(T node) {
return new PagePermission(navigationState(node));
}
/**
* Adds {@code node} to the {@link Sitemap}. {@code node} cannot be null
*
* @param node
*/
@Override
public synchronized void addNode(T node) {
checkLock();
checkNotNull(node);
addChild(null, node);
}
@Override
public synchronized void removeNode(T node) {
checkLock();
String uri = uri(node);
if (node.getLabelKey() instanceof StandardPageKey) {
StandardPageKey pageKey = (StandardPageKey) node.getLabelKey();
uriStandardPages.remove(uri);
standardPages.remove(pageKey);
}
forest.removeNode(node);
uriMap.remove(uri);
}
/**
* Returns the {@link SitemapNode} associated with {@code uri}, or the closest available if one cannot be found for
* the full URI. "Closest" means the node which matches the most segments of the URI. Returns null if no match at
* all is found
*
* @param uri
* @return
*/
@Override
public synchronized T nodeNearestFor(String uri) {
return nodeNearestFor(uriHandler.navigationState(uri));
}
/**
* Returns the {@link SitemapNode} associated with {@code navigationState}, or the closest available if one cannot
* be found for the full URI. "Closest" means the node which matches the most segments of the URI. Returns null if
* no match at all is found
*
* @param navigationState
* @return
*/
@Override
public synchronized T nodeNearestFor(NavigationState navigationState) {
List<String> segments = new ArrayList<>(navigationState.getPathSegments());
T node = null;
Joiner joiner = Joiner.on("/");
while ((segments.size() > 0) && (node == null)) {
String path = joiner.join(segments);
node = uriMap.get(path);
segments.remove(segments.size() - 1);
}
return node;
}
@Override
public synchronized String standardPageURI(StandardPageKey pageKey) {
checkNotNull(pageKey);
//can't use the uri method as the standard page keys may not be in the main uri map (which define the full uri by virtue of
//parent child relationships
for (Map.Entry<String, StandardPageKey> entry : uriStandardPages.entrySet()) {
if (entry.getValue() == pageKey) {
return entry.getKey();
}
}
throw new SitemapException("No URI found for StandardPageKey " + pageKey);
}
@Override
public synchronized ImmutableMap<StandardPageKey, T> getStandardPages() {
return ImmutableMap.copyOf(standardPages);
}
/**
* Delegates to {@link BasicForest#getChildren(Object)}
*
* @param parentNode
* @return
*/
@Override
public synchronized List<T> getChildren(T parentNode) {
return forest.getChildren(parentNode);
}
/**
* Returns the {@link SitemapNode} associated with {@code uri}, or null if none found
*
* @param uri
* @return
*/
@Override
public synchronized T nodeFor(String uri) {
return uriMap.get(uriHandler.navigationState(uri)
.getVirtualPage());
}
/**
* Returns the {@link SitemapNode} associated with {@code navigationState}, or null if none found
*
* @param navigationState
* @return
*/
@Override
public synchronized T nodeFor(NavigationState navigationState) {
if (navigationState == null) {
return null;
}
return uriMap.get(navigationState.getVirtualPage());
}
/**
* Returns a redirect for sourceNode if there is one, null if there is not. Allows for multiple levels of redirect
*
* @return
*/
@Override
public synchronized T getRedirectNodeFor(T sourceNode) {
String sourceUri = uri(sourceNode);
String redirectPageFor = getRedirectPageFor(sourceUri);
return nodeFor(redirectPageFor);
}
/**
* Safe copy of redirects
*
* @return
*/
@Override
public synchronized ImmutableMap<String, String> getRedirects() {
return ImmutableMap.copyOf(redirects);
}
/**
* Adds a redirect from {@code fromPage} to {@code toPage}. No checking is done of the validity or structure of the
* parameters. {@code toPage} is not checked for existence within the map, this is done by the
* {@link SitemapFinisher} once assembly of the {@link MasterSitemap} is complete
*
* @param fromPage
* @param toPage
* @return
*/
@Override
public synchronized Sitemap<T> addRedirect(String fromPage, String toPage) {
checkLock();
redirects.put(fromPage, toPage);
return this;
}
/**
* Returns a safe copy of all the URIs contained in the sitemap.
*
* @return
*/
@Override
public synchronized ImmutableList<String> uris() {
return ImmutableList.copyOf(uriMap.keySet());
}
@Override
public synchronized int getNodeCount() {
return forest.getNodeCount();
}
/**
* returns a list of {@link SitemapNode} matching the virtual page of the {@code navigationState} provided. Uses
* the
* {@link URIFragmentHandler} to get URI path segments and {@link Sitemap} to obtain the node chain.
* {@code allowPartialPath} determines how a partial match is handled (see
* {@link Sitemap#nodeChainForSegments(List, boolean)} javadoc
*
* @param uri
* @return
*/
@Override
public synchronized List<T> nodeChainForUri(String uri, boolean allowPartialPath) {
return nodeChainFor(uriHandler.navigationState(uri), allowPartialPath);
}
/**
* returns a list of {@link SitemapNode} matching the virtual page of the {@code navigationState} provided. Uses
* the {@link URIFragmentHandler} to get URI path segments and {@link Sitemap} to obtain the node chain.
*
* @param navigationState the navigation state to assess
* @param allowPartialPath determines how a partial match is handled (see
* {@link Sitemap#nodeChainForSegments(List, boolean)} javadoc
* @return a list of {@link SitemapNode} matching the virtual page of the {@code navigationState} provided.
*/
@Override
public synchronized List<T> nodeChainFor(NavigationState navigationState, boolean allowPartialPath) {
List<String> segments = navigationState.getPathSegments();
return nodeChainForSegments(segments, allowPartialPath);
}
/**
* Returns a list of {@link SitemapNode} matching the {@code segments} provided. If there is an incomplete match (a
* segment cannot be found) then:
* <ol>
* <li>if {@code allowPartialPath} is true a list of nodes is returned correct to the longest path possible.
* <li>if {@code allowPartialPath} is false an empty list is returned
*
* @param segments
* @return
*/
@Override
public synchronized List<T> nodeChainForSegments(List<String> segments, boolean allowPartialPath) {
List<T> nodeChain = new ArrayList<>();
int i = 0;
String currentSegment = null;
List<T> nodes = forest.getRoots();
boolean segmentNotFound = false;
T node = null;
while ((i < segments.size()) && (!segmentNotFound)) {
currentSegment = segments.get(i);
node = findNodeBySegment(nodes, currentSegment, false);
if (node != null) {
nodeChain.add(node);
nodes = forest.getChildren(node);
i++;
} else {
segmentNotFound = true;
}
}
if (segmentNotFound && !allowPartialPath) {
nodeChain.clear();
}
return nodeChain;
}
protected T findNodeBySegment(List<T> nodes, String segment, boolean createIfAbsent) {
T foundNode = null;
for (T node : nodes) {
if (node.getUriSegment()
.equals(segment)) {
foundNode = node;
break;
}
}
if ((foundNode == null) && (createIfAbsent)) {
foundNode = createNode(segment);
}
return foundNode;
}
protected abstract T createNode(String segment);
/**
* Returns a list of nodes which form the chain from this {@code node} to its root in the {@link Sitemap}. The list
* includes {@code node}
*
* @param node
* @return
*/
@Override
public synchronized List<T> nodeChainFor(T node) {
List<T> nodes = new ArrayList<>();
nodes.add(node);
T parent = this.getParent(node);
while (parent != null) {
nodes.add(0, parent);
parent = this.getParent(parent);
}
return nodes;
}
/**
* Returns the parent of {@code node}. Will be null if {@code node} has no parent (that is, it is a root node)
*
* @param childNode
* @return
*/
@Override
public synchronized T getParent(T childNode) {
return forest.getParent(childNode);
}
/**
* If the virtual page represented by {@code navigationState} has been redirected, return the page it has been
* redirected to, otherwise, just return the virtual page unchanged. Allows for multiple levels of redirect.
*
* @param navigationState the navigationState to assess
* @return
*/
@Override
public synchronized String getRedirectPageFor(NavigationState navigationState) {
String virtualPage = navigationState.getVirtualPage();
return getRedirectPageFor(virtualPage);
}
/**
* If the {@code page} has been redirected, return the page it has been redirected to, otherwise, just return
* {@code page}. Allows for multiple levels of redirect
*
* @param page
* @return
*/
@Override
public synchronized String getRedirectPageFor(String page) {
String p = redirects.get(page);
if (p == null) {
return page;
}
String p1 = null;
while (p != null) {
p1 = p;
p = redirects.get(p1);
}
return p1;
}
/**
* Adds the {@code childNode} to the {@code parentNode}. If either of the nodes do not currently exist in the
* {@link Sitemap} they will be added to it.
* <p>
*
* @param parentNode
* @param childNode
*/
@Override
public synchronized void addChild(T parentNode, T childNode) {
checkLock();
checkNotNull(childNode);
// add the parent node if not already there
if ((parentNode != null) && (!containsNode(parentNode))) {
forest.addNode(parentNode);
String newUri = uri(parentNode);
uriMap.put(newUri, parentNode);
checkForStandardPage(parentNode);
}
// remove the child node - it may be moving from one parent to another
if (containsNode(childNode)) {
removeNode(childNode);
}
// add it to structure first, otherwise the uri will be wrong
forest.addChild(parentNode, childNode);
uriMap.put(uri(childNode), childNode);
checkForStandardPage(childNode);
}
@Override
public BasicForest<T> getForest() {
return forest;
}
protected void checkForStandardPage(T node) {
checkNotNull(node);
if (node.getLabelKey() instanceof StandardPageKey) {
addStandardPage(node, uri(node));
}
}
@Override
public void addStandardPage(T node, String uri) {
checkLock();
checkArgument(node.getLabelKey() instanceof StandardPageKey, "Key must be a Standard Page Key");
StandardPageKey pageKey = (StandardPageKey) node.getLabelKey();
standardPages.put(pageKey, node);
uriStandardPages.put(uri, pageKey);
}
@Override
public void clear() {
checkLock();
forest.clear();
standardPages.clear();
uriMap.clear();
uriStandardPages.clear();
redirects.clear();
loaded = false;
log.debug("sitemap cleared");
}
@Override
public boolean isLoaded() {
return loaded;
}
@Override
public void setLoaded(boolean loaded) {
checkLock();
this.loaded = loaded;
}
/**
* Returns a safe copy of {@link #uriMap}
*
* @return
*/
@Override
public Map<String, T> getUriMap() {
return ImmutableMap.copyOf(uriMap);
}
/**
* {@link MasterSitemapNode} is immutable, but there are occasions where it needs to be "updated" in the sitemap - which in practice means replacing it.
* <p>
* If the sitemap already contains {@code childNode}, the {@code parentNode} is ignored, and the child replaces the original directly, transferring parent
* and child relationships.
* <p>
* If the sitemap does not contain {@code childNode}, then it is added to the sitemap and attached to the {@code parentNode}. The {@code parentNode} may
* still be null, if the {@code childNode} is a root.
* <p>
* {@code parentNode} may be null if child is to be a new root, but if not null must have an id and uriSegment<br>
* {@code childNode} cannot be null, and must have an id and uriSegment
*
* @param parentNode
* @param childNode
*/
public void addOrReplaceChild(@Nullable T parentNode, @Nonnull T childNode) {
checkLock();
checkNotNull(childNode);
checkArgument(childNode.getId() > 0);
checkNotNull(childNode.getUriSegment());
if (parentNode != null) {
checkArgument(parentNode.getId() > 0);
checkNotNull(parentNode.getUriSegment());
}
if (containsNode(childNode)) {
T oldNode = forest.getNode(childNode);
replaceNode(oldNode, childNode);
}
addChild(parentNode, childNode);
}
/**
* Replaces an existing node instance in the Sitemap, moving any connections from the old to the new. If the {@ oldInstance} is a standard page it is
* removed {@link #standardPages}. If the {@code newInstance} is a standard page, it is added to {@link #standardPages}
* if the node key is a {@link StandardPageKey}. A standard page is identified by its labelKey being a {@code StandardPageKey}
*
* @param oldInstance the instance to be replaced
* @param newInstance the instance to put in place
*/
public void replaceNode(@Nonnull T oldInstance, @Nonnull T newInstance) {
checkLock();
checkNotNull(oldInstance);
checkNotNull(newInstance);
forest.replaceNode(oldInstance, newInstance);
if (oldInstance.getLabelKey() instanceof StandardPageKey) {
standardPages.remove(oldInstance.getLabelKey());
}
if (newInstance.getLabelKey() instanceof StandardPageKey) {
standardPages.put((StandardPageKey) newInstance.getLabelKey(), newInstance);
uriStandardPages.put(uri(newInstance), (StandardPageKey) newInstance.getLabelKey());
}
uriMap.put(uri(newInstance), newInstance);
}
/**
* The standard page nodes are sometimes not in the user sitemap (for example, the login node is not there after
* login). Use the isxxxUri methods to test a uri for a match to a standard page
*
* @param navigationState the navigation state to test
* @return true if the navigation state represents the login uri
*/
@Override
public boolean isLoginUri(@Nonnull NavigationState navigationState) {
checkNotNull(navigationState);
return isStandardUri(StandardPageKey.Log_In, navigationState);
}
private boolean isStandardUri(StandardPageKey key, NavigationState navigationState) {
return key == (uriStandardPages.get(navigationState.getVirtualPage()));
}
@Override
public synchronized T standardPageNode(StandardPageKey pageKey) {
return standardPages.get(pageKey);
}
/**
* The standard page nodes are sometimes not in the user sitemap (for example, the login node is not there after
* login). Use the isxxxUri methods to test a uri for a match to a standard page
*
* @param navigationState the navigation state to test
* @return true if the navigation state represents the logout uri
*/
@Override
public boolean isLogoutUri(@Nonnull NavigationState navigationState) {
return isStandardUri(StandardPageKey.Log_Out, navigationState);
}
/**
* The standard page nodes are sometimes not in the user sitemap (for example, the login node is not there after
* login). Use the isxxxUri methods to test a uri for a match to a standard page
*
* @param navigationState the navigation state to test
* @return true if the navigation state represents the private home uri
*/
@Override
public boolean isPrivateHomeUri(@Nonnull NavigationState navigationState) {
return isStandardUri(StandardPageKey.Private_Home, navigationState);
}
/**
* The standard page nodes are sometimes not in the user sitemap (for example, the login node is not there after
* login). Use the isxxxUri methods to test a uri for a match to a standard page
*
* @param navigationState the navigation state to test
* @return true if the navigation state represents the public home uri
*/
@Override
public boolean isPublicHomeUri(@Nonnull NavigationState navigationState) {
return isStandardUri(StandardPageKey.Public_Home, navigationState);
}
public ImmutableMap<String, StandardPageKey> getStandardPageUris() {
return ImmutableMap.copyOf(uriStandardPages);
}
/**
* @throws SitemapLockedException is {@link #locked} is true
*/
protected void checkLock() {
if (locked) {
throw new SitemapLockedException("Sitemap is locked, available for read only");
}
}
}