blob: 50e64f33ccc5bc4d686cebfd27916061bbff1acc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.group.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
/**
* A thread-safe implementation of {@link IRowGroupModel} which is optimised for
* larger data-sets (it should cope with at least 10k rows spread across 2-300
* groups).
*
* @author Stefan Bolton
*
* @param <T>
*/
public class RowGroupModel<T> implements IRowGroupModel<T> {
// A map of top-level group names-to-RowGroups.
private final Map<String, IRowGroup<T>> namesToGroups;
// A performance cache of rows-to-row group nodes. Kept directly in sync
// with the model.
private final Map<T, IRowGroup<T>> rowToGroups;
// Row group change listeners.
private final Set<IRowGroupModelListener> listeners;
// A convenience cache of row indexes to/from row objects. Items are added
// on demand
// from the layers as-and-when they are needed.
private final RowCache<T> rowCache;
// For big model changes it can be easier to suppress model change
// notifications and fire a single one.
private boolean suppressNoficiations;
public RowGroupModel() {
this.rowToGroups = new ConcurrentHashMap<T, IRowGroup<T>>();
this.namesToGroups = new ConcurrentHashMap<String, IRowGroup<T>>();
this.rowCache = new RowCache<T>();
this.listeners = new HashSet<IRowGroupModelListener>();
this.suppressNoficiations = false;
}
@Override
public T getRowFromIndexCache(final int rowIndex) {
return this.rowCache.getRowFromIndexCache(rowIndex);
}
@Override
public int getIndexFromRowCache(final T row) {
return this.rowCache.getIndexFromRowCache(row);
};
@Override
public void invalidateIndexCache() {
this.rowCache.invalidateIndexCache();
}
@Override
public void setDataProvider(IRowDataProvider<T> dataProvider) {
this.rowCache.setDataProvider(dataProvider);
}
@Override
public IRowDataProvider<T> getDataProvider() {
return this.rowCache.getDataProvider();
}
/**
* <p>
* Notify any {@link IRowGroupModelListener}s that something in the model
* has changed.
* </p>
*/
@Override
public void notifyListeners() {
invalidateIndexCache();
if (!this.suppressNoficiations) {
for (IRowGroupModelListener listener : this.listeners) {
listener.rowGroupModelChanged();
}
}
}
/**
* Set to true to stop model change notifications.
*
* @param suppressNoficiations
*/
public void setSuppressNoficiations(boolean suppressNoficiations) {
this.suppressNoficiations = suppressNoficiations;
}
public boolean isSuppressNoficiations() {
return this.suppressNoficiations;
}
void addMemberRow(final T row, final RowGroup<T> rowGroup) {
this.rowToGroups.put(row, rowGroup);
}
void removeMemberRow(final T row) {
this.rowToGroups.remove(row);
}
@Override
public void addRowGroups(final List<IRowGroup<T>> rowGroups) {
// Add the group into the model now.
for (IRowGroup<T> rowGroup : rowGroups) {
this.namesToGroups.put(rowGroup.getGroupName(), rowGroup);
}
notifyListeners();
}
@Override
public boolean addRowGroup(final IRowGroup<T> rowGroup) {
// Only allow unique names.
if (this.namesToGroups.containsKey(rowGroup.getGroupName())) {
return false;
}
// Add the group into the model now.
this.namesToGroups.put(rowGroup.getGroupName(), rowGroup);
notifyListeners();
return true;
}
@Override
public boolean removeRowGroup(final IRowGroup<T> rowGroup) {
boolean removed = this.namesToGroups.containsKey(rowGroup.getGroupName());
if (removed) {
// Remove the group itself now.
this.namesToGroups.remove(rowGroup.getGroupName());
notifyListeners();
}
return removed;
}
@Override
public List<IRowGroup<T>> getRowGroups() {
return Collections.unmodifiableList(new ArrayList<IRowGroup<T>>(
this.namesToGroups.values()));
}
@Override
public IRowGroup<T> getRowGroupForName(final String groupName) {
return this.namesToGroups.get(groupName);
}
@Override
public IRowGroup<T> getRowGroupForRow(T row) {
if (this.rowToGroups.containsKey(row)) {
return getUltimateParent(this.rowToGroups.get(row));
}
return null;
}
private IRowGroup<T> getUltimateParent(IRowGroup<T> group) {
return (group.getParentGroup() == null ? group
: getUltimateParent(group.getParentGroup()));
}
@Override
public boolean isEmpty() {
return this.namesToGroups.isEmpty();
}
@Override
public void clear() {
this.namesToGroups.clear();
this.rowToGroups.clear();
this.rowCache.invalidateIndexCache();
notifyListeners();
}
@Override
public void registerRowGroupModelListener(
final IRowGroupModelListener listener) {
this.listeners.add(listener);
}
@Override
public void unregisterRowGroupModelListener(
final IRowGroupModelListener listener) {
this.listeners.remove(listener);
}
@Override
public void saveState(String prefix, Properties properties) {
// no additional states that need to be saved
}
@Override
public void loadState(String prefix, Properties properties) {
// no additional states that need to be saved
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(" ===== Row Group Model ==== \n"); //$NON-NLS-1$
synchronized (this.namesToGroups) {
for (IRowGroup<T> rowGroup : this.namesToGroups.values()) {
sb.append(((RowGroup<T>) rowGroup).toString());
}
}
return sb.toString();
}
private class RowCache<E> {
private IRowDataProvider<E> dataProvider;
private final Map<Integer, E> indexesToRows;
private final Map<E, Integer> rowsToIndexes;
public RowCache() {
this.indexesToRows = new LinkedHashMap<Integer, E>();
this.rowsToIndexes = new LinkedHashMap<E, Integer>();
}
public IRowDataProvider<E> getDataProvider() {
return this.dataProvider;
}
public void setDataProvider(IRowDataProvider<E> dataProvider) {
this.dataProvider = dataProvider;
}
public E getRowFromIndexCache(final int rowIndex) {
if (this.indexesToRows.containsKey(rowIndex)) {
return this.indexesToRows.get(rowIndex);
}
final E row = this.dataProvider.getRowObject(rowIndex);
// If the row we want to use is already in use, then clear the
// cache.
if (this.rowsToIndexes.containsKey(row)) {
invalidateIndexCache();
}
this.rowsToIndexes.put(row, rowIndex);
this.indexesToRows.put(rowIndex, row);
return row;
}
public int getIndexFromRowCache(final E row) {
if (this.rowsToIndexes.containsKey(row)) {
return this.rowsToIndexes.get(row);
}
final int rowIndex = this.dataProvider.indexOfRowObject(row);
// If the index we want to use is already in use, then clear the
// cache.
if (this.indexesToRows.containsKey(rowIndex)) {
invalidateIndexCache();
}
this.indexesToRows.put(rowIndex, row);
this.rowsToIndexes.put(row, rowIndex);
return rowIndex;
}
public void invalidateIndexCache() {
this.indexesToRows.clear();
this.rowsToIndexes.clear();
}
}
}