001package jmri.jmrit.logixng.expressions;
002
003import java.beans.*;
004import java.util.*;
005
006import javax.annotation.Nonnull;
007
008import jmri.*;
009import jmri.Block;
010import jmri.BlockManager;
011import jmri.jmrit.display.layoutEditor.LayoutBlock;
012import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
013import jmri.jmrit.logixng.*;
014import jmri.jmrit.logixng.util.ReferenceUtil;
015import jmri.jmrit.logixng.util.parser.*;
016import jmri.jmrit.logixng.util.parser.ExpressionNode;
017import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
018import jmri.util.TypeConversionUtil;
019
020/**
021 * This expression evaluates the state of a Block.
022 * The supported characteristics are:
023 * <ul>
024 *   <li>Is [not] Occupied (based on occupancy sensor state)</li>
025 *   <li>Is [not] Unoccupied (based on occupancy sensor state)</li>
026 *   <li>Is [not] Other (UNKNOWN, INCONSISTENT, UNDETECTED)</li>
027 *   <li>Is [not] Allocated (based on the LayoutBlock useAlternateColor)</li>
028 *   <li>Value [not] equals string</li>
029 * </ul>
030 * @author Daniel Bergqvist Copyright 2021
031 * @author Dave Sand Copyright 2021
032 */
033public class ExpressionBlock extends AbstractDigitalExpression
034        implements PropertyChangeListener, VetoableChangeListener {
035
036    private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct;
037    private NamedBeanHandle<Block> _blockHandle;
038    private String _reference = "";
039    private String _localVariable = "";
040    private String _formula = "";
041    private ExpressionNode _expressionNode;
042
043    private Is_IsNot_Enum _is_IsNot = Is_IsNot_Enum.Is;
044
045    private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct;
046    private BlockState _blockState = BlockState.Occupied;
047    private String _stateReference = "";
048    private String _stateLocalVariable = "";
049    private String _stateFormula = "";
050    private ExpressionNode _stateExpressionNode;
051
052    private NamedBeanAddressing _dataAddressing = NamedBeanAddressing.Direct;
053    private String _dataReference = "";
054    private String _dataLocalVariable = "";
055    private String _dataFormula = "";
056    private ExpressionNode _dataExpressionNode;
057
058    private String _blockValue = "";
059
060    public ExpressionBlock(String sys, String user)
061            throws BadUserNameException, BadSystemNameException {
062        super(sys, user);
063    }
064
065    @Override
066    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws ParserException {
067        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
068        String sysName = systemNames.get(getSystemName());
069        String userName = userNames.get(getSystemName());
070        if (sysName == null) sysName = manager.getAutoSystemName();
071        ExpressionBlock copy = new ExpressionBlock(sysName, userName);
072        copy.setComment(getComment());
073        copy.setAddressing(_addressing);
074        if (_blockHandle != null) copy.setBlock(_blockHandle);
075        copy.setReference(_reference);
076        copy.setLocalVariable(_localVariable);
077        copy.setFormula(_formula);
078
079        copy.set_Is_IsNot(_is_IsNot);
080
081        copy.setStateAddressing(_stateAddressing);
082        copy.setBeanState(_blockState);
083        copy.setStateReference(_stateReference);
084        copy.setStateLocalVariable(_stateLocalVariable);
085        copy.setStateFormula(_stateFormula);
086
087        copy.setDataAddressing(_dataAddressing);
088        copy.setDataReference(_dataReference);
089        copy.setDataLocalVariable(_dataLocalVariable);
090        copy.setDataFormula(_dataFormula);
091        copy.setBlockValue(_blockValue);
092        return manager.registerExpression(copy);
093    }
094
095    public void setBlock(@Nonnull String blockName) {
096        assertListenersAreNotRegistered(log, "setBlock");
097        Block block =
098                InstanceManager.getDefault(BlockManager.class).getNamedBean(blockName);
099        if (block != null) {
100            ExpressionBlock.this.setBlock(block);
101        } else {
102            removeBlock();
103            log.warn("block \"{}\" is not found", blockName);
104        }
105    }
106
107    public void setBlock(@Nonnull Block block) {
108        assertListenersAreNotRegistered(log, "setBlock");
109        ExpressionBlock.this.setBlock(InstanceManager.getDefault(NamedBeanHandleManager.class)
110                .getNamedBeanHandle(block.getDisplayName(), block));
111    }
112
113    public void setBlock(@Nonnull NamedBeanHandle<Block> handle) {
114        assertListenersAreNotRegistered(log, "setBlock");
115        _blockHandle = handle;
116        InstanceManager.getDefault(BlockManager.class).addVetoableChangeListener(this);
117    }
118
119    public void removeBlock() {
120        assertListenersAreNotRegistered(log, "removeBlock");
121        if (_blockHandle != null) {
122            InstanceManager.getDefault(BlockManager.class).removeVetoableChangeListener(this);
123            _blockHandle = null;
124        }
125    }
126
127    public NamedBeanHandle<Block> getBlock() {
128        return _blockHandle;
129    }
130
131    public void setAddressing(NamedBeanAddressing addressing) throws ParserException {
132        _addressing = addressing;
133        parseFormula();
134    }
135
136    public NamedBeanAddressing getAddressing() {
137        return _addressing;
138    }
139
140    public void setReference(@Nonnull String reference) {
141        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
142            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
143        }
144        _reference = reference;
145    }
146
147    public String getReference() {
148        return _reference;
149    }
150
151    public void setLocalVariable(@Nonnull String localVariable) {
152        _localVariable = localVariable;
153    }
154
155    public String getLocalVariable() {
156        return _localVariable;
157    }
158
159    public void setFormula(@Nonnull String formula) throws ParserException {
160        _formula = formula;
161        parseFormula();
162    }
163
164    public String getFormula() {
165        return _formula;
166    }
167
168    private void parseFormula() throws ParserException {
169        if (_addressing == NamedBeanAddressing.Formula) {
170            Map<String, Variable> variables = new HashMap<>();
171
172            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
173            _expressionNode = parser.parseExpression(_formula);
174        } else {
175            _expressionNode = null;
176        }
177    }
178
179
180    public void set_Is_IsNot(Is_IsNot_Enum is_IsNot) {
181        _is_IsNot = is_IsNot;
182    }
183
184    public Is_IsNot_Enum get_Is_IsNot() {
185        return _is_IsNot;
186    }
187
188
189    public void setStateAddressing(NamedBeanAddressing addressing) throws ParserException {
190        _stateAddressing = addressing;
191        parseStateFormula();
192    }
193
194    public NamedBeanAddressing getStateAddressing() {
195        return _stateAddressing;
196    }
197
198    public void setBeanState(BlockState state) {
199        _blockState = state;
200    }
201
202    public BlockState getBeanState() {
203        return _blockState;
204    }
205
206    public void setStateReference(@Nonnull String reference) {
207        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
208            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
209        }
210        _stateReference = reference;
211    }
212
213    public String getStateReference() {
214        return _stateReference;
215    }
216
217    public void setStateLocalVariable(@Nonnull String localVariable) {
218        _stateLocalVariable = localVariable;
219    }
220
221    public String getStateLocalVariable() {
222        return _stateLocalVariable;
223    }
224
225    public void setStateFormula(@Nonnull String formula) throws ParserException {
226        _stateFormula = formula;
227        parseStateFormula();
228    }
229
230    public String getStateFormula() {
231        return _stateFormula;
232    }
233
234    private void parseStateFormula() throws ParserException {
235        if (_stateAddressing == NamedBeanAddressing.Formula) {
236            Map<String, Variable> variables = new HashMap<>();
237
238            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
239            _stateExpressionNode = parser.parseExpression(_stateFormula);
240        } else {
241            _stateExpressionNode = null;
242        }
243    }
244
245
246    public void setDataAddressing(NamedBeanAddressing addressing) throws ParserException {
247        _dataAddressing = addressing;
248        parseDataFormula();
249    }
250
251    public NamedBeanAddressing getDataAddressing() {
252        return _dataAddressing;
253    }
254
255    public void setDataReference(@Nonnull String reference) {
256        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
257            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
258        }
259        _dataReference = reference;
260    }
261
262    public String getDataReference() {
263        return _dataReference;
264    }
265
266    public void setDataLocalVariable(@Nonnull String localVariable) {
267        _dataLocalVariable = localVariable;
268    }
269
270    public String getDataLocalVariable() {
271        return _dataLocalVariable;
272    }
273
274    public void setDataFormula(@Nonnull String formula) throws ParserException {
275        _dataFormula = formula;
276        parseDataFormula();
277    }
278
279    public String getDataFormula() {
280        return _dataFormula;
281    }
282
283    private void parseDataFormula() throws ParserException {
284        if (_dataAddressing == NamedBeanAddressing.Formula) {
285            Map<String, Variable> variables = new HashMap<>();
286
287            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
288            _dataExpressionNode = parser.parseExpression(_dataFormula);
289        } else {
290            _dataExpressionNode = null;
291        }
292    }
293
294
295    public void setBlockValue(@Nonnull String value) {
296        _blockValue = value;
297    }
298
299    public String getBlockValue() {
300        return _blockValue;
301    }
302
303
304    @Override
305    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
306        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
307            if (evt.getOldValue() instanceof Block) {
308                if (evt.getOldValue().equals(getBlock().getBean())) {
309                    PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null);
310                    throw new PropertyVetoException(Bundle.getMessage("Block_BlockInUseVeto", getDisplayName()), e); // NOI18N
311                }
312            }
313        }
314    }
315
316    /** {@inheritDoc} */
317    @Override
318    public Category getCategory() {
319        return Category.ITEM;
320    }
321
322    private String getNewState() throws JmriException {
323
324        switch (_stateAddressing) {
325            case Reference:
326                return ReferenceUtil.getReference(
327                        getConditionalNG().getSymbolTable(), _stateReference);
328
329            case LocalVariable:
330                SymbolTable symbolTable =
331                        getConditionalNG().getSymbolTable();
332                return TypeConversionUtil
333                        .convertToString(symbolTable.getValue(_stateLocalVariable), false);
334
335            case Formula:
336                return _stateExpressionNode != null
337                        ? TypeConversionUtil.convertToString(
338                                _stateExpressionNode.calculate(
339                                        getConditionalNG().getSymbolTable()), false)
340                        : null;
341
342            default:
343                throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name());
344        }
345    }
346
347    private String getNewData() throws JmriException {
348
349        switch (_dataAddressing) {
350            case Reference:
351                return ReferenceUtil.getReference(
352                        getConditionalNG().getSymbolTable(), _dataReference);
353
354            case LocalVariable:
355                SymbolTable symbolTable =
356                        getConditionalNG().getSymbolTable();
357                return TypeConversionUtil
358                        .convertToString(symbolTable.getValue(_dataLocalVariable), false);
359
360            case Formula:
361                return _dataExpressionNode != null
362                        ? TypeConversionUtil.convertToString(
363                                _dataExpressionNode.calculate(
364                                        getConditionalNG().getSymbolTable()), false)
365                        : null;
366
367            default:
368                throw new IllegalArgumentException("invalid _addressing state: " + _dataAddressing.name());
369        }
370    }
371
372    /**
373     * A block is considered to be allocated if the related layout block has use extra color enabled.
374     * @param block The block whose allocation state is requested.
375     * @return true if the layout block is using the extra color.
376     */
377    public boolean isBlockAllocated(Block block) {
378        boolean result = false;
379        LayoutBlock layoutBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(block);
380        if (layoutBlock != null) {
381            result = layoutBlock.getUseExtraColor();
382        }
383        return result;
384    }
385
386    /** {@inheritDoc} */
387    @Override
388    public boolean evaluate() throws JmriException {
389        Block block;
390
391        switch (_addressing) {
392            case Direct:
393                block = _blockHandle != null ? _blockHandle.getBean() : null;
394                break;
395
396            case Reference:
397                String ref = ReferenceUtil.getReference(
398                        getConditionalNG().getSymbolTable(), _reference);
399                block = InstanceManager.getDefault(BlockManager.class)
400                        .getNamedBean(ref);
401                break;
402
403            case LocalVariable:
404                SymbolTable symbolTable =
405                        getConditionalNG().getSymbolTable();
406                block = InstanceManager.getDefault(BlockManager.class)
407                        .getNamedBean(TypeConversionUtil
408                                .convertToString(symbolTable.getValue(_localVariable), false));
409                break;
410
411            case Formula:
412                block = _expressionNode != null ?
413                        InstanceManager.getDefault(BlockManager.class)
414                                .getNamedBean(TypeConversionUtil
415                                        .convertToString(_expressionNode.calculate(
416                                                getConditionalNG().getSymbolTable()), false))
417                        : null;
418                break;
419
420            default:
421                throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
422        }
423
424        if (block == null) {
425            return false;
426        }
427
428        BlockState checkBlockState;
429
430        if ((_stateAddressing == NamedBeanAddressing.Direct)) {
431            checkBlockState = _blockState;
432        } else {
433            checkBlockState = BlockState.valueOf(getNewState());
434        }
435
436        int currentState = block.getState();
437        Object currentValue = null;
438
439        switch (checkBlockState) {
440            case Other:
441                if (currentState != Block.OCCUPIED && currentState != Block.UNOCCUPIED) {
442                    currentState = BlockState.Other.getID();
443                } else {
444                    currentState = 0;
445                }
446                break;
447
448            case Allocated:
449                boolean cuurrentAllocation = isBlockAllocated(block);
450                currentState = cuurrentAllocation ? BlockState.Allocated.getID() : 0;
451                break;
452
453            case ValueMatches:
454                currentValue = block.getValue();
455                if (_dataAddressing == NamedBeanAddressing.Direct) {
456                    currentState = _blockValue.equals(currentValue) ? BlockState.ValueMatches.getID() : 0;
457                } else {
458                    currentState = getNewData().equals(currentValue) ? BlockState.ValueMatches.getID() : 0;
459                }
460                break;
461
462            default:
463                break;
464        }
465
466        if (_is_IsNot == Is_IsNot_Enum.Is) {
467            return currentState == checkBlockState.getID();
468        } else {
469            return currentState != checkBlockState.getID();
470        }
471    }
472
473    @Override
474    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
475        throw new UnsupportedOperationException("Not supported.");
476    }
477
478    @Override
479    public int getChildCount() {
480        return 0;
481    }
482
483    @Override
484    public String getShortDescription(Locale locale) {
485        return Bundle.getMessage(locale, "Block_Short");
486    }
487
488    @Override
489    public String getLongDescription(Locale locale) {
490        String namedBean;
491        String state;
492
493        switch (_addressing) {
494            case Direct:
495                String blockName;
496                if (_blockHandle != null) {
497                    blockName = _blockHandle.getBean().getDisplayName();
498                } else {
499                    blockName = Bundle.getMessage(locale, "BeanNotSelected");
500                }
501                namedBean = Bundle.getMessage(locale, "AddressByDirect", blockName);
502                break;
503
504            case Reference:
505                namedBean = Bundle.getMessage(locale, "AddressByReference", _reference);
506                break;
507
508            case LocalVariable:
509                namedBean = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable);
510                break;
511
512            case Formula:
513                namedBean = Bundle.getMessage(locale, "AddressByFormula", _formula);
514                break;
515
516            default:
517                throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
518        }
519
520        switch (_stateAddressing) {
521            case Direct:
522                if (_blockState == BlockState.ValueMatches) {
523                    String bundleKey = "Block_Long_Value";
524                    String equalsString = _is_IsNot == Is_IsNot_Enum.Is ? Bundle.getMessage("Block_Equal") : Bundle.getMessage("Block_NotEqual");
525                    switch (_dataAddressing) {
526                        case Direct:
527                            return Bundle.getMessage(locale, bundleKey, namedBean, equalsString, _blockValue);
528                        case Reference:
529                            return Bundle.getMessage(locale, bundleKey, namedBean, equalsString, Bundle.getMessage("AddressByReference", _dataReference));
530                        case LocalVariable:
531                            return Bundle.getMessage(locale, bundleKey, namedBean, equalsString, Bundle.getMessage("AddressByLocalVariable", _dataLocalVariable));
532                        case Formula:
533                            return Bundle.getMessage(locale, bundleKey, namedBean, equalsString, Bundle.getMessage("AddressByFormula", _dataFormula));
534                        default:
535                            throw new IllegalArgumentException("invalid _dataAddressing state: " + _dataAddressing.name());
536                    }
537                } else if (_blockState == BlockState.Other) {
538                    state = Bundle.getMessage(locale, "AddressByDirect", _blockState._text);
539                    return Bundle.getMessage(locale, "Block_Long", namedBean, "", state);
540                } else {
541                    state = Bundle.getMessage(locale, "AddressByDirect", _blockState._text);
542                }
543               break;
544
545            case Reference:
546                state = Bundle.getMessage(locale, "AddressByReference", _stateReference);
547                break;
548
549            case LocalVariable:
550                state = Bundle.getMessage(locale, "AddressByLocalVariable", _stateLocalVariable);
551                break;
552
553            case Formula:
554                state = Bundle.getMessage(locale, "AddressByFormula", _stateFormula);
555                break;
556
557            default:
558                throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name());
559        }
560
561
562        return Bundle.getMessage(locale, "Block_Long", namedBean, _is_IsNot.toString(), state);
563    }
564
565    /** {@inheritDoc} */
566    @Override
567    public void setup() {
568        // Do nothing
569    }
570
571    /** {@inheritDoc} */
572    @Override
573    public void registerListenersForThisClass() {
574        if (!_listenersAreRegistered && (_blockHandle != null)) {
575            _blockHandle.getBean().addPropertyChangeListener(this);
576            _listenersAreRegistered = true;
577        }
578    }
579
580    /** {@inheritDoc} */
581    @Override
582    public void unregisterListenersForThisClass() {
583        if (_listenersAreRegistered) {
584            _blockHandle.getBean().removePropertyChangeListener(this);
585            _listenersAreRegistered = false;
586        }
587    }
588
589    /** {@inheritDoc} */
590    @Override
591    public void propertyChange(PropertyChangeEvent evt) {
592        getConditionalNG().execute();
593    }
594
595    /** {@inheritDoc} */
596    @Override
597    public void disposeMe() {
598    }
599
600    public enum BlockState {
601        Occupied(2, Bundle.getMessage("Block_StateOccupied")),
602        NotOccupied(4, Bundle.getMessage("Block_StateNotOccupied")),
603        Other(-1, Bundle.getMessage("Block_StateOther")),
604        Allocated(-2, Bundle.getMessage("Block_Allocated")),
605        ValueMatches(-3, Bundle.getMessage("Block_ValueMatches"));
606
607        private final int _id;
608        private final String _text;
609
610        private BlockState(int id, String text) {
611            this._id = id;
612            this._text = text;
613        }
614
615        public int getID() {
616            return _id;
617        }
618
619        @Override
620        public String toString() {
621            return _text;
622        }
623    }
624
625    /** {@inheritDoc} */
626    @Override
627    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
628        log.debug("getUsageReport :: ExpressionBlock: bean = {}, report = {}", cdl, report);
629        if (getBlock() != null && bean.equals(getBlock().getBean())) {
630            report.add(new NamedBeanUsageReport("LogixNGExpression", cdl, getLongDescription()));
631        }
632    }
633
634    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExpressionBlock.class);
635
636}