001package jmri.jmrit.logixng.actions; 002 003import java.util.List; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.Locale; 007import java.util.Map; 008 009import javax.annotation.Nonnull; 010import javax.annotation.CheckForNull; 011 012import jmri.InstanceManager; 013import jmri.JmriException; 014import jmri.Manager; 015import jmri.NamedBean; 016import jmri.NamedBeanUsageReport; 017import jmri.jmrit.logixng.*; 018import jmri.jmrit.logixng.implementation.DefaultFemaleGenericExpressionSocket; 019import jmri.jmrit.logixng.util.parser.ParserException; 020import jmri.jmrit.logixng.util.parser.RecursiveDescentParser; 021import jmri.jmrit.logixng.util.parser.Variable; 022import jmri.jmrit.logixng.util.parser.GenericExpressionVariable; 023import jmri.jmrit.logixng.util.parser.ExpressionNode; 024 025/** 026 * This action evaluates the formula. 027 * 028 * @author Daniel Bergqvist Copyright 2021 029 */ 030public class DigitalFormula extends AbstractDigitalAction implements FemaleSocketListener { 031 032 private String _formula = ""; 033 private ExpressionNode _expressionNode; 034 private final List<ExpressionEntry> _expressionEntries = new ArrayList<>(); 035 private boolean _disableCheckForUnconnectedSocket = false; 036 037 /** 038 * Create a new instance of Formula with system name and user name. 039 * @param sys the system name 040 * @param user the user name 041 */ 042 public DigitalFormula(@Nonnull String sys, @CheckForNull String user) { 043 super(sys, user); 044 _expressionEntries 045 .add(new ExpressionEntry(createFemaleSocket(this, this, getNewSocketName()))); 046 } 047 048 /** 049 * Create a new instance of Formula with system name and user name. 050 * @param sys the system name 051 * @param user the user name 052 * @param expressionSystemNames a list of system names for the expressions 053 * this formula uses 054 */ 055 public DigitalFormula(@Nonnull String sys, @CheckForNull String user, 056 List<SocketData> expressionSystemNames) { 057 super(sys, user); 058 setExpressionSystemNames(expressionSystemNames); 059 } 060 061 @Override 062 protected String getPreferredSocketPrefix() { 063 return "E"; 064 } 065 066 @Override 067 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 068 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 069 String sysName = systemNames.get(getSystemName()); 070 String userName = userNames.get(getSystemName()); 071 if (sysName == null) sysName = manager.getAutoSystemName(); 072 DigitalFormula copy = new DigitalFormula(sysName, userName); 073 copy.setComment(getComment()); 074 copy.setNumSockets(getChildCount()); 075 copy.setFormula(_formula); 076 return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames); 077 } 078 079 private void setExpressionSystemNames(List<SocketData> systemNames) { 080 if (!_expressionEntries.isEmpty()) { 081 throw new RuntimeException("expression system names cannot be set more than once"); 082 } 083 084 for (SocketData socketData : systemNames) { 085 FemaleGenericExpressionSocket socket = 086 createFemaleSocket(this, this, socketData._socketName); 087// FemaleGenericExpressionSocket socket = 088// InstanceManager.getDefault(AnalogExpressionManager.class) 089// .createFemaleSocket(this, this, entry.getKey()); 090 091 _expressionEntries.add(new ExpressionEntry(socket, socketData._socketSystemName, socketData._manager)); 092 } 093 } 094 095 public String getExpressionSystemName(int index) { 096 return _expressionEntries.get(index)._socketSystemName; 097 } 098 099 public String getExpressionManager(int index) { 100 return _expressionEntries.get(index)._manager; 101 } 102 103 private FemaleGenericExpressionSocket createFemaleSocket( 104 Base parent, FemaleSocketListener listener, String socketName) { 105 106 return new DefaultFemaleGenericExpressionSocket( 107 FemaleGenericExpressionSocket.SocketType.GENERIC, parent, listener, socketName); 108 } 109 110 public final void setFormula(String formula) throws ParserException { 111 Map<String, Variable> variables = new HashMap<>(); 112 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 113 for (int i=0; i < getChildCount(); i++) { 114 Variable v = new GenericExpressionVariable((FemaleGenericExpressionSocket)getChild(i)); 115 variables.put(v.getName(), v); 116 } 117 _expressionNode = parser.parseExpression(formula); 118 // parseExpression() may throw an exception and we don't want to set 119 // the field _formula until we now parseExpression() has succeeded. 120 _formula = formula; 121 } 122 123 public String getFormula() { 124 return _formula; 125 } 126 127 private void parseFormula() { 128 try { 129 setFormula(_formula); 130 } catch (ParserException e) { 131 log.error("Unexpected exception when parsing the formula", e); 132 } 133 } 134 135 /** {@inheritDoc} */ 136 @Override 137 public LogixNG_Category getCategory() { 138 return LogixNG_Category.COMMON; 139 } 140 141 /** {@inheritDoc} */ 142 @Override 143 public void execute() throws JmriException { 144 145 if (_formula.isEmpty()) return; 146 147 _expressionNode.calculate(getConditionalNG().getSymbolTable()); 148 } 149 150 @Override 151 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 152 return _expressionEntries.get(index)._socket; 153 } 154 155 @Override 156 public int getChildCount() { 157 return _expressionEntries.size(); 158 } 159 160 public void setChildCount(int count) { 161 List<FemaleSocket> addList = new ArrayList<>(); 162 List<FemaleSocket> removeList = new ArrayList<>(); 163 164 // Is there too many children? 165 while (_expressionEntries.size() > count) { 166 int childNo = _expressionEntries.size()-1; 167 FemaleSocket socket = _expressionEntries.get(childNo)._socket; 168 if (socket.isConnected()) { 169 socket.disconnect(); 170 } 171 removeList.add(_expressionEntries.get(childNo)._socket); 172 _expressionEntries.remove(childNo); 173 } 174 175 // Is there not enough children? 176 while (_expressionEntries.size() < count) { 177 FemaleGenericExpressionSocket socket = 178 createFemaleSocket(this, this, getNewSocketName()); 179 _expressionEntries.add(new ExpressionEntry(socket)); 180 addList.add(socket); 181 } 182 parseFormula(); 183 firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList); 184 } 185 186 @Override 187 public String getShortDescription(Locale locale) { 188 return Bundle.getMessage(locale, "DigitalFormula_Short"); 189 } 190 191 @Override 192 public String getLongDescription(Locale locale) { 193 if (_formula.isEmpty()) { 194 return Bundle.getMessage(locale, "DigitalFormula_Long_Empty"); 195 } else { 196 return Bundle.getMessage(locale, "DigitalFormula_Long", _formula); 197 } 198 } 199 200 // This method ensures that we have enough of children 201 private void setNumSockets(int num) { 202 List<FemaleSocket> addList = new ArrayList<>(); 203 204 // Is there not enough children? 205 while (_expressionEntries.size() < num) { 206 FemaleGenericExpressionSocket socket = 207 createFemaleSocket(this, this, getNewSocketName()); 208 _expressionEntries.add(new ExpressionEntry(socket)); 209 addList.add(socket); 210 } 211 parseFormula(); 212 firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList); 213 } 214 215 private void checkFreeSocket() { 216 boolean hasFreeSocket = false; 217 218 for (ExpressionEntry entry : _expressionEntries) { 219 hasFreeSocket |= !entry._socket.isConnected(); 220 } 221 if (!hasFreeSocket) { 222 FemaleGenericExpressionSocket socket = 223 createFemaleSocket(this, this, getNewSocketName()); 224 _expressionEntries.add(new ExpressionEntry(socket)); 225 226 List<FemaleSocket> list = new ArrayList<>(); 227 list.add(socket); 228 parseFormula(); 229 firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, list); 230 } 231 } 232 233 /** {@inheritDoc} */ 234 @Override 235 public boolean isSocketOperationAllowed(int index, FemaleSocketOperation oper) { 236 switch (oper) { 237 case Remove: // Possible if socket is not connected 238 return ! getChild(index).isConnected(); 239 case InsertBefore: 240 return true; // Always possible 241 case InsertAfter: 242 return true; // Always possible 243 case MoveUp: 244 return index > 0; // Possible if not first socket 245 case MoveDown: 246 return index+1 < getChildCount(); // Possible if not last socket 247 default: 248 throw new UnsupportedOperationException("Oper is unknown" + oper.name()); 249 } 250 } 251 252 private void insertNewSocket(int index) { 253 FemaleGenericExpressionSocket socket = 254 createFemaleSocket(this, this, getNewSocketName()); 255 _expressionEntries.add(index, new ExpressionEntry(socket)); 256 257 List<FemaleSocket> addList = new ArrayList<>(); 258 addList.add(socket); 259 parseFormula(); 260 firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList); 261 } 262 263 private void removeSocket(int index) { 264 List<FemaleSocket> removeList = new ArrayList<>(); 265 removeList.add(_expressionEntries.remove(index)._socket); 266 parseFormula(); 267 firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, null); 268 } 269 270 private void moveSocketDown(int index) { 271 ExpressionEntry temp = _expressionEntries.get(index); 272 _expressionEntries.set(index, _expressionEntries.get(index+1)); 273 _expressionEntries.set(index+1, temp); 274 275 List<FemaleSocket> list = new ArrayList<>(); 276 list.add(_expressionEntries.get(index)._socket); 277 list.add(_expressionEntries.get(index)._socket); 278 parseFormula(); 279 firePropertyChange(Base.PROPERTY_CHILD_REORDER, null, list); 280 } 281 282 /** {@inheritDoc} */ 283 @Override 284 public void doSocketOperation(int index, FemaleSocketOperation oper) { 285 switch (oper) { 286 case Remove: 287 if (getChild(index).isConnected()) throw new UnsupportedOperationException("Socket is connected"); 288 removeSocket(index); 289 break; 290 case InsertBefore: 291 insertNewSocket(index); 292 break; 293 case InsertAfter: 294 insertNewSocket(index+1); 295 break; 296 case MoveUp: 297 if (index == 0) throw new UnsupportedOperationException("cannot move up first child"); 298 moveSocketDown(index-1); 299 break; 300 case MoveDown: 301 if (index+1 == getChildCount()) throw new UnsupportedOperationException("cannot move down last child"); 302 moveSocketDown(index); 303 break; 304 default: 305 throw new UnsupportedOperationException("Oper is unknown" + oper.name()); 306 } 307 } 308 309 @Override 310 public void connected(FemaleSocket socket) { 311 if (_disableCheckForUnconnectedSocket) return; 312 313 for (ExpressionEntry entry : _expressionEntries) { 314 if (socket == entry._socket) { 315 entry._socketSystemName = 316 socket.getConnectedSocket().getSystemName(); 317 } 318 } 319 320 checkFreeSocket(); 321 } 322 323 @Override 324 public void disconnected(FemaleSocket socket) { 325 for (ExpressionEntry entry : _expressionEntries) { 326 if (socket == entry._socket) { 327 entry._socketSystemName = null; 328 break; 329 } 330 } 331 } 332 333 /** {@inheritDoc} */ 334 @Override 335 public void socketNameChanged(FemaleSocket socket) { 336 parseFormula(); 337 } 338 339 /** {@inheritDoc} */ 340 @Override 341 public void setup() { 342 // We don't want to check for unconnected sockets while setup sockets 343 _disableCheckForUnconnectedSocket = true; 344 345 for (ExpressionEntry ee : _expressionEntries) { 346 try { 347 if ( !ee._socket.isConnected() 348 || !ee._socket.getConnectedSocket().getSystemName() 349 .equals(ee._socketSystemName)) { 350 351 String socketSystemName = ee._socketSystemName; 352 String manager = ee._manager; 353 ee._socket.disconnect(); 354 if (socketSystemName != null) { 355 Manager<? extends MaleSocket> m = 356 InstanceManager.getDefault(LogixNG_Manager.class) 357 .getManager(manager); 358 MaleSocket maleSocket = m.getBySystemName(socketSystemName); 359 if (maleSocket != null) { 360 ee._socket.connect(maleSocket); 361 maleSocket.setup(); 362 } else { 363 log.error("cannot load digital expression {}", socketSystemName); 364 } 365 } 366 } else { 367 ee._socket.getConnectedSocket().setup(); 368 } 369 } catch (SocketAlreadyConnectedException ex) { 370 // This shouldn't happen and is a runtime error if it does. 371 throw new RuntimeException("socket is already connected"); 372 } 373 } 374 375 parseFormula(); 376 checkFreeSocket(); 377 378 _disableCheckForUnconnectedSocket = false; 379 } 380 381 /** {@inheritDoc} */ 382 @Override 383 public void registerListenersForThisClass() { 384 // Do nothing 385 } 386 387 /** {@inheritDoc} */ 388 @Override 389 public void unregisterListenersForThisClass() { 390 // Do nothing 391 } 392 393 /** {@inheritDoc} */ 394 @Override 395 public void disposeMe() { 396 } 397 398 /** {@inheritDoc} */ 399 @Override 400 public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) { 401 log.debug("getUsageReport :: Action DigitalFormula: bean = {}, f = \"{}\", cdl = {}", bean, getFormula(), cdl); 402 if (bean != null) { 403 // Search for possible bean references using system and user names. False positives are possible. 404 var formula = getFormula(); 405 var uname = bean.getUserName(); 406 if (formula.contains(bean.getSystemName()) || (uname != null && formula.contains(uname))) { 407 report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription())); 408 } 409 } 410 } 411 412 public static class SocketData { 413 public final String _socketName; 414 public final String _socketSystemName; 415 public final String _manager; 416 417 public SocketData(String socketName, String socketSystemName, String manager) { 418 _socketName = socketName; 419 _socketSystemName = socketSystemName; 420 _manager = manager; 421 } 422 } 423 424 425 /* This class is public since ExpressionFormulaXml needs to access it. */ 426 public static class ExpressionEntry { 427 private final FemaleGenericExpressionSocket _socket; 428 private String _socketSystemName; 429 public String _manager; 430 431 public ExpressionEntry(FemaleGenericExpressionSocket socket, String socketSystemName, String manager) { 432 _socket = socket; 433 _socketSystemName = socketSystemName; 434 _manager = manager; 435 } 436 437 private ExpressionEntry(FemaleGenericExpressionSocket socket) { 438 this._socket = socket; 439 } 440 441 } 442 443 444 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DigitalFormula.class); 445}