package fr.openwide.core.wicket.more.ajax;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.AjaxRequestTarget.AbstractListener;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.repeater.RefreshingView;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.markup.repeater.ReuseIfModelsEqualStrategy;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.visit.IVisitFilter;
import org.apache.wicket.util.visit.Visits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import fr.openwide.core.wicket.more.condition.Condition;
import fr.openwide.core.wicket.more.markup.html.template.js.jquery.plugins.bootstrap.modal.component.IAjaxModalPopupPanel;
import fr.openwide.core.wicket.more.markup.repeater.IRefreshableOnDemandRepeater;
import fr.openwide.core.wicket.more.util.visit.VisitFilters;
import fr.openwide.core.wicket.more.util.visit.Visitors;
public final class AjaxListeners {
private static final Logger LOGGER = LoggerFactory.getLogger(AjaxListeners.class);
private AjaxListeners() {
}
public static void add(AjaxRequestTarget target, AjaxRequestTarget.AbstractListener ... listeners) {
add(target, Arrays.asList(listeners));
}
public static void add(AjaxRequestTarget target, Iterable<? extends AjaxRequestTarget.AbstractListener> listeners) {
for (AjaxRequestTarget.AbstractListener listener : listeners) {
target.addListener(listener);
}
}
public static void add(AjaxRequestTarget target, Map<Collection<AbstractListener>, Condition> listeners) {
for (Entry<Collection<AbstractListener>, Condition> listener : listeners.entrySet()) {
if (listener.getValue().applies()) {
add(target, listener.getKey());
}
}
}
public static void addChildrenIfVisible(final AjaxRequestTarget target, MarkupContainer parent,
Class<?> childCriteria) {
Args.notNull(parent, "parent");
Args.notNull(childCriteria, "childCriteria");
Visits.visitChildren(
parent, Visitors.addToTarget(target),
VisitFilters.every(VisitFilters.including(childCriteria), VisitFilters.renderedComponents())
);
}
public static AjaxRequestTarget.AbstractListener refreshPage() {
return new SerializableListener() {
private static final long serialVersionUID = 1L;
@Override
public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
//target.add(target.getPage()); // Won't work, since the handler already decided started to respond without redirecting when this method is called
throw new RestartResponseException(target.getPage());
}
};
}
/**
* Adds or remove items of the given refreshing view using javascript, without refreshing any of the existing items
* that were not removed nor added during this request.
*
* <p><strong>WARNING:</strong> the repeater must ensure that its items are reused after a refresh. This means in
* particular that callers should make sure to call
* {@link RefreshingView#setItemReuseStrategy(org.apache.wicket.markup.repeater.IItemReuseStrategy)} on their
* {@link RefreshingView} with something like {@link ReuseIfModelsEqualStrategy#getInstance()} as a parameter, or
* to call {@link ListView#setReuseItems(boolean)} on their {@link ListView}.
*
* <p><strong>WARNING:</strong> removing markup of removed items will only work for {@link ListView}s and
* {@link RefreshingView}. Instances of {@link RepeatingView} (or subclasses) do not allow detection of the
* removed items.
*
* <p><strong>WARNING:</strong> added items will be added as last child of the repeater's parent. If it's
* not what you want, you may simply wrap the repeater in a {@link WebMarkupContainer} whose single child is
* the repeater.
*/
public static AjaxRequestTarget.AbstractListener refreshNewAndRemovedItems(final IRefreshableOnDemandRepeater repeater) {
if (!repeater.getParent().getOutputMarkupId()) {
LOGGER.warn("Trying to update new and removed items on a repeater whose parent does not"
+ " output its markup id. This is likely to fail on the client side. Repeater: {}", repeater);
}
return new RefreshNewAndRemovedItemsListener(repeater);
}
private static class RefreshNewAndRemovedItemsListener extends SerializableListener {
private static final long serialVersionUID = 1L;
private IRefreshableOnDemandRepeater repeater;
public RefreshNewAndRemovedItemsListener(IRefreshableOnDemandRepeater repeater) {
this.repeater = repeater;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!getClass().equals(obj.getClass())) {
return false;
}
RefreshNewAndRemovedItemsListener other = (RefreshNewAndRemovedItemsListener) obj;
return new EqualsBuilder().append(repeater, other.repeater).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(repeater).toHashCode();
}
@Override
public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
if (willUpdateRepeater(map)) {
// Skip refresh, since the whole repeater will be rendered anyway
return;
}
Set<Component> itemsBefore = Sets.newLinkedHashSet();
Set<Component> itemsAfter = Sets.newLinkedHashSet();
Iterators.addAll(itemsBefore, repeater.iterator());
repeater.refreshItems();
Iterators.addAll(itemsAfter, repeater.iterator());
/*
* Remove markup of components that were removed from the repeater when calling beforeRender().
*
* This will only work for self-populating components, such as ListViews or RefreshingViews.
* Others, such as an instance of RepeatingView (not a subclass), will have had their children
* removed before the call to refreshItems, so we can't possibly know these children
* existed in the first place.
*/
for (Component removedItem : Sets.difference(itemsBefore, itemsAfter)) {
if (!removedItem.getOutputMarkupId()) {
LOGGER.warn("Trying to remove a repeater item that does not"
+ " output its markup id. This is likely to fail on the client side."
+ " Repeater: {}, removed item : {}",
repeater, removedItem);
}
target.prependJavaScript(
String.format(
"Wicket.$('%s').remove();",
removedItem.getMarkupId()
)
);
}
for (Component itemAfter : repeater) {
if (!itemAfter.hasBeenRendered() && itemAfter.determineVisibility()) {
// First-time rendering for this item: we should add it to the markup.
target.prependJavaScript(
String.format(
"var item=document.createElement('%s');item.id='%s';" +
"Wicket.$('%s').appendChild(item);",
"tr", itemAfter.getMarkupId(), repeater.getParent().getMarkupId()
)
);
target.add(itemAfter);
}
}
}
private boolean willUpdateRepeater(Map<String, Component> map) {
Component cursor = repeater.getParent();
while (cursor != null) {
if (map.containsValue(cursor)) {
return true;
}
cursor = cursor.getParent();
}
return false;
}
};
/**
* @return A listener that will trigger the refresh of every given component.
*/
public static AjaxRequestTarget.AbstractListener refresh(Component first, Component ... rest) {
final List<Component> list = Lists.asList(first, rest);
return new SerializableListener() {
private static final long serialVersionUID = 1L;
@Override
public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
for (Component component : list) {
target.add(component);
}
}
};
}
/**
* @return A listener that will trigger the refresh of every component in the page of the given class(es).
*/
@SafeVarargs
public static AjaxRequestTarget.AbstractListener refresh(Class<? extends Component> first,
Class<? extends Component> ... rest) {
return refresh(VisitFilters.including(first, rest));
}
public static AjaxRequestTarget.AbstractListener refresh(final IVisitFilter ... additiveFilters) {
return new SerializableListener() {
private static final long serialVersionUID = 1L;
@Override
public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
Visits.visitChildren(
target.getPage(),
Visitors.addToTarget(target),
VisitFilters.every(additiveFilters)
);
}
};
}
/**
* @deprecated Use {@link #refreshChildren(MarkupContainer, Class, Class...)} instead.
*/
@SafeVarargs
public static AjaxRequestTarget.AbstractListener refresh(final MarkupContainer parent,
Class<? extends Component> first, Class<? extends Component> ... rest) {
return refreshChildren(parent, first, rest);
}
@SafeVarargs
public static AjaxRequestTarget.AbstractListener refreshChildren(final MarkupContainer parent,
Class<? extends Component> first, Class<? extends Component> ... rest) {
return refreshChildren(parent, VisitFilters.including(first, rest));
}
public static AjaxRequestTarget.AbstractListener refreshChildren(final MarkupContainer parent,
IVisitFilter ... additiveFilters) {
final IVisitFilter filter = VisitFilters.every(additiveFilters);
return new SerializableListener() {
private static final long serialVersionUID = 1L;
@Override
public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
Visits.visitChildren(
parent,
Visitors.addToTarget(target),
filter
);
}
};
}
/**
* @return A listener that will trigger the refresh of every component in the page
* of the given class(es), <strong>except <code>self</code> and components in {@link IAjaxModalPopupPanel modals}</strong>.
*/
@SafeVarargs
public static <T extends Component> AjaxRequestTarget.AbstractListener refreshOthersNotInAjaxModals(final T self,
Class<? extends T> first, Class<? extends T> ... rest) {
Args.notNull(self, "self");
return refresh(
VisitFilters.every(
VisitFilters.renderedComponents(),
VisitFilters.including(first, rest),
VisitFilters.downToIncluding(IAjaxModalPopupPanel.class),
VisitFilters.excluding(self)
)
);
}
public static ImmutableSet.Builder<AjaxRequestTarget.AbstractListener> chain() {
return ImmutableSet.builder();
}
public static ImmutableMap.Builder<Collection<AjaxRequestTarget.AbstractListener>, Condition> chainCondition() {
return ImmutableMap.builder();
}
}