| /*=============================================================================# |
| # 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); |
| } |
| |
| } |