| /*=============================================================================# |
| # Copyright (c) 2012, 2020 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 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.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.IRDataTableVariable; |
| import org.eclipse.statet.r.ui.dataeditor.RDataTableColumn; |
| import org.eclipse.statet.rj.data.RFactorStore; |
| import org.eclipse.statet.rj.data.UnexpectedRDataException; |
| import org.eclipse.statet.rj.ts.core.RToolService; |
| |
| |
| 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 RDataTableContentDescription input; |
| |
| private boolean inputUpdate; |
| |
| 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() { |
| return NLS.bind(Messages.UpdateJob_label, FilterSet.this.input.getLabel()); |
| } |
| |
| @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 final List<VariableFilter> filters= new ArrayList<>(); |
| private final List<String> filterNames= new ArrayList<>(); |
| |
| private final CopyOnWriteIdentityListSet<IFilterListener> listeners= new CopyOnWriteIdentityListSet<>(); |
| private final CopyOnWriteIdentityListSet<IFilterListener> 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 IFilterListener 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 IFilterListener listener : FilterSet.this.postListeners.toList()) { |
| listener.filterChanged(); |
| } |
| } |
| }; |
| |
| private final Realm realm; |
| |
| private boolean enabled; |
| |
| private String filterRExpression; |
| |
| |
| public FilterSet(final Realm realm) { |
| this.realm= realm; |
| this.enabled= true; |
| } |
| |
| |
| public Realm getRealm() { |
| return this.realm; |
| } |
| |
| protected void runInRealm(final Runnable runnable) { |
| if (this.realm.isCurrent()) { |
| runnable.run(); |
| } |
| else { |
| this.realm.asyncExec(runnable); |
| } |
| } |
| |
| public void updateInput(final RDataTableContentDescription input) { |
| synchronized (this.updateLock) { |
| this.input= input; |
| this.inputUpdate= true; |
| } |
| synchronized (this) { |
| int idx= 0; |
| if (input != null) { |
| final List<RDataTableColumn> columns= input.getDataColumns(); |
| for (; idx < columns.size(); idx++) { |
| final RDataTableColumn column= columns.get(idx); |
| if (column.getRExpression() == null || column.getName() == null) { |
| continue; |
| } |
| final VariableFilter filter= createFilter(getDefaultFilter(column), column); |
| if (filter == null) { |
| continue; |
| } |
| final int oldIdx= this.filterNames.indexOf(column.getName()); |
| if (oldIdx >= 0) { |
| final VariableFilter oldFilter= this.filters.get(oldIdx); |
| filter.load(oldFilter); |
| if (idx != oldIdx) { |
| this.filterNames.remove(oldIdx); |
| this.filters.remove(oldIdx); |
| this.filterNames.add(idx, column.getName()); |
| this.filters.add(idx, filter); |
| } |
| else { |
| this.filters.set(idx, filter); |
| } |
| filterReplaced(idx, oldFilter, filter); |
| continue; |
| } |
| this.filterNames.add(idx, column.getName()); |
| this.filters.add(idx, filter); |
| filterAdded(idx, filter); |
| } |
| } |
| while (this.filters.size() > idx) { |
| this.filterNames.remove(idx); |
| final VariableFilter oldFilter= this.filters.remove(idx); |
| filterRemoved(oldFilter); |
| } |
| } |
| synchronized (this.updateLock) { |
| this.inputUpdate= false; |
| scheduleUpdate(true); |
| } |
| } |
| |
| protected void filterRemoved(final VariableFilter oldFilter) { |
| } |
| |
| protected void filterReplaced(final int idx, final VariableFilter oldFilter, final VariableFilter newFilter) { |
| } |
| |
| protected void filterAdded(final int idx, final VariableFilter newFilter) { |
| } |
| |
| public FilterType getDefaultFilter(final RDataTableColumn column) { |
| switch (column.getVarType()) { |
| case IRDataTableVariable.LOGI: |
| case IRDataTableVariable.FACTOR: |
| case IRDataTableVariable.RAW: |
| return FilterType.LEVEL; |
| case IRDataTableVariable.INT: |
| case IRDataTableVariable.NUM: |
| case IRDataTableVariable.DATE: |
| case IRDataTableVariable.DATETIME: |
| return FilterType.INTERVAL; |
| case IRDataTableVariable.CHAR: |
| return FilterType.TEXT; |
| default: |
| return null; |
| } |
| } |
| |
| public ImList<FilterType> getAvailableFilters(final RDataTableColumn column) { |
| switch (column.getVarType()) { |
| case IRDataTableVariable.LOGI: |
| case IRDataTableVariable.RAW: |
| return ImCollections.newList(FilterType.LEVEL); |
| case IRDataTableVariable.FACTOR: |
| if (((RFactorStore) column.getDataStore()).isOrdered()) { |
| return ImCollections.newList(FilterType.LEVEL, FilterType.INTERVAL); |
| } |
| return ImCollections.newList(FilterType.LEVEL); |
| case IRDataTableVariable.INT: |
| case IRDataTableVariable.NUM: |
| case IRDataTableVariable.DATE: |
| case IRDataTableVariable.DATETIME: |
| return ImCollections.newList(FilterType.INTERVAL, FilterType.LEVEL); |
| case IRDataTableVariable.CHAR: |
| return ImCollections.newList(FilterType.TEXT, FilterType.LEVEL); |
| default: |
| return null; |
| } |
| } |
| |
| public VariableFilter replace(final VariableFilter currentFilter, final FilterType filterType) { |
| synchronized (this.updateLock) { |
| this.inputUpdate= true; |
| } |
| final VariableFilter filter; |
| synchronized (this) { |
| final int idx= this.filters.indexOf(currentFilter); |
| if (idx < 0) { |
| return currentFilter; |
| } |
| filter= createFilter(filterType, currentFilter.getColumn()); |
| if (filter == null) { |
| return currentFilter; |
| } |
| filter.load(currentFilter); |
| this.filters.set(idx, filter); |
| filterReplaced(idx, currentFilter, filter); |
| } |
| synchronized (this.updateLock) { |
| this.inputUpdate= false; |
| filter.scheduleUpdate(); |
| } |
| return filter; |
| } |
| |
| protected VariableFilter createFilter(final 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 ImCollections.toList(this.filters); |
| } |
| |
| |
| protected void scheduleUpdate(final boolean all) { |
| synchronized (this.updateLock) { |
| if (all) { |
| this.updateAll= true; |
| } |
| if (this.updateScheduled || this.inputUpdate) { |
| return; |
| } |
| if (this.input != null) { |
| this.input.getRHandle().getQueue().add(this.updateRunnable); |
| this.updateScheduled= true; |
| } |
| } |
| } |
| |
| protected Tool getTool() { |
| final RDataTableContentDescription input= this.input; |
| return (input != null) ? input.getRHandle() : null; |
| } |
| |
| private void runUpdate(final RToolService r, final ProgressMonitor m) { |
| final boolean all; |
| synchronized (this.updateLock) { |
| this.updateScheduled= false; |
| if (this.inputUpdate || this.input == null || this.input.getRHandle() != r.getTool()) { |
| return; |
| } |
| all= this.updateAll; |
| this.updateAll= false; |
| } |
| final VariableFilter[] filters; |
| synchronized (this) { |
| filters= this.filters.toArray(new VariableFilter[this.filters.size()]); |
| } |
| for (int i= 0; i < filters.length; i++) { |
| final VariableFilter filter= filters[i]; |
| 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(); |
| filters[i].setError(error.getMessage()); |
| } |
| } |
| } |
| updateFilter(true); |
| } |
| |
| |
| public void addListener(final IFilterListener listener) { |
| this.listeners.add(listener); |
| } |
| |
| public void removeListener(final IFilterListener listener) { |
| this.listeners.remove(listener); |
| } |
| |
| public void addPostListener(final IFilterListener listener) { |
| this.postListeners.add(listener); |
| } |
| |
| public void removePostListener(final IFilterListener listener) { |
| this.postListeners.remove(listener); |
| } |
| |
| public String getFilterRExpression() { |
| return this.filterRExpression; |
| } |
| |
| public String getFilterRExpression(String varExpression, final int nameFlags) { |
| final VariableFilter[] filters; |
| synchronized (this) { |
| if (this.inputUpdate || this.input == null ) { |
| return null; |
| } |
| filters= this.filters.toArray(new VariableFilter[this.filters.size()]); |
| } |
| if (varExpression == null) { |
| varExpression= this.input.getElementName().getDisplayName(nameFlags); |
| } |
| final StringBuilder sb= new StringBuilder(); |
| for (int i= 0; i < filters.length; i++) { |
| final VariableFilter filter= filters[i]; |
| 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 VariableFilter[] filters; |
| synchronized (this) { |
| if (this.inputUpdate || this.input == null ) { |
| return; |
| } |
| filters= this.filters.toArray(new VariableFilter[this.filters.size()]); |
| } |
| final StringBuilder sb= new StringBuilder(); |
| for (int i= 0; i < filters.length; i++) { |
| final VariableFilter filter= filters[i]; |
| 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); |
| } |
| |
| } |