001package jmri.jmrit.logixng.expressions;
002
003import static jmri.Conditional.OPERATOR_AND;
004import static jmri.Conditional.OPERATOR_NONE;
005import static jmri.Conditional.OPERATOR_OR;
006
007import java.util.List;
008import java.util.ArrayList;
009import java.util.BitSet;
010import java.util.Locale;
011import java.util.Map;
012import javax.annotation.Nonnull;
013import javax.annotation.CheckForNull;
014import jmri.InstanceManager;
015import jmri.JmriException;
016import jmri.jmrit.logixng.*;
017
018/**
019 * Evaluates to True if the antecedent evaluates to true
020 * 
021 * @author Daniel Bergqvist Copyright 2018
022 */
023public class Antecedent extends AbstractDigitalExpression implements FemaleSocketListener {
024
025    static final java.util.ResourceBundle rbx = java.util.ResourceBundle.getBundle("jmri.jmrit.conditional.ConditionalBundle");  // NOI18N
026    
027    private String _antecedent = "";
028    private final List<ExpressionEntry> _expressionEntries = new ArrayList<>();
029    private boolean disableCheckForUnconnectedSocket = false;
030    
031    /**
032     * Create a new instance of Antecedent with system name and user name.
033     * @param sys the system name
034     * @param user the user name
035     */
036    public Antecedent(@Nonnull String sys, @CheckForNull String user) {
037        super(sys, user);
038        _expressionEntries
039                .add(new ExpressionEntry(InstanceManager.getDefault(DigitalExpressionManager.class)
040                        .createFemaleSocket(this, this, getNewSocketName())));
041    }
042
043    /**
044     * Create a new instance of Antecedent with system name and user name.
045     * @param sys the system name
046     * @param user the user name
047     * @param expressionSystemNames a list of system names for the expressions
048     * this antecedent uses
049     */
050    public Antecedent(@Nonnull String sys, @CheckForNull String user,
051            List<Map.Entry<String, String>> expressionSystemNames)
052            throws BadUserNameException, BadSystemNameException {
053        super(sys, user);
054        setExpressionSystemNames(expressionSystemNames);
055    }
056    
057    @Override
058    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
059        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
060        String sysName = systemNames.get(getSystemName());
061        String userName = userNames.get(getSystemName());
062        if (sysName == null) sysName = manager.getAutoSystemName();
063        Antecedent copy = new Antecedent(sysName, userName);
064        copy.setComment(getComment());
065        copy.setNumSockets(getChildCount());
066        copy.setAntecedent(_antecedent);
067        return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames);
068    }
069
070    private void setExpressionSystemNames(List<Map.Entry<String, String>> systemNames) {
071        if (!_expressionEntries.isEmpty()) {
072            throw new RuntimeException("expression system names cannot be set more than once");
073        }
074        
075        for (Map.Entry<String, String> entry : systemNames) {
076            FemaleDigitalExpressionSocket socket =
077                    InstanceManager.getDefault(DigitalExpressionManager.class)
078                            .createFemaleSocket(this, this, entry.getKey());
079            
080            _expressionEntries.add(new ExpressionEntry(socket, entry.getValue()));
081        }
082    }
083    
084    public String getExpressionSystemName(int index) {
085        return _expressionEntries.get(index)._socketSystemName;
086    }
087
088    /** {@inheritDoc} */
089    @Override
090    public Category getCategory() {
091        return Category.COMMON;
092    }
093    
094    /** {@inheritDoc} */
095    @Override
096    public boolean evaluate() throws JmriException {
097        
098        if (_antecedent.isEmpty()) {
099            return false;
100        }
101        
102        boolean result;
103        
104        char[] ch = _antecedent.toCharArray();
105        int n = 0;
106        for (int j = 0; j < ch.length; j++) {
107            if (ch[j] != ' ') {
108                if (ch[j] == '{' || ch[j] == '[') {
109                    ch[j] = '(';
110                } else if (ch[j] == '}' || ch[j] == ']') {
111                    ch[j] = ')';
112                }
113                ch[n++] = ch[j];
114            }
115        }
116        try {
117            List<ExpressionEntry> list = new ArrayList<>();
118            for (ExpressionEntry e : _expressionEntries) {
119                list.add(e);
120            }
121            DataPair dp = parseCalculate(new String(ch, 0, n), list);
122            result = dp.result;
123        } catch (NumberFormatException | IndexOutOfBoundsException | JmriException nfe) {
124            result = false;
125            log.error("{} parseCalculation error antecedent= {}, ex= {}: {}",
126                getDisplayName(), _antecedent, nfe.getClass().getName(), nfe.getMessage());  // NOI18N
127        }
128        
129        return result;
130    }
131    
132    @Override
133    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
134        return _expressionEntries.get(index)._socket;
135    }
136    
137    @Override
138    public int getChildCount() {
139        return _expressionEntries.size();
140    }
141    
142    public void setChildCount(int count) {
143        List<FemaleSocket> addList = new ArrayList<>();
144        List<FemaleSocket> removeList = new ArrayList<>();
145        
146        // Is there too many children?
147        while (_expressionEntries.size() > count) {
148            int childNo = _expressionEntries.size()-1;
149            FemaleSocket socket = _expressionEntries.get(childNo)._socket;
150            if (socket.isConnected()) {
151                socket.disconnect();
152            }
153            removeList.add(_expressionEntries.get(childNo)._socket);
154            _expressionEntries.remove(childNo);
155        }
156        
157        // Is there not enough children?
158        while (_expressionEntries.size() < count) {
159            FemaleDigitalExpressionSocket socket =
160                    InstanceManager.getDefault(DigitalExpressionManager.class)
161                            .createFemaleSocket(this, this, getNewSocketName());
162            _expressionEntries.add(new ExpressionEntry(socket));
163            addList.add(socket);
164        }
165        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList);
166    }
167    
168    @Override
169    public String getShortDescription(Locale locale) {
170        return Bundle.getMessage(locale, "Antecedent_Short");
171    }
172    
173    @Override
174    public String getLongDescription(Locale locale) {
175        if (_antecedent.isEmpty()) {
176            return Bundle.getMessage(locale, "Antecedent_Long_Empty");
177        } else {
178            return Bundle.getMessage(locale, "Antecedent_Long", _antecedent);
179        }
180    }
181
182    public String getAntecedent() {
183        return _antecedent;
184    }
185
186    public final void setAntecedent(String antecedent) throws JmriException {
187//        String result = validateAntecedent(antecedent, _expressionEntries);
188//        if (result != null) System.out.format("DANIEL: Exception: %s%n", result);
189//        if (result != null) throw new IllegalArgumentException(result);
190        _antecedent = antecedent;
191    }
192    
193    // This method ensures that we have enough of children
194    private void setNumSockets(int num) {
195        List<FemaleSocket> addList = new ArrayList<>();
196        
197        // Is there not enough children?
198        while (_expressionEntries.size() < num) {
199            FemaleDigitalExpressionSocket socket =
200                    InstanceManager.getDefault(DigitalExpressionManager.class)
201                            .createFemaleSocket(this, this, getNewSocketName());
202            _expressionEntries.add(new ExpressionEntry(socket));
203            addList.add(socket);
204        }
205        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
206    }
207    
208    private void checkFreeSocket() {
209        boolean hasFreeSocket = false;
210        
211        for (ExpressionEntry entry : _expressionEntries) {
212            hasFreeSocket |= !entry._socket.isConnected();
213        }
214        if (!hasFreeSocket) {
215            FemaleDigitalExpressionSocket socket =
216                    InstanceManager.getDefault(DigitalExpressionManager.class)
217                                    .createFemaleSocket(this, this, getNewSocketName());
218            _expressionEntries.add(new ExpressionEntry(socket));
219            
220            List<FemaleSocket> list = new ArrayList<>();
221            list.add(socket);
222            firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, list);
223        }
224    }
225    
226    /** {@inheritDoc} */
227    @Override
228    public boolean isSocketOperationAllowed(int index, FemaleSocketOperation oper) {
229        switch (oper) {
230            case Remove:        // Possible if socket is not connected
231                return ! getChild(index).isConnected();
232            case InsertBefore:
233                return true;    // Always possible
234            case InsertAfter:
235                return true;    // Always possible
236            case MoveUp:
237                return index > 0;   // Possible if not first socket
238            case MoveDown:
239                return index+1 < getChildCount();   // Possible if not last socket
240            default:
241                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
242        }
243    }
244    
245    private void insertNewSocket(int index) {
246        FemaleDigitalExpressionSocket socket =
247                InstanceManager.getDefault(DigitalExpressionManager.class)
248                        .createFemaleSocket(this, this, getNewSocketName());
249        _expressionEntries.add(index, new ExpressionEntry(socket));
250        
251        List<FemaleSocket> addList = new ArrayList<>();
252        addList.add(socket);
253        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
254    }
255    
256    private void removeSocket(int index) {
257        List<FemaleSocket> removeList = new ArrayList<>();
258        removeList.add(_expressionEntries.remove(index)._socket);
259        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, null);
260    }
261    
262    private void moveSocketDown(int index) {
263        ExpressionEntry temp = _expressionEntries.get(index);
264        _expressionEntries.set(index, _expressionEntries.get(index+1));
265        _expressionEntries.set(index+1, temp);
266        
267        List<FemaleSocket> list = new ArrayList<>();
268        list.add(_expressionEntries.get(index)._socket);
269        list.add(_expressionEntries.get(index)._socket);
270        firePropertyChange(Base.PROPERTY_CHILD_REORDER, null, list);
271    }
272    
273    /** {@inheritDoc} */
274    @Override
275    public void doSocketOperation(int index, FemaleSocketOperation oper) {
276        switch (oper) {
277            case Remove:
278                if (getChild(index).isConnected()) throw new UnsupportedOperationException("Socket is connected");
279                removeSocket(index);
280                break;
281            case InsertBefore:
282                insertNewSocket(index);
283                break;
284            case InsertAfter:
285                insertNewSocket(index+1);
286                break;
287            case MoveUp:
288                if (index == 0) throw new UnsupportedOperationException("cannot move up first child");
289                moveSocketDown(index-1);
290                break;
291            case MoveDown:
292                if (index+1 == getChildCount()) throw new UnsupportedOperationException("cannot move down last child");
293                moveSocketDown(index);
294                break;
295            default:
296                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
297        }
298    }
299    
300    @Override
301    public void connected(FemaleSocket socket) {
302        if (disableCheckForUnconnectedSocket) return;
303        
304        for (ExpressionEntry entry : _expressionEntries) {
305            if (socket == entry._socket) {
306                entry._socketSystemName =
307                        socket.getConnectedSocket().getSystemName();
308            }
309        }
310        
311        checkFreeSocket();
312    }
313
314    @Override
315    public void disconnected(FemaleSocket socket) {
316        for (ExpressionEntry entry : _expressionEntries) {
317            if (socket == entry._socket) {
318                entry._socketSystemName = null;
319                break;
320            }
321        }
322    }
323
324    /** {@inheritDoc} */
325    @Override
326    public void setup() {
327        // We don't want to check for unconnected sockets while setup sockets
328        disableCheckForUnconnectedSocket = true;
329        
330        for (ExpressionEntry ee : _expressionEntries) {
331            try {
332                if ( !ee._socket.isConnected()
333                        || !ee._socket.getConnectedSocket().getSystemName()
334                                .equals(ee._socketSystemName)) {
335
336                    String socketSystemName = ee._socketSystemName;
337                    ee._socket.disconnect();
338                    if (socketSystemName != null) {
339                        MaleSocket maleSocket =
340                                InstanceManager.getDefault(DigitalExpressionManager.class)
341                                        .getBySystemName(socketSystemName);
342                        if (maleSocket != null) {
343                            ee._socket.connect(maleSocket);
344                            maleSocket.setup();
345                        } else {
346                            log.error("cannot load digital expression {}", socketSystemName);
347                        }
348                    }
349                } else {
350                    ee._socket.getConnectedSocket().setup();
351                }
352            } catch (SocketAlreadyConnectedException ex) {
353                // This shouldn't happen and is a runtime error if it does.
354                throw new RuntimeException("socket is already connected");
355            }
356        }
357        
358        checkFreeSocket();
359        
360        disableCheckForUnconnectedSocket = false;
361    }
362
363
364
365    /**
366     * Check that an antecedent is well formed.
367     *
368     * @param ant the antecedent string description
369     * @param expressionEntryList arraylist of existing ExpressionEntries
370     * @return error message string if not well formed
371     * @throws jmri.JmriException when an exception occurs
372     */
373    public String validateAntecedent(String ant, List<ExpressionEntry> expressionEntryList) throws JmriException {
374        char[] ch = ant.toCharArray();
375        int n = 0;
376        for (int j = 0; j < ch.length; j++) {
377            if (ch[j] != ' ') {
378                if (ch[j] == '{' || ch[j] == '[') {
379                    ch[j] = '(';
380                } else if (ch[j] == '}' || ch[j] == ']') {
381                    ch[j] = ')';
382                }
383                ch[n++] = ch[j];
384            }
385        }
386        int count = 0;
387        for (int j = 0; j < n; j++) {
388            if (ch[j] == '(') {
389                count++;
390            }
391            if (ch[j] == ')') {
392                count--;
393            }
394        }
395        if (count > 0) {
396            return java.text.MessageFormat.format(
397                    rbx.getString("ParseError7"), new Object[]{')'});  // NOI18N
398        }
399        if (count < 0) {
400            return java.text.MessageFormat.format(
401                    rbx.getString("ParseError7"), new Object[]{'('});  // NOI18N
402        }
403        try {
404            DataPair dp = parseCalculate(new String(ch, 0, n), expressionEntryList);
405            if (n != dp.indexCount) {
406                return java.text.MessageFormat.format(
407                        rbx.getString("ParseError4"), new Object[]{ch[dp.indexCount - 1]});  // NOI18N
408            }
409            int index = dp.argsUsed.nextClearBit(0);
410            if (index >= 0 && index < expressionEntryList.size()) {
411//                System.out.format("Daniel: ant: %s%n", ant);
412                return java.text.MessageFormat.format(
413                        rbx.getString("ParseError5"),  // NOI18N
414                        new Object[]{expressionEntryList.size(), index + 1});
415            }
416        } catch (NumberFormatException | IndexOutOfBoundsException | JmriException nfe) {
417            return rbx.getString("ParseError6") + nfe.getMessage();  // NOI18N
418        }
419        return null;
420    }
421
422    /**
423     * Parses and computes one parenthesis level of a boolean statement.
424     * <p>
425     * Recursively calls inner parentheses levels. Note that all logic operators
426     * are detected by the parsing, therefore the internal negation of a
427     * variable is washed.
428     *
429     * @param s            The expression to be parsed
430     * @param expressionEntryList ExpressionEntries for R1, R2, etc
431     * @return a data pair consisting of the truth value of the level a count of
432     *         the indices consumed to parse the level and a bitmap of the
433     *         variable indices used.
434     * @throws jmri.JmriException if unable to compute the logic
435     */
436    DataPair parseCalculate(String s, List<ExpressionEntry> expressionEntryList)
437            throws JmriException {
438
439        // for simplicity, we force the string to upper case before scanning
440        s = s.toUpperCase();
441
442        BitSet argsUsed = new BitSet(expressionEntryList.size());
443        DataPair dp = null;
444        boolean leftArg = false;
445        boolean rightArg = false;
446        int oper = OPERATOR_NONE;
447        int k = -1;
448        int i = 0;      // index of String s
449        //int numArgs = 0;
450        if (s.charAt(i) == '(') {
451            dp = parseCalculate(s.substring(++i), expressionEntryList);
452            leftArg = dp.result;
453            i += dp.indexCount;
454            argsUsed.or(dp.argsUsed);
455        } else // cannot be '('.  must be either leftArg or notleftArg
456        {
457            if (s.charAt(i) == 'R') {  // NOI18N
458                try {
459                    k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
460                    i += 2;
461                } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
462                    k = Integer.parseInt(String.valueOf(s.charAt(++i)));
463                }
464                leftArg = expressionEntryList.get(k - 1)._socket.evaluate();
465                i++;
466                argsUsed.set(k - 1);
467            } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
468                i += 3;
469
470                // not leftArg
471                if (s.charAt(i) == '(') {
472                    dp = parseCalculate(s.substring(++i), expressionEntryList);
473                    leftArg = dp.result;
474                    i += dp.indexCount;
475                    argsUsed.or(dp.argsUsed);
476                } else if (s.charAt(i) == 'R') {  // NOI18N
477                    try {
478                        k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
479                        i += 2;
480                    } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
481                        k = Integer.parseInt(String.valueOf(s.charAt(++i)));
482                    }
483                    leftArg = expressionEntryList.get(k - 1)._socket.evaluate();
484                    i++;
485                    argsUsed.set(k - 1);
486                } else {
487                    throw new JmriException(java.text.MessageFormat.format(
488                            rbx.getString("ParseError1"), new Object[]{s.substring(i)}));  // NOI18N
489                }
490                leftArg = !leftArg;
491            } else {
492                throw new JmriException(java.text.MessageFormat.format(
493                        rbx.getString("ParseError9"), new Object[]{s}));  // NOI18N
494            }
495        }
496        // crank away to the right until a matching parent is reached
497        while (i < s.length()) {
498            if (s.charAt(i) != ')') {
499                // must be either AND or OR
500                if ("AND".equals(s.substring(i, i + 3))) {  // NOI18N
501                    i += 3;
502                    oper = OPERATOR_AND;
503                } else if ("OR".equals(s.substring(i, i + 2))) {  // NOI18N
504                    i += 2;
505                    oper = OPERATOR_OR;
506                } else {
507                    throw new JmriException(java.text.MessageFormat.format(
508                            rbx.getString("ParseError2"), new Object[]{s.substring(i)}));  // NOI18N
509                }
510                if (s.charAt(i) == '(') {
511                    dp = parseCalculate(s.substring(++i), expressionEntryList);
512                    rightArg = dp.result;
513                    i += dp.indexCount;
514                    argsUsed.or(dp.argsUsed);
515                } else // cannot be '('.  must be either rightArg or notRightArg
516                {
517                    if (s.charAt(i) == 'R') {  // NOI18N
518                        try {
519                            k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
520                            i += 2;
521                        } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
522                            k = Integer.parseInt(String.valueOf(s.charAt(++i)));
523                        }
524                        rightArg = expressionEntryList.get(k - 1)._socket.evaluate();
525                        i++;
526                        argsUsed.set(k - 1);
527                    } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
528                        i += 3;
529                        // not rightArg
530                        if (s.charAt(i) == '(') {
531                            dp = parseCalculate(s.substring(++i), expressionEntryList);
532                            rightArg = dp.result;
533                            i += dp.indexCount;
534                            argsUsed.or(dp.argsUsed);
535                        } else if (s.charAt(i) == 'R') {  // NOI18N
536                            try {
537                                k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
538                                i += 2;
539                            } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
540                                k = Integer.parseInt(String.valueOf(s.charAt(++i)));
541                            }
542                            rightArg = expressionEntryList.get(k - 1)._socket.evaluate();
543                            i++;
544                            argsUsed.set(k - 1);
545                        } else {
546                            throw new JmriException(java.text.MessageFormat.format(
547                                    rbx.getString("ParseError3"), new Object[]{s.substring(i)}));  // NOI18N
548                        }
549                        rightArg = !rightArg;
550                    } else {
551                        throw new JmriException(java.text.MessageFormat.format(
552                                rbx.getString("ParseError9"), new Object[]{s.substring(i)}));  // NOI18N
553                    }
554                }
555                if (oper == OPERATOR_AND) {
556                    leftArg = (leftArg && rightArg);
557                } else if (oper == OPERATOR_OR) {
558                    leftArg = (leftArg || rightArg);
559                }
560            } else {  // This level done, pop recursion
561                i++;
562                break;
563            }
564        }
565        dp = new DataPair();
566        dp.result = leftArg;
567        dp.indexCount = i;
568        dp.argsUsed = argsUsed;
569        return dp;
570    }
571
572
573    static class DataPair {
574        boolean result = false;
575        int indexCount = 0;         // index reached when parsing completed
576        BitSet argsUsed = null;     // error detection for missing arguments
577    }
578
579    /* This class is public since ExpressionAntecedentXml needs to access it. */
580    public static class ExpressionEntry {
581        private String _socketSystemName;
582        private final FemaleDigitalExpressionSocket _socket;
583        
584        public ExpressionEntry(FemaleDigitalExpressionSocket socket, String socketSystemName) {
585            _socketSystemName = socketSystemName;
586            _socket = socket;
587        }
588        
589        private ExpressionEntry(FemaleDigitalExpressionSocket socket) {
590            this._socket = socket;
591        }
592        
593    }
594
595    /** {@inheritDoc} */
596    @Override
597    public void registerListenersForThisClass() {
598        // Do nothing
599    }
600
601    /** {@inheritDoc} */
602    @Override
603    public void unregisterListenersForThisClass() {
604        // Do nothing
605    }
606    
607    /** {@inheritDoc} */
608    @Override
609    public void disposeMe() {
610    }
611    
612    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Antecedent.class);
613}