/*
*
* * 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.util;
import com.vaadin.ui.MenuBar;
import com.vaadin.ui.Tree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Copies source tree to target tree, to a depth specified by {@link #setMaxDepth(int)} with the option to provide
* filters to select which source nodes to exclude. Source and target trees are wrapped in implementations of
* {@link SourceTreeWrapper} and {@link TargetTreeWrapper}, to allow different tree implementations to be copied. Note
* that by default nodes are not cloned, the copy is by reference, but you can determine exactly how target nodes are
* created by implementing {@link NodeModifier}, and injecting it your the {@link TargetTreeWrapper} <br>
* <br>
* {@link TargetTreeWrapper} implementations are provided for the Vaadin {@link Tree}, {@link MenuBar} and
* {@link BasicForest} <br>
* {@link SourceTreeWrapper} implementations are provided for Vaadin {@link Tree} and {@link BasicForest} <br>
* <br>
* The way in which target nodes are constructed has a bearing on how a sort of nodes can be performed. For example,
* linked node trees may require that a child is constructed from a parent or the sort field may be required at the
* time
* of node construction. This means that a sort must be performed before the target nodes are created. This utility
* supports several variations by the use of {@link #sortOption} (the default is to sort the target nodes after
* creation
* but before before adding them to the target). If target nodes are the same as the source nodes, however, it makes no
* difference whether the sort is performed on source or target nodes.<br>
* <br>
* A source or target node sort comparator is set to determine sort order using {@link #setSourceSortComparator(Comparator)}
* or {@link #setTargetSortComparator(Comparator)} - but note that if no
* comparator is provided, nodes must implement {@link Comparable} to provide a natural ordering. <br>
* <br>
* An extension may be injected using {@link #setExtension(TreeCopyExtension)}, to enable post-processing. <br>
* <br>
* If a target node is of a different type to the source node, call
* {@link TargetTreeWrapper#setNodeModifier(NodeModifier)} with an implementation to make the conversion. The default
* simply returns a reference to the source node;
*
* @param <S> source node type
* @param <T> target node type
* @author David Sowerby
* @date 27 May 2014
*/
public class TreeCopy<S, T> {
public enum SortOption {
SORT_SOURCE_NODES, SORT_TARGET_NODES_AFTER_ADD, SORT_TARGET_NODES_BEFORE_ADD
}
private static Logger log = LoggerFactory.getLogger(TreeCopy.class);
private final SourceTreeWrapper<S> source;
private final TargetTreeWrapper<S, T> target;
// We need this to keep a lookup - the target list is sorted before being
private final Map<S, T> sourceToTargetNodeMap = new HashMap<>();
private final LinkedList<NodeFilter<S>> sourceFilters = new LinkedList<>();
private TreeCopyExtension<S, T> extension;
private boolean limitedDepth = false;
private int maxDepth = Integer.MAX_VALUE;
private SortOption sortOption = SortOption.SORT_TARGET_NODES_BEFORE_ADD;
private boolean sorted = true;
private Comparator<S> sourceSortComparator;
private Comparator<T> targetSortComparator;
public TreeCopy(SourceTreeWrapper<S> source, TargetTreeWrapper<S, T> target) {
super();
this.source = source;
this.target = target;
}
public LinkedList<NodeFilter<S>> getSourceFilters() {
return new LinkedList<>(sourceFilters);
}
public int getMaxDepth() {
return maxDepth;
}
/**
* Sets the depth of the copy required. This automatically sets {@link #limitedDepth} to true.
*
* @param maxDepth
*/
public void setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
limitedDepth = true;
log.debug("maxDepth set to {}", maxDepth);
}
public void addSourceFilter(NodeFilter<S> filter) {
sourceFilters.add(filter);
}
public void removeSourceFilter(NodeFilter<S> filter) {
sourceFilters.remove(filter);
}
public boolean isLimitedDepth() {
return limitedDepth;
}
/**
* Once a maximum depth has been specified, that maximum is enabled or disabled by this property.
* {@link #setMaxDepth(int)} will automatically set this property to 'true'
*
* @param limitedDepth
*/
public void setLimitedDepth(boolean limitedDepth) {
this.limitedDepth = limitedDepth;
}
public void copy() {
loadNodeList(null, source.getRoots(), 1);
if (extension != null) {
extension.invoke(source, target, sourceToTargetNodeMap);
}
}
private void loadNodeList(T parentNode, List<S> sourceNodeList, int level) {
Map<T, S> targetToSourceNodeMap = null;
switch (sortOption) {
// some target nodes can only be created from their parent, and cannot be sorted after creation
// so the sort order must be determined before constructing target nodes. This could be used as default
// as it
// will fit most cases, but occasionally a target node may use a different sort key
case SORT_SOURCE_NODES:
log.debug("sorting list of source nodes, using comparator");
if (sorted) {
Collections.sort(sourceNodeList, sourceSortComparator);
}
targetToSourceNodeMap = createTargetNodes(parentNode, sourceNodeList, sortOption);
addNodesToTarget(parentNode, targetToSourceNodeMap);
drillDown(targetToSourceNodeMap, level);
break;
// This is the default option, create the child nodes for a given parent,
// then sort them before adding them to
// the target
case SORT_TARGET_NODES_BEFORE_ADD:
targetToSourceNodeMap = createTargetNodes(parentNode, sourceNodeList, sortOption);
addNodesToTarget(parentNode, targetToSourceNodeMap);
drillDown(targetToSourceNodeMap, level);
break;
// There are some cases, usually with linked node trees, where a child has to be created with the sort field
// present, for example, a caption for MenuItem. Depending on the implementation,
// it may be easier to sort the
// children after they have been added to the parent (if the implementation allows it, as MenuItem does)
case SORT_TARGET_NODES_AFTER_ADD:
targetToSourceNodeMap = createTargetNodes(parentNode, sourceNodeList, sortOption);
addNodesToTarget(parentNode, targetToSourceNodeMap);
if (sorted) {
log.debug("sorting child target nodes after they have been added to target parent node");
target.sortChildren(parentNode, targetSortComparator);
}
drillDown(targetToSourceNodeMap, level);
break;
}
if (extension != null) {
extension.invoke(source, target, sourceToTargetNodeMap);
}
}
/**
* If nodes are not been added during creation, adds the target nodes to the target (attaching them to their parent
* as required)
*
* @param parentNode
* @param targetToSourceNodeMap
*/
private void addNodesToTarget(T parentNode, Map<T, S> targetToSourceNodeMap) {
if (target.getNodeModifier() == null || !target.getNodeModifier()
.attachOnCreate()) {
log.debug("adding {} nodes to target", targetToSourceNodeMap.size());
for (T node : targetToSourceNodeMap.keySet()) {
target.addChild(parentNode, node);
}
}
}
/**
* Now iterate over this level and drill down to next level. SetLeaf is always called, because some implementations require that a node in a navigation
* component may be displayed as a leaf, becuase its children are not to be displayed (for example, excluded pages (positionIndex <0), from
* UserNavigationTree). This logic is determined by the NodeModifier
*
* @param targetToSourceNodeMap
*/
private void drillDown(Map<T, S> targetToSourceNodeMap, int level) {
for (Map.Entry<T, S> entry : targetToSourceNodeMap.entrySet()) {
T childNode = entry.getKey();
List<S> subNodeList = source.getChildren(entry.getValue());
// drill down to next level
if (subNodeList.size() > 0) {
if (!isLimitedDepth() || level < maxDepth) {
loadNodeList(childNode, subNodeList, level + 1);
target.setLeaf(childNode);
} else {
target.forceSetLeaf(childNode);
}
} else {
target.setLeaf(childNode);
}
}
}
/**
* create the source nodes using target.addNode for this set of children. Target nodes are created using a map
* which
* will honour the sort requirements.
*
* @param sourceNodeList
* @return
*/
private Map<T, S> createTargetNodes(T parentNode, List<S> sourceNodeList, SortOption sortOption) {
Map<T, S> targetToSourceNodeMap = null;
switch (sortOption) {
case SORT_SOURCE_NODES:
// nodes has already been sorted (if required), so using LinkedHashMap to keep current order
targetToSourceNodeMap = new LinkedHashMap<>();
break;
case SORT_TARGET_NODES_BEFORE_ADD:
// use the TreeMap to sort if sorted is true
if (sorted) {
log.debug("using SortedMap with comparator to sort target nodes");
targetToSourceNodeMap = new TreeMap<>(targetSortComparator);
} else {
log.debug("sorting not required, use LinkedHashMap for target nodes");
targetToSourceNodeMap = new LinkedHashMap<>();
}
break;
case SORT_TARGET_NODES_AFTER_ADD:
// the map implementation does not really matter
targetToSourceNodeMap = new LinkedHashMap<>();
break;
default:
break;
}
// create the target nodes using target.addNode for this set of children
// parentNode is supplied so that the new child can be attached to its parent
for (S sourceNode : sourceNodeList) {
if (passesFilter(sourceNode)) {
T targetNode = target.createNode(parentNode, sourceNode);
if (targetNode != null) {
targetToSourceNodeMap.put(targetNode, sourceNode);
// we need an easy way to get from source node to target node
// especially for the extension (if there is one)
sourceToTargetNodeMap.put(sourceNode, targetNode);
}
}
}
return targetToSourceNodeMap;
}
private boolean passesFilter(S node) {
boolean accept = true;
for (NodeFilter<S> filter : sourceFilters) {
if (!filter.accept(node)) {
accept = false;
break;
}
}
return accept;
}
public Comparator<S> getSourceSortComparator() {
return sourceSortComparator;
}
public void setSourceSortComparator(Comparator<S> sortComparator) {
this.sourceSortComparator = sortComparator;
}
public TreeCopyExtension<S, T> getExtension() {
return extension;
}
public void setExtension(TreeCopyExtension<S, T> extension) {
this.extension = extension;
}
public Comparator<T> getTargetSortComparator() {
return targetSortComparator;
}
public void setTargetSortComparator(Comparator<T> targetSortComparator) {
this.targetSortComparator = targetSortComparator;
}
public SortOption getSortOption() {
return sortOption;
}
public void setSortOption(SortOption sortOption) {
this.sortOption = sortOption;
}
public boolean isSorted() {
return sorted;
}
public void setSorted(boolean sorted) {
this.sorted = sorted;
}
}