001package jmri.jmrit.logixng.expressions;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006import java.util.regex.Matcher;
007import java.util.regex.Pattern;
008
009import javax.annotation.Nonnull;
010
011import jmri.*;
012import jmri.jmrit.logixng.*;
013import jmri.jmrit.logixng.util.LogixNG_SelectNamedBean;
014import jmri.util.TypeConversionUtil;
015
016/**
017 * Evaluates the state of a Reporter.
018 *
019 * @author Daniel Bergqvist Copyright 2018
020 * @author Dave Sand Copyright 2021
021 */
022public class ExpressionReporter extends AbstractDigitalExpression
023        implements PropertyChangeListener {
024
025    private final LogixNG_SelectNamedBean<Reporter> _selectNamedBean =
026            new LogixNG_SelectNamedBean<>(
027                    this, Reporter.class, InstanceManager.getDefault(ReporterManager.class), this);
028
029    private final LogixNG_SelectNamedBean<Memory> _selectMemoryNamedBean =
030            new LogixNG_SelectNamedBean<>(
031                    this, Memory.class, InstanceManager.getDefault(MemoryManager.class), this);
032
033    private ReporterValue _reporterValue = ReporterValue.CurrentReport;
034    private ReporterOperation _reporterOperation = ReporterOperation.Equal;
035    private CompareTo _compareTo = CompareTo.Value;
036
037    private boolean _caseInsensitive = false;
038    private String _constantValue = "";
039    private String _localVariable = "";
040    private String _regEx = "";
041    private boolean _listenToMemory = true;
042
043    public ExpressionReporter(String sys, String user)
044            throws BadUserNameException, BadSystemNameException {
045        super(sys, user);
046    }
047
048    @Override
049    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
050        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
051        String sysName = systemNames.get(getSystemName());
052        String userName = userNames.get(getSystemName());
053        if (sysName == null) sysName = manager.getAutoSystemName();
054        ExpressionReporter copy = new ExpressionReporter(sysName, userName);
055        copy.setComment(getComment());
056        _selectNamedBean.copy(copy._selectNamedBean);
057        _selectMemoryNamedBean.copy(copy._selectMemoryNamedBean);
058        copy.setReporterValue(_reporterValue);
059        copy.setReporterOperation(_reporterOperation);
060        copy.setCompareTo(_compareTo);
061        copy.setCaseInsensitive(_caseInsensitive);
062        copy.setConstantValue(_constantValue);
063        copy.setListenToMemory(_listenToMemory);
064        return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames);
065    }
066
067    public LogixNG_SelectNamedBean<Reporter> getSelectNamedBean() {
068        return _selectNamedBean;
069    }
070
071    public LogixNG_SelectNamedBean<Memory> getSelectMemoryNamedBean() {
072        return _selectMemoryNamedBean;
073    }
074
075    public void setLocalVariable(@Nonnull String localVariable) {
076        assertListenersAreNotRegistered(log, "setLocalVariable");
077        _localVariable = localVariable;
078    }
079
080    public String getLocalVariable() {
081        return _localVariable;
082    }
083
084
085    public void setConstantValue(String constantValue) {
086        _constantValue = constantValue;
087    }
088
089    public String getConstantValue() {
090        return _constantValue;
091    }
092
093
094    public void setRegEx(String regEx) {
095        _regEx = regEx;
096    }
097
098    public String getRegEx() {
099        return _regEx;
100    }
101
102
103    public void setListenToMemory(boolean listenToMemory) {
104        this._listenToMemory = listenToMemory;
105    }
106
107    public boolean getListenToMemory() {
108        return _listenToMemory;
109    }
110
111
112    public void setReporterValue(ReporterValue reporterValue) {
113        _reporterValue = reporterValue;
114    }
115
116    public ReporterValue getReporterValue() {
117        return _reporterValue;
118    }
119
120
121    public void setReporterOperation(ReporterOperation reporterOperation) {
122        _reporterOperation = reporterOperation;
123    }
124
125    public ReporterOperation getReporterOperation() {
126        return _reporterOperation;
127    }
128
129
130    public void setCompareTo(CompareTo compareTo) {
131        _compareTo = compareTo;
132    }
133
134    public CompareTo getCompareTo() {
135        return _compareTo;
136    }
137
138
139    public void setCaseInsensitive(boolean caseInsensitive) {
140        _caseInsensitive = caseInsensitive;
141    }
142
143    public boolean getCaseInsensitive() {
144        return _caseInsensitive;
145    }
146
147
148    /** {@inheritDoc} */
149    @Override
150    public Category getCategory() {
151        return Category.ITEM;
152    }
153
154    private String getString(Object o) {
155        if (o != null) {
156            return o.toString();
157        }
158        return null;
159    }
160
161    /**
162     * Compare two values using the comparator set using the comparison
163     * instructions in {@link #_reporterOperation}.
164     *
165     * <strong>Note:</strong> {@link #_reporterOperation} must be one of
166     * {@link #ExpressionReporter.ReporterOperation.LESS_THAN},
167     * {@link #ExpressionReporter.ReporterOperation.LESS_THAN_OR_EQUAL},
168     * {@link #ExpressionReporter.ReporterOperation.EQUAL},
169     * {@link #ExpressionReporter.ReporterOperation.GREATER_THAN_OR_EQUAL},
170     * or {@link #ExpressionReporter.ReporterOperation.GREATER_THAN}.
171     *
172     * @param value1          left side of the comparison
173     * @param value2          right side of the comparison
174     * @param caseInsensitive true if comparison should be case insensitive;
175     *                        false otherwise
176     * @return true if values compare per _reporterOperation; false otherwise
177     */
178    private boolean compare(String value1, String value2, boolean caseInsensitive) {
179        if (value1 == null) {
180            return value2 == null;
181        } else {
182            if (value2 == null) {
183                return false;
184            }
185            value1 = value1.trim();
186            value2 = value2.trim();
187        }
188        try {
189            int n1 = Integer.parseInt(value1);
190            try {
191                int n2 = Integer.parseInt(value2);
192                log.debug("Compare numbers: n1= {} to n2= {}", n1, n2);
193                switch (_reporterOperation) // both are numbers
194                {
195                    case LessThan:
196                        return (n1 < n2);
197                    case LessThanOrEqual:
198                        return (n1 <= n2);
199                    case Equal:
200                        return (n1 == n2);
201                    case NotEqual:
202                        return (n1 != n2);
203                    case GreaterThanOrEqual:
204                        return (n1 >= n2);
205                    case GreaterThan:
206                        return (n1 > n2);
207                    default:
208                        throw new IllegalArgumentException("_reporterOperation has unknown value: "+_reporterOperation.name());
209                }
210            } catch (NumberFormatException nfe) {
211                return _reporterOperation == ReporterOperation.NotEqual;   // n1 is a number, n2 is not
212            }
213        } catch (NumberFormatException nfe) {
214            try {
215                Integer.parseInt(value2);
216                return _reporterOperation == ReporterOperation.NotEqual;     // n1 is not a number, n2 is
217            } catch (NumberFormatException ex) { // OK neither a number
218            }
219        }
220        log.debug("Compare Strings: value1= {} to value2= {}", value1, value2);
221        int compare;
222        if (caseInsensitive) {
223            compare = value1.compareToIgnoreCase(value2);
224        } else {
225            compare = value1.compareTo(value2);
226        }
227        switch (_reporterOperation) {
228            case LessThan:
229                if (compare < 0) {
230                    return true;
231                }
232                break;
233            case LessThanOrEqual:
234                if (compare <= 0) {
235                    return true;
236                }
237                break;
238            case Equal:
239                if (compare == 0) {
240                    return true;
241                }
242                break;
243            case NotEqual:
244                if (compare != 0) {
245                    return true;
246                }
247                break;
248            case GreaterThanOrEqual:
249                if (compare >= 0) {
250                    return true;
251                }
252                break;
253            case GreaterThan:
254                if (compare > 0) {
255                    return true;
256                }
257                break;
258            default:
259                throw new IllegalArgumentException("_reporterOperation has unknown value: "+_reporterOperation.name());
260        }
261        return false;
262    }
263
264    private boolean matchRegex(String reporterValue, String regex) {
265        Pattern pattern = Pattern.compile(regex);
266        Matcher m = pattern.matcher(reporterValue);
267        return m.matches();
268    }
269
270    /** {@inheritDoc} */
271    @Override
272    public boolean evaluate() throws JmriException {
273        Reporter reporter = _selectNamedBean.evaluateNamedBean(getConditionalNG());
274
275        if (reporter == null) return false;
276
277        Object obj;
278        switch (_reporterValue) {
279            case CurrentReport:
280                obj = reporter.getCurrentReport();
281                break;
282
283            case LastReport:
284                obj = reporter.getLastReport();
285                break;
286
287            case State:
288                obj = reporter.getState();
289                break;
290
291            default:
292                throw new IllegalArgumentException("_reporterValue has unknown value: "+_reporterValue.name());
293        }
294        String reporterValue = getString(obj);
295        String otherValue = null;
296        boolean result;
297
298        switch (_compareTo) {
299            case Value:
300                otherValue = _constantValue;
301                break;
302            case Memory:
303                Memory memory = _selectMemoryNamedBean.evaluateNamedBean(getConditionalNG());
304                otherValue = getString(memory.getValue());
305                break;
306            case LocalVariable:
307                otherValue = TypeConversionUtil.convertToString(getConditionalNG().getSymbolTable().getValue(_localVariable), false);
308                break;
309            case RegEx:
310                // Do nothing
311                break;
312            default:
313                throw new IllegalArgumentException("_compareTo has unknown value: "+_compareTo.name());
314        }
315
316        switch (_reporterOperation) {
317            case LessThan:
318                // fall through
319            case LessThanOrEqual:
320                // fall through
321            case Equal:
322                // fall through
323            case NotEqual:
324                // fall through
325            case GreaterThanOrEqual:
326                // fall through
327            case GreaterThan:
328                result = compare(reporterValue, otherValue, _caseInsensitive);
329                break;
330
331            case IsNull:
332                result = reporterValue == null;
333                break;
334
335            case IsNotNull:
336                result = reporterValue != null;
337                break;
338
339            case MatchRegex:
340                result = matchRegex(reporterValue, _regEx);
341                break;
342
343            case NotMatchRegex:
344                result = !matchRegex(reporterValue, _regEx);
345                break;
346
347            default:
348                throw new IllegalArgumentException("_reporterOperation has unknown value: "+_reporterOperation.name());
349        }
350
351        return result;
352    }
353
354    @Override
355    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
356        throw new UnsupportedOperationException("Not supported.");
357    }
358
359    @Override
360    public int getChildCount() {
361        return 0;
362    }
363
364    @Override
365    public String getShortDescription(Locale locale) {
366        return Bundle.getMessage(locale, "Reporter_Short");
367    }
368
369    @Override
370    public String getLongDescription(Locale locale) {
371        String reporterName = _selectNamedBean.getDescription(locale);
372
373        String memoryName = _selectMemoryNamedBean.getDescription(locale);
374
375        String message;
376        String other;
377        switch (_compareTo) {
378            case Value:
379                message = "Reporter_Long_CompareConstant";
380                other = _constantValue;
381                break;
382
383            case Memory:
384                message = "Reporter_Long_CompareMemory";
385                other = memoryName;
386                break;
387
388            case LocalVariable:
389                message = "Reporter_Long_CompareLocalVariable";
390                other = _localVariable;
391                break;
392
393            case RegEx:
394                message = "Reporter_Long_CompareRegEx";
395                other = _regEx;
396                break;
397
398            default:
399                throw new IllegalArgumentException("_compareTo has unknown value: "+_compareTo.name());
400        }
401
402        switch (_reporterOperation) {
403            case LessThan:
404                // fall through
405            case LessThanOrEqual:
406                // fall through
407            case Equal:
408                // fall through
409            case NotEqual:
410                // fall through
411            case GreaterThanOrEqual:
412                // fall through
413            case GreaterThan:
414                return Bundle.getMessage(locale, message, reporterName, _reporterValue._text, _reporterOperation._text, other);
415
416            case IsNull:
417                // fall through
418            case IsNotNull:
419                return Bundle.getMessage(locale, "Reporter_Long_CompareNull", reporterName, _reporterValue._text, _reporterOperation._text);
420
421            case MatchRegex:
422                // fall through
423            case NotMatchRegex:
424                return Bundle.getMessage(locale, "Reporter_Long_CompareRegEx", reporterName, _reporterValue._text, _reporterOperation._text, other);
425
426            default:
427                throw new IllegalArgumentException("_reporterOperation has unknown value: "+_reporterOperation.name());
428        }
429    }
430
431    /** {@inheritDoc} */
432    @Override
433    public void setup() {
434        // Do nothing
435    }
436
437    /** {@inheritDoc} */
438    @Override
439    public void registerListenersForThisClass() {
440        if (!_listenersAreRegistered) {
441            switch (_reporterValue) {
442                case CurrentReport:
443                    _selectNamedBean.addPropertyChangeListener("currentReport", this);
444                    break;
445
446                case LastReport:
447                    _selectNamedBean.addPropertyChangeListener("lastReport", this);
448                    break;
449
450                case State:
451                    // No property change event is sent when state is changed for reports
452                    break;
453
454                default:
455                    // Do nothing
456            }
457            if (_listenToMemory) {
458                _selectMemoryNamedBean.addPropertyChangeListener("value", this);
459            }
460            _selectNamedBean.registerListeners();
461            _listenersAreRegistered = true;
462        }
463    }
464
465    /** {@inheritDoc} */
466    @Override
467    public void unregisterListenersForThisClass() {
468        if (_listenersAreRegistered) {
469            _selectNamedBean.removePropertyChangeListener("currentReport", this);
470            _selectNamedBean.removePropertyChangeListener("lastReport", this);
471            if (_listenToMemory) {
472                _selectMemoryNamedBean.removePropertyChangeListener("value", this);
473            }
474            _selectNamedBean.unregisterListeners();
475            _listenersAreRegistered = false;
476        }
477    }
478
479    /** {@inheritDoc} */
480    @Override
481    public void propertyChange(PropertyChangeEvent evt) {
482        getConditionalNG().execute();
483    }
484
485    /** {@inheritDoc} */
486    @Override
487    public void disposeMe() {
488    }
489
490    public enum ReporterValue {
491        CurrentReport(Bundle.getMessage("Reporter_Value_CurrentReport")),
492        LastReport(Bundle.getMessage("Reporter_Value_LastReport")),
493        State(Bundle.getMessage("Reporter_Value_State"));
494
495        private final String _text;
496
497        private ReporterValue(String text) {
498            this._text = text;
499        }
500
501        @Override
502        public String toString() {
503            return _text;
504        }
505    }
506
507
508    public enum ReporterOperation {
509        LessThan(Bundle.getMessage("ReporterOperation_LessThan"), true),
510        LessThanOrEqual(Bundle.getMessage("ReporterOperation_LessThanOrEqual"), true),
511        Equal(Bundle.getMessage("ReporterOperation_Equal"), true),
512        GreaterThanOrEqual(Bundle.getMessage("ReporterOperation_GreaterThanOrEqual"), true),
513        GreaterThan(Bundle.getMessage("ReporterOperation_GreaterThan"), true),
514        NotEqual(Bundle.getMessage("ReporterOperation_NotEqual"), true),
515        IsNull(Bundle.getMessage("ReporterOperation_IsNull"), false),
516        IsNotNull(Bundle.getMessage("ReporterOperation_IsNotNull"), false),
517        MatchRegex(Bundle.getMessage("ReporterOperation_MatchRegEx"), true),
518        NotMatchRegex(Bundle.getMessage("ReporterOperation_NotMatchRegEx"), true);
519
520        private final String _text;
521        private final boolean _extraValue;
522
523        private ReporterOperation(String text, boolean extraValue) {
524            this._text = text;
525            this._extraValue = extraValue;
526        }
527
528        @Override
529        public String toString() {
530            return _text;
531        }
532
533        public boolean hasExtraValue() {
534            return _extraValue;
535        }
536
537    }
538
539
540    public enum CompareTo {
541        Value(Bundle.getMessage("Reporter_CompareTo_Value")),
542        Memory(Bundle.getMessage("Reporter_CompareTo_Memory")),
543        LocalVariable(Bundle.getMessage("Reporter_CompareTo_LocalVariable")),
544        RegEx(Bundle.getMessage("Reporter_CompareTo_RegularExpression"));
545
546        private final String _text;
547
548        private CompareTo(String text) {
549            this._text = text;
550        }
551
552        @Override
553        public String toString() {
554            return _text;
555        }
556
557    }
558
559    /** {@inheritDoc} */
560    @Override
561    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
562        log.debug("getUsageReport :: ExpressionReporter: bean = {}, report = {}", cdl, report);
563        _selectNamedBean.getUsageDetail(level, bean, report, cdl, this, LogixNG_SelectNamedBean.Type.Expression);
564        _selectMemoryNamedBean.getUsageDetail(level, bean, report, cdl, this, LogixNG_SelectNamedBean.Type.Expression);
565    }
566
567    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExpressionReporter.class);
568
569}