blob: ad2f26432f597217b4c62f28d9136537d03b3ff4 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2012, 2021 Stephan Wahlbrink 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, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.ui.datafilter;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.osgi.util.NLS;
import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.status.ProgressMonitor;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.jcommons.ts.core.SystemRunnable;
import org.eclipse.statet.jcommons.ts.core.Tool;
import org.eclipse.statet.jcommons.ts.core.ToolRunnable;
import org.eclipse.statet.jcommons.ts.core.ToolService;
import org.eclipse.statet.internal.r.ui.dataeditor.RDataTableContentDescription;
import org.eclipse.statet.r.ui.dataeditor.RDataTableColumn;
import org.eclipse.statet.r.ui.dataeditor.RDataTableVariable;
import org.eclipse.statet.rj.data.RFactorStore;
import org.eclipse.statet.rj.data.UnexpectedRDataException;
import org.eclipse.statet.rj.ts.core.RToolService;
@NonNullByDefault
public class FilterSet {
private final static int POST_DELAY= 400;
private final static int STD_DELAY= 1;
private final static int NO_DELAY= 2;
private @Nullable RDataTableContentDescription input;
private int inFilterUpdate;
private boolean isInputFilterUpdate;
private final Object updateLock= new Object();
private boolean updateScheduled;
private final ToolRunnable updateRunnable= new SystemRunnable() {
@Override
public String getTypeId() {
return "r/datafilter/load"; //$NON-NLS-1$
}
@Override
public String getLabel() {
final RDataTableContentDescription input= FilterSet.this.input;
return NLS.bind(Messages.UpdateJob_label, (input != null) ? input.getLabel() : "..."); //$NON-NLS-1$
}
@Override
public boolean canRunIn(final Tool tool) {
return true; // TODO
}
@Override
public boolean changed(final int event, final Tool tool) {
switch (event) {
case MOVING_FROM:
return false;
case REMOVING_FROM:
case BEING_ABANDONED:
synchronized (FilterSet.this.updateLock) {
FilterSet.this.updateScheduled= false;
FilterSet.this.updateLock.notifyAll();
}
break;
default:
break;
}
return true;
}
@Override
public void run(final ToolService service, final ProgressMonitor m) throws StatusException {
runUpdate((RToolService)service, m);
}
};
private boolean updateAll;
private ImList<VariableFilter> filters= ImCollections.emptyList();
private ImList<String> filterNames= ImCollections.emptyList();
private final CopyOnWriteIdentityListSet<FilterListener> listeners= new CopyOnWriteIdentityListSet<>();
private final CopyOnWriteIdentityListSet<FilterListener> postListeners= new CopyOnWriteIdentityListSet<>();
private volatile int listenerScheduled;
private final Runnable listenerRunnable= new Runnable() {
@Override
public void run() {
final int schedule= FilterSet.this.listenerScheduled;
FilterSet.this.listenerScheduled= 0;
for (final FilterListener listener : FilterSet.this.listeners.toList()) {
listener.filterChanged();
}
if (schedule != NO_DELAY) {
FilterSet.this.postListenerTime= System.nanoTime() + POST_DELAY;
if (FilterSet.this.postListenerScheduled) {
return;
}
FilterSet.this.postListenerScheduled= true;
FilterSet.this.realm.timerExec(POST_DELAY, FilterSet.this.postListenerRunnable);
}
else {
FilterSet.this.postListenerTime= System.nanoTime();
FilterSet.this.postListenerScheduled= true;
FilterSet.this.postListenerRunnable.run();
}
}
};
private boolean postListenerScheduled;
private long postListenerTime;
private final Runnable postListenerRunnable= new Runnable() {
@Override
public void run() {
if (FilterSet.this.listenerScheduled > 0) {
FilterSet.this.postListenerScheduled= false;
return;
}
final long time= FilterSet.this.postListenerTime - System.nanoTime();
if (time > 20) {
FilterSet.this.realm.timerExec((int)time, this);
return;
}
FilterSet.this.postListenerScheduled= false;
for (final FilterListener listener : FilterSet.this.postListeners.toList()) {
listener.filterChanged();
}
}
};
private final Realm realm;
private boolean enabled;
private @Nullable String filterRExpression;
public FilterSet(final Realm realm) {
this.realm= realm;
this.enabled= true;
}
public Realm getRealm() {
return this.realm;
}
protected final void runInRealm(final Runnable runnable) {
if (this.realm.isCurrent()) {
runnable.run();
}
else {
this.realm.asyncExec(runnable);
}
}
public void updateInput(final @Nullable RDataTableContentDescription input) {
synchronized (this.updateLock) {
this.input= input;
this.inFilterUpdate++;
}
try {
synchronized (this) {
final var oldFilters= this.filters;
final var oldFilterNames= this.filterNames;
final var filters= new ArrayList<VariableFilter>();
final var filterNames= new ArrayList<String>();
this.isInputFilterUpdate= true;
try {
beginInputFilterUpdate();
int filterIdx= 0;
if (input != null) {
final List<RDataTableColumn> columns= input.getDataColumns();
filters.ensureCapacity(columns.size());
filterNames.ensureCapacity(columns.size());
for (final var column : columns) {
final String columnName= column.getName();
if (column.getRExpression() == null || columnName == null) {
continue;
}
VariableFilter oldFilter= null;
VariableFilter newFilter= null;
final int oldIdx= oldFilterNames.indexOf(columnName);
if (oldIdx >= 0) {
oldFilter= oldFilters.get(oldIdx);
if (getAvailableFilters(column).contains(oldFilter.getType())) {
newFilter= createFilter(oldFilter.getType(), column);
}
if (newFilter == null) {
newFilter= createFilter(getDefaultFilter(column), column);
}
if (newFilter == null) {
continue;
}
newFilter.load(oldFilter);
}
else {
newFilter= createFilter(getDefaultFilter(column), column);
if (newFilter == null) {
continue;
}
}
filters.add(newFilter);
filterNames.add(columnName);
updateFilter(filterIdx++, oldFilter, newFilter);
}
}
}
finally {
this.filters= ImCollections.toList(filters);
this.filterNames= ImCollections.toList(filterNames);
try {
completeInputFilterUpdate(this.filters);
}
finally {
this.isInputFilterUpdate= false;
}
}
}
}
finally {
synchronized (this.updateLock) {
this.inFilterUpdate--;
scheduleUpdate(true);
}
}
}
protected final boolean isInputFilterUpdate() {
return this.isInputFilterUpdate;
}
protected void beginInputFilterUpdate() {
}
protected void updateFilter(final int idx,
final @Nullable VariableFilter oldFilter, final @Nullable VariableFilter newFilter) {
}
protected void completeInputFilterUpdate(final ImList<VariableFilter> filter) {
}
public @Nullable FilterType getDefaultFilter(final RDataTableColumn column) {
switch (column.getVarType()) {
case RDataTableVariable.LOGI:
case RDataTableVariable.FACTOR:
case RDataTableVariable.RAW:
return FilterType.LEVEL;
case RDataTableVariable.INT:
case RDataTableVariable.NUM:
case RDataTableVariable.DATE:
case RDataTableVariable.DATETIME:
return FilterType.INTERVAL;
case RDataTableVariable.CHAR:
return FilterType.TEXT;
default:
return null;
}
}
public ImList<FilterType> getAvailableFilters(final RDataTableColumn column) {
switch (column.getVarType()) {
case RDataTableVariable.LOGI:
case RDataTableVariable.RAW:
return ImCollections.newList(FilterType.LEVEL);
case RDataTableVariable.FACTOR:
if (((RFactorStore)column.getDataStore()).isOrdered()) {
return ImCollections.newList(FilterType.LEVEL, FilterType.INTERVAL);
}
return ImCollections.newList(FilterType.LEVEL);
case RDataTableVariable.INT:
case RDataTableVariable.NUM:
case RDataTableVariable.DATE:
case RDataTableVariable.DATETIME:
return ImCollections.newList(FilterType.INTERVAL, FilterType.LEVEL);
case RDataTableVariable.CHAR:
return ImCollections.newList(FilterType.TEXT, FilterType.LEVEL);
default:
return ImCollections.emptyList();
}
}
public VariableFilter replace(final VariableFilter currentFilter, final FilterType filterType) {
VariableFilter newFilter= null;
synchronized (this.updateLock) {
if (this.inFilterUpdate > 0) {
return currentFilter;
}
this.inFilterUpdate++;
}
try {
synchronized (this) {
final var oldFilters= this.filters;
final int filterIdx= oldFilters.indexOf(currentFilter);
if (filterIdx < 0) {
return currentFilter;
}
newFilter= createFilter(filterType, currentFilter.getColumn());
if (newFilter == null) {
return currentFilter;
}
newFilter.load(currentFilter);
this.filters= ImCollections.setElement(oldFilters, filterIdx, newFilter);
updateFilter(filterIdx, currentFilter, newFilter);
return newFilter;
}
}
finally {
synchronized (this.updateLock) {
this.inFilterUpdate--;
if (newFilter != null) {
newFilter.scheduleUpdate();
}
}
}
}
protected @Nullable VariableFilter createFilter(final @Nullable FilterType filterType,
final RDataTableColumn column) {
if (filterType == null) {
return null;
}
switch (filterType.getId()) {
case 0:
return new LevelVariableFilter(this, column);
case 1:
return new IntervalVariableFilter(this, column);
case 2:
return new TextVariableFilter(this, column);
default:
throw new UnsupportedOperationException(filterType.toString());
}
}
public synchronized ImList<VariableFilter> getFilters() {
return this.filters;
}
protected void scheduleUpdate(final boolean all) {
final RDataTableContentDescription input;
synchronized (this.updateLock) {
if (all) {
this.updateAll= true;
}
if (this.updateScheduled || this.inFilterUpdate > 0) {
return;
}
input= this.input;
if (input != null) {
input.getRHandle().getQueue().add(this.updateRunnable);
this.updateScheduled= true;
}
}
}
protected @Nullable Tool getTool() {
final RDataTableContentDescription input= this.input;
return (input != null) ? input.getRHandle() : null;
}
private void runUpdate(final RToolService r, final ProgressMonitor m) {
final RDataTableContentDescription input;
final boolean all;
synchronized (this.updateLock) {
this.updateScheduled= false;
input= this.input;
if (this.inFilterUpdate > 0 || input == null || input.getRHandle() != r.getTool()) {
return;
}
all= this.updateAll;
this.updateAll= false;
}
final ImList<VariableFilter> filters;
synchronized (this) {
filters= this.filters;
}
for (final var filter : filters) {
if (all || filter.updateScheduled) {
filter.updateScheduled= false;
Exception error= null;
try {
filter.update(r, m);
}
catch (final StatusException e) {
error= e;
}
catch (final UnexpectedRDataException e) {
error= e;
}
if (error != null) {
error.printStackTrace();
filter.setError(error.getMessage());
}
}
}
updateFilter(true);
}
public void addListener(final FilterListener listener) {
this.listeners.add(listener);
}
public void removeListener(final FilterListener listener) {
this.listeners.remove(listener);
}
public void addPostListener(final FilterListener listener) {
this.postListeners.add(listener);
}
public void removePostListener(final FilterListener listener) {
this.postListeners.remove(listener);
}
public @Nullable String getFilterRExpression() {
return this.filterRExpression;
}
public @Nullable String getFilterRExpression(@Nullable String varExpression, final int nameFlags) {
final RDataTableContentDescription input;
final ImList<VariableFilter> filters;
synchronized (this) {
input= this.input;
if (this.inFilterUpdate > 0 || input == null ) {
return null;
}
filters= this.filters;
}
if (varExpression == null) {
varExpression= nonNullAssert(input.getElementName().getDisplayName(nameFlags));
}
final StringBuilder sb= new StringBuilder();
for (final var filter : filters) {
final String rExpression= filter.getFilterRExpression(varExpression, nameFlags);
if (rExpression != null && !rExpression.isEmpty()) {
if (sb.length() > 0) {
sb.append(" & "); //$NON-NLS-1$
}
sb.append(rExpression);
}
}
return sb.toString();
}
public void setEnabled(final boolean enabled) {
if (this.enabled == enabled) {
return;
}
this.enabled= enabled;
notifyListeners(NO_DELAY);
}
public boolean getEnabled() {
return this.enabled;
}
void updateFilter(final boolean delay) {
final RDataTableContentDescription input;
final ImList<VariableFilter> filters;
synchronized (this) {
input= this.input;
if (this.inFilterUpdate > 0 || input == null ) {
return;
}
filters= this.filters;
}
final StringBuilder sb= new StringBuilder();
for (final var filter : filters) {
final String rExpression= filter.getFilterRExpression();
if (rExpression != null && !rExpression.isEmpty()) {
if (sb.length() > 0) {
sb.append(" & "); //$NON-NLS-1$
}
sb.append(rExpression);
}
}
String filterRExpression;
if (sb.length() == 0) {
filterRExpression= null;
if (this.filterRExpression == null) {
return;
}
}
else {
filterRExpression= sb.toString();
if (filterRExpression.equals(this.filterRExpression)) {
return;
}
}
this.filterRExpression= filterRExpression;
notifyListeners((delay) ? STD_DELAY : NO_DELAY);
}
private void notifyListeners(final int mode) {
final int schedule= this.listenerScheduled;
if (schedule >= mode) {
return;
}
this.listenerScheduled= mode;
runInRealm(this.listenerRunnable);
}
}