package lt.inventi.wicket.component.breadcrumb;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.Page;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.request.component.IRequestablePage;
import lt.inventi.wicket.component.breadcrumb.collapse.DisplayedBreadcrumb;
import lt.inventi.wicket.component.breadcrumb.collapse.IBreadcrumbCollapser;
import lt.inventi.wicket.component.breadcrumb.collapse.NoopCollapser;
import lt.inventi.wicket.component.breadcrumb.collapse.RepeatingBreadcrumbCollapser;
import lt.inventi.wicket.component.breadcrumb.hierarchy.IBreadcrumbHierarchy;
public final class BreadcrumbsSettings {
private static final MetaDataKey<BreadcrumbsSettings> KEY = new MetaDataKey<BreadcrumbsSettings>(){ /* empty */ };
/**
* PRIVATE API
*/
static boolean useStatefulBreadcrumbLinks() {
return Application.get().getMetaData(KEY).useStatefulBreadcrumbLinks;
}
static IBreadcrumbCollapser getBreadcrumbsCollapser() {
return Application.get().getMetaData(KEY).collapser;
}
static IBreadcrumbHierarchy getBreadcrumbsHierarchy() {
return Application.get().getMetaData(KEY).hierarchy;
}
static boolean isCompactionEnabled() {
return Application.get().getMetaData(KEY).compactionEnabled;
}
private IBreadcrumbPageFilter pageFilter = new IBreadcrumbPageFilter() {
@Override
public boolean shouldCreateBreadcrumbFor(IRequestablePage page) {
if (page instanceof Page) {
return ((Page) page).visitChildren(BreadcrumbsPanel.class).hasNext();
}
return false;
}
};
private Class<? extends BookmarkablePageLink<?>> linkTypeToDecorate = null;
private Class<? extends BookmarkablePageLink<?>> linkTypeToNotDecorate = null;
private boolean useStatefulBreadcrumbLinks;
private Integer timesToRepeatBeforeCollapse;
private IBreadcrumbCollapser collapser;
private boolean compactionEnabled;
private IComponentBreadcrumbTitleProvider localizedTitleProvider = new LocalizedTitleProvider("breadcrumb");
private IBreadcrumbHierarchy hierarchy;
/**
* Will create breadcrumbs only for pages annotated with the specified
* annotation type.
* <p>
* By default breadcrumbs will be created for all of the pages containing an
* instance of {@link BreadcrumbsPanel}.
* <p>
* Useful if you already use an annotation to distinguish pages which
* require bound sessions (like {@code RequiresAuthentication}).
*
* @param annotation
* type of the annotation
* @return current settings for chaining
*/
public BreadcrumbsSettings forPagesAnnotatedWith(final Class<? extends Annotation> annotation) {
this.pageFilter = new IBreadcrumbPageFilter() {
@Override
public boolean shouldCreateBreadcrumbFor(IRequestablePage page) {
return page.getClass().getAnnotation(annotation) != null;
}
};
return this;
}
/**
* Will create breadcrumbs only for pages assignable to the specified type.
* <p>
* By default breadcrumbs will be created for all of the pages containing an
* instance of {@link BreadcrumbsPanel}.
* <p>
* Useful if you have a common base class for pages requiring breadcrumbs.
* You also may use {@code IBreadcrumbsOperations} as a parameter.
*
* @param type
* type of the page
* @return current settings for chaining
*/
public BreadcrumbsSettings forInstancesOf(final Class<?> type) {
this.pageFilter = new IBreadcrumbPageFilter() {
@Override
public boolean shouldCreateBreadcrumbFor(IRequestablePage page) {
return type.isAssignableFrom(page.getClass());
}
};
return this;
}
/**
* @return current settings for chaining
*/
public BreadcrumbsSettings withStaticHierarchy(IBreadcrumbHierarchy newHierarchy) {
this.hierarchy = newHierarchy;
return this;
}
/**
* If set, all instances of {@link BookmarkablePageLink} on pages which
* support breadcrumbs (specified by {@link #forPagesAnnotatedWith(Class)}
* will be decorated with breadcrumb trail parameter. This way bookmarkable
* links will automatically extend the breadcrumb trail.
* <p>
* In order to create bookmarkable links which <strong>do not</strong>
* extend the trail, use {@link NonTrailingBookmarkablePageLink}.
*
* @return current settings for chaining
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public BreadcrumbsSettings withDecoratedBookmarkableLinks() {
this.linkTypeToDecorate = (Class) BookmarkablePageLink.class;
this.linkTypeToNotDecorate = (Class) NonTrailingBookmarkablePageLink.class;
return this;
}
/**
* If set, stateful links will be used in the {@code BreadcrumbsPanel}.
* <p>
* This must be used in tests if you want to click on breadcrumb links using
* {@code WicketTester}.
*
* @return current settings for chaining
*/
public BreadcrumbsSettings withStatefulBreadcrumbLinks() {
this.useStatefulBreadcrumbLinks = true;
return this;
}
/**
* Sets the localization key to be used in order to find breadcrumb title
* for the page. By default the key is set to <b>breadcrumb</b>.
* <p>
* Localized string will be resolved according to the Wicket's localization
* rules starting from the {@code Page} the breadcrumb points to.
*
* @param localizationKey
* to be used in order to find breadcrumb title for the page
* @return current settings for chaining
*/
public BreadcrumbsSettings useKeyForBreadcrumbTitle(String localizationKey) {
if (localizationKey == null || localizationKey.isEmpty()) {
throw new IllegalArgumentException("Localization key for breadcrumbs cannot be empty!");
}
this.localizedTitleProvider = new LocalizedTitleProvider(localizationKey);
return this;
}
/**
* If set to a positive value, will force the {@code BreadcrumbsPanel} to
* collapse breadcrumbs encountered more than {@code times} times.
* <p>
* For example, if {@code times} is 2 and you have a breadcrumb trail
* consisting of
*
* <pre>
* First / Second / First / Second
* </pre>
*
* and the next page is <b>First</b>, the breadcrumb trail will become
*
* <pre>
* ... / First
* </pre>
*
* Where {@code ...} can be expanded to look at the collapsed part of the
* trail.
*
* <p>
* <b>If {@link #compact()} is set, collapse won't happen as all of the
* repeated breadcrumbs will be instantly compacted</b>
* </p>
*
* @param times
* the breadcrumb must be encountered in a single trail in order
* to be collapsed
* @return current settings for chaining
*/
public BreadcrumbsSettings collapseWhenRepeated(int times) {
if (times < 1) {
throw new IllegalArgumentException("Cannot collapse when repeated " + times + " times, must be positive!");
}
this.timesToRepeatBeforeCollapse = times;
return this;
}
/**
* Will force the {@code BreadcrumbsPanel} to compact breadcrumbs encountered more than once.
* <p>
* For example, if {@link #compact} is set and you have a breadcrumb trail
* consisting of
*
* <pre>
* First / Second / Third
* </pre>
*
* and the next page is <b>First</b>, the breadcrumb trail will become
*
* <pre>
* First
* </pre>
*
* i.e. all of the crumbs between the repeated pages will be discarded.
*
* <p>
* <b>This effectivly discards any {@link #collapseWhenRepeated(int)} settings.</b>
* </p>
*
* @return current settings for chaining
*/
public BreadcrumbsSettings compact() {
this.compactionEnabled = true;
return this;
}
public void install(Application app) {
if (this.timesToRepeatBeforeCollapse != null && this.timesToRepeatBeforeCollapse > 0) {
this.collapser = new RepeatingBreadcrumbCollapser(timesToRepeatBeforeCollapse, new TypeBreadcrumbEquality());
} else {
this.collapser = new NoopCollapser();
}
if (this.hierarchy == null) {
this.hierarchy = new NoHierarchy();
}
app.setMetaData(KEY, this);
app.getComponentPreOnBeforeRenderListeners().add(
new BreadcrumbTrailExtendingListener(pageFilter, new DefaultTitleProvider(localizedTitleProvider)));
if (linkTypeToDecorate != null) {
app.getComponentInitializationListeners().add(
new BookmarkableBreadcrumbPageInitializationListener(pageFilter, linkTypeToDecorate, linkTypeToNotDecorate));
}
}
private static class DefaultTitleProvider implements IComponentBreadcrumbTitleProvider {
private IComponentBreadcrumbTitleProvider next;
DefaultTitleProvider(IComponentBreadcrumbTitleProvider next) {
this.next = next;
}
@Override
public BreadcrumbTitle getBreadcrumbTitle(Component c) {
if (c instanceof IBreadcrumbTitleProvider) {
return ((IBreadcrumbTitleProvider) c).getBreadcrumbTitle();
}
if (c instanceof IBreadcrumbTitleModelProvider) {
return new BreadcrumbTitle(((IBreadcrumbTitleModelProvider) c).getBreadcrumbTitleModel());
}
return next.getBreadcrumbTitle(c);
}
}
private static class LocalizedTitleProvider implements IComponentBreadcrumbTitleProvider {
private final String key;
LocalizedTitleProvider(String key) {
this.key = key;
}
@Override
public BreadcrumbTitle getBreadcrumbTitle(Component c) {
return new BreadcrumbTitle(new StringResourceModel(key, c, c.getDefaultModel()));
}
}
private static class NoHierarchy implements IBreadcrumbHierarchy {
@Override
public List<DisplayedBreadcrumb> restoreMissingHierarchy(List<Breadcrumb> originalCrumbs) {
return Collections.emptyList();
}
}
}