/*
* Copyright (c) 2010-2012 Lockheed Martin Corporation
*
* 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 org.eurekastreams.web.client.ui.common.pagedlist;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eurekastreams.server.action.request.PageableRequest;
import org.eurekastreams.server.domain.PagedSet;
import org.eurekastreams.web.client.events.Observer;
import org.eurekastreams.web.client.events.PagerUpdatedEvent;
import org.eurekastreams.web.client.events.SwitchToFilterOnPagedFilterPanelEvent;
import org.eurekastreams.web.client.events.UpdateHistoryEvent;
import org.eurekastreams.web.client.events.UpdatedHistoryParametersEvent;
import org.eurekastreams.web.client.history.CreateUrlRequest;
import org.eurekastreams.web.client.model.Fetchable;
import org.eurekastreams.web.client.ui.Session;
import org.eurekastreams.web.client.ui.common.Pager;
import org.eurekastreams.web.client.ui.pages.master.StaticResourceBundle;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
/**
* This is a fairly complex control. Basically, it supports a series of
* StaticResourceBundle.INSTANCE.coreCss().filters() (which can be though of as data sets. These's data sets can be
* sorted and/or paged. Add sets by feeding it Fetchable models and a renderer for the individual items, and this
* control should take care of all the logic.
*
*/
public class PagedListPanel extends FlowPanel
{
/** URL parameter for the list ID. */
public static final String URL_PARAM_LIST_ID = "listId";
/** URL parameter for the filter name. */
public static final String URL_PARAM_FILTER = "listFilter";
/** URL parameter for the sort. */
public static final String URL_PARAM_SORT = "listSort";
/** List id (so we can have >1 on a page). */
private String listId = "list";
/** The current filter. */
private String currentFilter = "";
/** The current sort. */
private String currentSortKey = "";
/** Current pager start index. */
private Integer currentStartIndex = null;
/** Current pager end index. */
private Integer currentEndIndex = null;
/** An initial state is being stored awaiting the tabs to be added. */
private boolean storingInitialState = false;
/** The initial filter. */
private String initialFilter;
/** The initial sort. */
private String initialSortKey;
/** initial pager start index. */
private String initialStartIndex;
/** The renderers keyed by filter. */
@SuppressWarnings("unchecked")
private final Map<String, ItemRenderer> renderers = new HashMap<String, ItemRenderer>();
/** Requests keyed by filter. */
private final Map<String, HashMap<String, PageableRequest>> requests = // \n
new HashMap<String, HashMap<String, PageableRequest>>();
/** Links keyed by filter. */
private final Map<String, Anchor> filterLinks = new HashMap<String, Anchor>();
/** Fetchers keyed by filter. */
@SuppressWarnings("unchecked")
private final Map<String, Fetchable> fetchers = new HashMap<String, Fetchable>();
/** Sorters keyed by filter. */
private final Map<String, FlowPanel> sortPanels = new HashMap<String, FlowPanel>();
/** Sort Links keyed by filter and sort. */
private final Map<String, HashMap<String, Anchor>> sortLinks = new HashMap<String, HashMap<String, Anchor>>();
/** Collection of filters that have been loaded and the available sorts for each filter. */
private final Map<String, List<String>> loadedFilters = new HashMap<String, List<String>>();
/** Used to lay out the page; default is two columns. */
private PagedListRenderer pageRenderer = new TwoColumnPagedListRenderer();
/** The bottom pager. Pass in true to show the buttons. */
private final Pager bottomPager;
/** Navigation panel. */
private final FlowPanel navPanel;
/** Contains the items. */
private final FlowPanel renderContainer = new FlowPanel();
/** Contains the filter switchers. */
private final FlowPanel filterContainer = new FlowPanel();
/** Contains the sort switchers. */
private final FlowPanel sortContainer = new FlowPanel();
/** Waiting spinner. */
FlowPanel waitSpinner = new FlowPanel();
/**
* Default constructor.
*
* @param inListId
* the list id.
*/
public PagedListPanel(final String inListId)
{
this(inListId, null, null);
}
/**
* Constructor.
*
* @param inListId
* the list id.
* @param inContextParam
* Parameter to look for in the URL to determine if URL change events apply to this list.
* @param inContextParamValue
* Value of parameter to look for in the URL to determine if URL change events apply to this list.
*/
public PagedListPanel(final String inListId, final String inContextParam, final String inContextParamValue)
{
listId = inListId;
bottomPager = new Pager("filteredPager" + listId, true);
waitSpinner.addStyleName(StaticResourceBundle.INSTANCE.coreCss().waitSpinner());
renderContainer.addStyleName(StaticResourceBundle.INSTANCE.coreCss().renderContainer());
addStyleName(StaticResourceBundle.INSTANCE.coreCss().connectionMaster());
filterContainer.add(new Label("View:"));
navPanel = new FlowPanel();
navPanel.addStyleName(StaticResourceBundle.INSTANCE.coreCss().navpanel());
this.add(filterContainer);
navPanel.add(sortContainer);
this.add(navPanel);
filterContainer.addStyleName(StaticResourceBundle.INSTANCE.coreCss().options());
filterContainer.addStyleName(StaticResourceBundle.INSTANCE.coreCss().views());
sortContainer.addStyleName(StaticResourceBundle.INSTANCE.coreCss().options());
bottomPager.addStyleName(StaticResourceBundle.INSTANCE.coreCss().bottomPager());
this.add(waitSpinner);
this.add(renderContainer);
Session.getInstance().getEventBus().addObserver(PagerUpdatedEvent.class, new Observer<PagerUpdatedEvent>()
{
public void update(final PagerUpdatedEvent event)
{
if (event.getPager().getPagerId().equals("filteredPager" + listId)
&& event.getPager().getStartIndex() != currentStartIndex)
{
currentStartIndex = event.getPager().getStartIndex();
currentEndIndex = event.getPager().getEndIndex();
reload();
}
}
});
Session.getInstance()
.getEventBus()
.addObserver(SwitchToFilterOnPagedFilterPanelEvent.class,
new Observer<SwitchToFilterOnPagedFilterPanelEvent>()
{
public void update(final SwitchToFilterOnPagedFilterPanelEvent event)
{
// ignore events for other lists
if (!listId.equals(event.getListId()))
{
return;
}
updateStateIfChanged(event.getFilterName(), event.getSortKey(), null, null,
!event.isFromUrlChange());
}
});
Session.getInstance().getEventBus()
.addObserver(UpdatedHistoryParametersEvent.class, new Observer<UpdatedHistoryParametersEvent>()
{
public void update(final UpdatedHistoryParametersEvent event)
{
// determine if the history update pertains to this list
// If there's a list id, and it not ours, bail out. If there's no list id, then check for the
// "context parameter" (basically snoop on the URL to see if the tab we're on is showing); if
// present and it's not ours, bail out. Otherwise, we can't be sure if the update pertains to us
// or not, so we have to try to process it.
Map<String, String> parms = event.getParameters();
String eventListId = parms.get(URL_PARAM_LIST_ID);
if (eventListId == null)
{
if (inContextParam != null && inContextParamValue != null
&& !inContextParamValue.equals(parms.get(inContextParam)))
{
return;
}
}
else if (!listId.equals(eventListId))
{
return;
}
// store state if tabs not added yet
if (loadedFilters.isEmpty())
{
storingInitialState = true;
initialFilter = parms.get(URL_PARAM_FILTER);
initialSortKey = parms.get(URL_PARAM_SORT);
initialStartIndex = parms.get(Pager.URL_PARAM_START_INDEX);
}
// only handle event if initialized
else
{
updateStateIfChanged(parms.get(URL_PARAM_FILTER), parms.get(URL_PARAM_SORT),
parms.get(Pager.URL_PARAM_START_INDEX), parms.get(Pager.URL_PARAM_END_INDEX), // \n
false);
}
}
}, true);
this.add(bottomPager);
FlowPanel clear = new FlowPanel();
clear.addStyleName(StaticResourceBundle.INSTANCE.coreCss().clear());
this.add(clear);
}
/**
* Constructor.
*
* @param inListId
* the list id.
* @param inPageRenderer
* page layout renderer.
*/
public PagedListPanel(final String inListId, final PagedListRenderer inPageRenderer)
{
this(inListId, null, null);
pageRenderer = inPageRenderer;
}
/**
* Constructor.
*
* @param inListId
* the list id.
* @param inPageRenderer
* page layout renderer.
* @param inContextParam
* Parameter to look for in the URL to determine if URL change events apply to this list.
* @param inContextParamValue
* Value of parameter to look for in the URL to determine if URL change events apply to this list.
*/
public PagedListPanel(final String inListId, final PagedListRenderer inPageRenderer, final String inContextParam,
final String inContextParamValue)
{
this(inListId, inContextParam, inContextParamValue);
pageRenderer = inPageRenderer;
}
/**
* Updates the state of the widget (loading data, etc.) if the current state does not match the desired state.
*
* @param inFilter
* Requested filter name.
* @param inSort
* Requested sort key.
* @param inStartIndex
* Requested start index.
* @param inEndIndex
* Requested end index.
* @param mayUpdateUrl
* If the URL is allowed to be updated (e.g. don't update URL in response to a URL change, etc.).
* @return Whether the state was updated.
*/
private boolean updateStateIfChanged(final String inFilter, final String inSort, final String inStartIndex,
final String inEndIndex, final boolean mayUpdateUrl)
{
String filter = inFilter;
String sort = inSort;
Integer start = normalizeIndex(inStartIndex);
// For missing parameters, replace with the default values
if (filter == null)
{
filter = (String) loadedFilters.keySet().toArray()[0];
// treat sort as a child of filter, so if filter is missing, ignore any sort specified
sort = null;
}
// reject unknown filters (maybe extraneous URL parameters or we caught an event that wasn't for us)
else if (!loadedFilters.containsKey(filter))
{
return false;
}
if (sort == null)
{
sort = loadedFilters.get(filter).get(0);
}
// reject unknown sorts (maybe extraneous URL parameters or we caught an event that wasn't for us)
else if (!loadedFilters.get(filter).contains(sort))
{
return false;
}
if (start == null)
{
start = 0;
}
// determine if state is different
if (currentFilter == null || !currentFilter.equals(filter))
{
currentFilter = filter;
currentSortKey = sort;
currentStartIndex = start;
}
else if (!currentSortKey.equals(sort))
{
currentSortKey = sort;
currentStartIndex = start;
}
else if (!currentStartIndex.equals(start))
{
currentStartIndex = start;
}
else
{
return false;
}
currentStartIndex = start;
currentEndIndex = start + bottomPager.getPageSize() - 1;
bottomPager.setStartIndex(currentStartIndex);
bottomPager.setEndIndex(currentEndIndex);
Session.getInstance().getEventBus().notifyObservers(new PagerUpdatedEvent(bottomPager));
reload();
// update the URL when allowed (if from user action on a link, but not if from a history/URL change (otherwise
// we'd be re-updating the URL which really messes up the back button).
if (mayUpdateUrl)
{
HashMap<String, String> params = new HashMap<String, String>();
params.put(URL_PARAM_LIST_ID, listId);
params.put(URL_PARAM_FILTER, currentFilter);
params.put(URL_PARAM_SORT, currentSortKey);
params.put(Pager.URL_PARAM_START_INDEX, currentStartIndex.toString());
params.put(Pager.URL_PARAM_END_INDEX, currentEndIndex.toString());
Session.getInstance().getEventBus()
.notifyObservers(new UpdateHistoryEvent(new CreateUrlRequest(params, false)));
}
return true;
}
/**
* Invalidates the current state of the control so the next parameter update will force a refresh.
*/
public void invalidateState()
{
currentFilter = null;
}
/**
* Normalizes a start/end index value.
*
* @param value
* String form.
* @return Numeric form (null if not present/valid).
*/
private Integer normalizeIndex(final String value)
{
if (value == null || value.isEmpty())
{
return null;
}
try
{
return Integer.valueOf(value);
}
catch (NumberFormatException ex)
{
return null;
}
}
/**
* Reload the panel to a default filter.
*
* @param filter
* The default filter to use after reset.
*/
public void reload(final String filter)
{
currentFilter = filter;
reload();
}
/**
* Reload the panel.
*/
public void reload()
{
for (Anchor filterLink : filterLinks.values())
{
filterLink.removeStyleName(StaticResourceBundle.INSTANCE.coreCss().active());
}
if (sortLinks.get(currentFilter) != null)
{
for (Anchor sortLink : sortLinks.get(currentFilter).values())
{
sortLink.removeStyleName(StaticResourceBundle.INSTANCE.coreCss().active());
}
if (sortLinks.get(currentFilter).get(currentSortKey) != null)
{
sortLinks.get(currentFilter).get(currentSortKey)
.addStyleName(StaticResourceBundle.INSTANCE.coreCss().active());
}
}
if (sortPanels.get(currentFilter) != null)
{
sortContainer.clear();
sortContainer.add(sortPanels.get(currentFilter));
}
else
{
sortContainer.clear();
}
filterLinks.get(currentFilter).addStyleName(StaticResourceBundle.INSTANCE.coreCss().active());
refreshData();
}
/**
* Causes the data for the current filter and sort to be refreshed (via fetching from the model).
*/
@SuppressWarnings("unchecked")
public void refreshData()
{
waitSpinner.setVisible(true);
PageableRequest request = requests.get(currentFilter).get(currentSortKey);
request.setStartIndex(currentStartIndex);
request.setEndIndex(currentEndIndex);
fetchers.get(currentFilter).fetch(request, false);
}
/**
* Add a filter w/o a sort.
*
* @param name
* name of the filter.
* @param fetchable
* the fetchable model.
* @param renderer
* the renderer.
* @param request
* the request.
*/
@SuppressWarnings("unchecked")
public void addSet(final String name, final Fetchable fetchable, final ItemRenderer renderer,
final PageableRequest request)
{
addSet(name, fetchable, renderer, request, "");
}
/**
* Add a filter w/o a sort.
*
* @param name
* name of the filter.
* @param fetchable
* the fetchable model.
* @param renderer
* the renderer.
* @param request
* the request.
* @param sortKey
* the sort key.
*/
@SuppressWarnings("unchecked")
public void addSet(final String name, final Fetchable fetchable, final ItemRenderer renderer,
final PageableRequest request, final String sortKey)
{
if (requests.get(name) == null)
{
requests.put(name, new HashMap<String, PageableRequest>());
renderers.put(name, renderer);
fetchers.put(name, fetchable);
Anchor filterLink = new Anchor(name);
filterLink.addStyleName(StaticResourceBundle.INSTANCE.coreCss().connectionFilterButton());
filterLink.addClickHandler(new ClickHandler()
{
public void onClick(final ClickEvent event)
{
updateStateIfChanged(name, sortKey, null, null, true);
}
});
filterLinks.put(name, filterLink);
filterContainer.add(filterLink);
sortLinks.put(name, new HashMap<String, Anchor>());
}
requests.get(name).put(sortKey, request);
if (!sortKey.equals(""))
{
if (sortPanels.get(name) == null)
{
FlowPanel sortPanel = new FlowPanel();
sortPanel.add(new Label("Sort: "));
sortPanels.put(name, sortPanel);
}
Anchor sortLink = new Anchor(sortKey);
sortLink.addStyleName(StaticResourceBundle.INSTANCE.coreCss().connectionFilterButton());
sortLink.addClickHandler(new ClickHandler()
{
public void onClick(final ClickEvent event)
{
updateStateIfChanged(name, sortKey, null, null, true);
}
});
sortPanels.get(name).add(sortLink);
sortLinks.get(name).put(sortKey, sortLink);
}
if (loadedFilters.containsKey(name))
{
loadedFilters.get(name).add(sortKey);
}
else
{
List sorts = new ArrayList();
sorts.add(sortKey);
loadedFilters.put(name, sorts);
}
// attempt to apply the initial state
// This tab may be the one that satisfies the stored initial state. So we try to apply the state; if it doesn't
// fit, then nothing happens and the initial state will still be stored for the next tab added. If it worked,
// then we clear the initial state.
if (storingInitialState)
{
if (updateStateIfChanged(initialFilter, initialSortKey, initialStartIndex, null, false))
{
storingInitialState = false;
}
}
}
/**
* Updates the request for a given set that has no sort key.
*
* @param name
* The name of the set to update.
* @param request
* The updated request.
*/
public void updateSetRequest(final String name, final PageableRequest request)
{
updateSetRequest(name, request, "");
}
/**
* Updates the request for a given set.
*
* @param name
* The name of the set to update.
* @param request
* The updated request.
* @param sortKey
* The sort key of the set to update.
*/
public void updateSetRequest(final String name, final PageableRequest request, final String sortKey)
{
if (requests.get(name) != null)
{
requests.get(name).remove(sortKey);
requests.get(name).put(sortKey, request);
}
}
/**
* Render the panel.
*
* @param <T>
* the type of item.
* @param items
* the items.
* @param noItemsMessage
* the message to display when nothing is there.
*/
@SuppressWarnings("unchecked")
public <T extends Serializable> void render(final PagedSet<T> items, final String noItemsMessage)
{
ItemRenderer render = renderers.get(currentFilter);
renderContainer.clear();
if (items.getTotal() > 0)
{
removeStyleName(StaticResourceBundle.INSTANCE.coreCss().emptyList());
}
else
{
addStyleName(StaticResourceBundle.INSTANCE.coreCss().emptyList());
}
pageRenderer.render(renderContainer, render, items, noItemsMessage);
bottomPager.setTotal(items.getTotal());
waitSpinner.setVisible(false);
}
/**
* @return The current filter being displayed.
*/
public String getCurrentFilter()
{
return currentFilter;
}
/**
* Sets the text displayed on the filter link.
*
* @param name
* The name of the filter.
* @param title
* The text to display.
*/
public void setFilterTitle(final String name, final String title)
{
filterLinks.get(name).setText(title);
}
}