/*
*
* * 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.view.component;
import com.google.inject.Inject;
import com.vaadin.data.Property;
import com.vaadin.ui.Tree;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.engio.mbassy.listener.Handler;
import net.engio.mbassy.listener.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.eventbus.SessionBus;
import uk.q3c.krail.core.eventbus.SubscribeTo;
import uk.q3c.krail.core.eventbus.UIBus;
import uk.q3c.krail.core.guice.uiscope.UIScoped;
import uk.q3c.krail.core.i18n.DescriptionKey;
import uk.q3c.krail.core.i18n.LabelKey;
import uk.q3c.krail.core.navigate.Navigator;
import uk.q3c.krail.core.navigate.sitemap.*;
import uk.q3c.krail.core.navigate.sitemap.comparator.DefaultUserSitemapSorters.SortType;
import uk.q3c.krail.core.navigate.sitemap.comparator.UserSitemapSorters;
import uk.q3c.krail.core.option.Option;
import uk.q3c.krail.core.option.OptionContext;
import uk.q3c.krail.core.option.OptionKey;
import uk.q3c.krail.core.view.KrailView;
import uk.q3c.util.ID;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A navigation tree for users to find their way around the site. Uses {@link UserSitemap} to provide the structure
* which in turn provides a view on {@link MasterSitemap} filtered for the current user's selection of locale and
* authorised pages. Although this seems naturally to be a {@link UIScoped} class it is not currently possible to have
* a
* UIScoped Component (see https://github.com/davidsowerby/krail/issues/177)
*
* @author David Sowerby 17 May 2013
* @modified David Sowerby 29 May 2015
*/
@Listener
@SubscribeTo({UIBus.class, SessionBus.class})
public class DefaultUserNavigationTree extends Tree implements OptionContext, UserNavigationTree {
public static final OptionKey<SortType> optionKeySortType = new OptionKey<>(SortType.ALPHA, DefaultUserNavigationTree.class, LabelKey.Sort_Type,
DescriptionKey.Sort_Type);
public static final OptionKey<Boolean> optionKeySortAscending = new OptionKey<>(Boolean.TRUE, DefaultUserNavigationTree.class, LabelKey.Sort_Ascending,
DescriptionKey.Sort_Ascending);
public static final OptionKey<Integer> optionKeyMaximumDepth = new OptionKey<>(10, DefaultUserNavigationTree.class, LabelKey.Maxiumum_Depth, DescriptionKey
.Maximum_Tree_Depth);
private static Logger log = LoggerFactory.getLogger(DefaultUserNavigationTree.class);
private final UserSitemap userSitemap;
private final Navigator navigator;
private final Option option;
private final UserNavigationTreeBuilder builder;
private final UserSitemapSorters sorters;
private boolean rebuildRequired = true;
private boolean suppressValueChangeEvents;
@Inject
protected DefaultUserNavigationTree(UserSitemap userSitemap, Navigator navigator, Option option, UserNavigationTreeBuilder builder, UserSitemapSorters
sorters) {
super();
this.userSitemap = userSitemap;
this.navigator = navigator;
this.option = option;
this.builder = builder;
this.sorters = sorters;
builder.setUserNavigationTree(this);
setImmediate(true);
setItemCaptionMode(ItemCaptionMode.EXPLICIT);
addValueChangeListener(this);
setId(ID.getId(Optional.empty(), this));
sorters.setOptionSortAscending(getOptionSortAscending());
}
public final boolean getOptionSortAscending() {
return option.get(optionKeySortAscending);
}
@Override
public void setOptionSortAscending(boolean ascending) {
setOptionSortAscending(ascending, true);
}
@Override
public void setOptionSortAscending(boolean ascending, boolean rebuild) {
sorters.setOptionSortAscending(ascending);
option.set(optionKeySortAscending, ascending);
rebuildRequired = true;
if (rebuild) {
build();
}
}
/**
* See {@link UserNavigationTree#build()}
*/
@SuppressFBWarnings("LO_APPENDED_STRING_IN_FORMAT_STRING") // no other way to do it
@Override
public void build() {
if (rebuildRequired) {
log.debug("rebuilding user navigation tree");
clear();
builder.build();
rebuildRequired = false;
if (log.isDebugEnabled()) {
Collection<?> t = this.getItemIds();
StringBuilder buf = new StringBuilder();
for (Object o : t) {
String itemCaption = getItemCaption(o);
buf.append(itemCaption);
buf.append(',');
}
log.debug(buf.toString());
}
} else {
log.debug("rebuild of user navigation tree is not required");
}
}
@Override
public void clear() {
removeAllItems();
}
public SortType getOptionSortType() {
return option.get(optionKeySortType);
}
@Override
public void setOptionKeySortType(@Nonnull SortType sortType) {
checkNotNull(sortType);
setOptionSortType(sortType, true);
}
@Override
public void setOptionSortType(SortType sortType, boolean rebuild) {
sorters.setOptionKeySortType(sortType);
option.set(optionKeySortType, sortType);
rebuildRequired = true;
if (rebuild) {
build();
}
}
public UserNavigationTreeBuilder getBuilder() {
return builder;
}
/**
* Returns true if the {@code node} is a leaf as far as this {@link DefaultUserNavigationTree} is concerned. It may
* be a leaf here, but not in the {@link #userSitemap}, depending on the setting of {@link #getOptionMaxDepth()}
*
* @param node
* @return
*/
public boolean isLeaf(UserSitemapNode node) {
return !areChildrenAllowed(node);
}
@Override
public int getOptionMaxDepth() {
return option.get(optionKeyMaximumDepth);
}
/**
* See {@link UserNavigationTree#setOptionMaxDepth(int)}
*/
@Override
public void setOptionMaxDepth(int maxDepth) {
setOptionMaxDepth(maxDepth, true);
}
/**
* See {@link UserNavigationTree#setOptionMaxDepth(int, boolean)}
*/
@Override
public void setOptionMaxDepth(int maxDepth, boolean rebuild) {
if (maxDepth > 0) {
option.set(optionKeyMaximumDepth, maxDepth);
build();
} else {
log.warn("Attempt to set max depth value to {}, but has been ignored. It must be greater than 0. ");
}
}
@Override
public void valueChange(Property.ValueChangeEvent event) {
if (!suppressValueChangeEvents) {
if (getValue() != null) {
String url = userSitemap.uri((UserSitemapNode) getValue());
navigator.navigateTo(url);
}
}
}
/**
* After a navigation change, select the appropriate node.
*
* @see KrailView for a description of the call sequence
*/
@Handler
public void afterViewChange(AfterViewChangeBusMessage busMessage) {
// TODO could this use the message instead - the order of change then will not matter??
UserSitemapNode selectedNode = navigator.getCurrentNode();
UserSitemapNode childNode = selectedNode;
UserSitemapNode parentNode = (UserSitemapNode) getParent(childNode);
while (parentNode != null) {
expandItem(parentNode);
parentNode = (UserSitemapNode) getParent(parentNode);
}
suppressValueChangeEvents = true;
log.debug("selecting node for uri '{}'", userSitemap.uri(selectedNode));
this.select(selectedNode);
suppressValueChangeEvents = false;
}
@Override
public Tree getTree() {
return this;
}
/**
* Although only {@link UserSitemap} labels (and therefore captions) have changed, the tree may need to be
* re-sorted to reflect the change in language, so it is easier just to rebuild the tree
*/
@Handler
public void labelsChanged(UserSitemapLabelChangeMessage busMessage) {
rebuildRequired = true;
build();
}
/**
* {@link UserSitemap} structure has changed, we need to rebuild
*/
@Handler
public void structureChanged(UserSitemapStructureChangeMessage busMessage) {
rebuildRequired = true;
build();
}
@Override
public Comparator<UserSitemapNode> getSortComparator() {
return sorters.getSortComparator();
}
public boolean isRebuildRequired() {
return rebuildRequired;
}
public void setRebuildRequired(boolean rebuildRequired) {
this.rebuildRequired = rebuildRequired;
}
@Nonnull
@Override
public Option getOption() {
return option;
}
@Override
public void optionValueChanged(Property.ValueChangeEvent event) {
rebuildRequired = true;
build();
}
}