001package jmri.jmrit.logixng.util.parser;
002
003import java.lang.reflect.*;
004import java.util.*;
005
006import jmri.JmriException;
007import jmri.jmrit.logixng.SymbolTable;
008
009/**
010 * A parsed expression
011 *
012 * @author Daniel Bergqvist 2021
013 */
014public class ExpressionNodeMethod implements ExpressionNodeWithParameter {
015
016    private final String _method;
017    private final List<ExpressionNode> _parameterList;
018
019    public ExpressionNodeMethod(String method, Map<String, Variable> variables,
020            List<ExpressionNode> parameterList) throws FunctionNotExistsException {
021        _method = method;
022        _parameterList = parameterList;
023    }
024
025    private boolean isAssignableFrom(Class<?> type, Object param) {
026        if (param == null) return true;
027        if (type.isAssignableFrom(param.getClass())) return true;
028
029        if ((type == Byte.TYPE) && (param instanceof Byte)) return true;
030        if ((type == Short.TYPE) && (param instanceof Byte)) return true;
031        if ((type == Integer.TYPE) && (param instanceof Byte)) return true;
032        if ((type == Long.TYPE) && (param instanceof Byte)) return true;
033        if ((type == Float.TYPE) && (param instanceof Byte)) return true;
034        if ((type == Double.TYPE) && (param instanceof Byte)) return true;
035
036        if ((type == Byte.TYPE) && (param instanceof Short)) return true;
037        if ((type == Short.TYPE) && (param instanceof Short)) return true;
038        if ((type == Integer.TYPE) && (param instanceof Short)) return true;
039        if ((type == Long.TYPE) && (param instanceof Short)) return true;
040        if ((type == Float.TYPE) && (param instanceof Short)) return true;
041        if ((type == Double.TYPE) && (param instanceof Short)) return true;
042
043        if ((type == Byte.TYPE) && (param instanceof Integer)) return true;
044        if ((type == Short.TYPE) && (param instanceof Integer)) return true;
045        if ((type == Integer.TYPE) && (param instanceof Integer)) return true;
046        if ((type == Long.TYPE) && (param instanceof Integer)) return true;
047        if ((type == Float.TYPE) && (param instanceof Integer)) return true;
048        if ((type == Double.TYPE) && (param instanceof Integer)) return true;
049
050        if ((type == Byte.TYPE) && (param instanceof Long)) return true;
051        if ((type == Short.TYPE) && (param instanceof Long)) return true;
052        if ((type == Integer.TYPE) && (param instanceof Long)) return true;
053        if ((type == Long.TYPE) && (param instanceof Long)) return true;
054        if ((type == Float.TYPE) && (param instanceof Long)) return true;
055        if ((type == Double.TYPE) && (param instanceof Long)) return true;
056
057        if ((type == Float.TYPE) && (param instanceof Float)) return true;
058        if ((type == Double.TYPE) && (param instanceof Float)) return true;
059
060        if ((type == Float.TYPE) && (param instanceof Double)) return true;
061        return ((type == Double.TYPE) && (param instanceof Double));
062    }
063
064    private boolean canCall(Method m, Object[] params) {
065        Class<?>[] paramTypes = m.getParameterTypes();
066        if (paramTypes.length != params.length) return false;
067        for (int i=0; i < paramTypes.length; i++) {
068            if (!isAssignableFrom(paramTypes[i], params[i])) return false;
069        }
070        return true;
071    }
072
073    @SuppressWarnings("rawtypes")   // We don't know the generic types of Map.Entry in this method
074    private Object callMethod(Method method, Object obj, Object[] params)
075            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
076
077        Class<?>[] paramTypes = method.getParameterTypes();
078        Object[] newParams = new Object[params.length];
079        for (int i=0; i < params.length; i++) {
080            Object newParam;
081            if ((params[i] == null) || (paramTypes[i].isAssignableFrom(params[i].getClass()))) {
082                newParam = params[i];
083            }
084
085            else if ((paramTypes[i] == Byte.TYPE) && (params[i] instanceof Byte)) newParam = params[i];
086            else if ((paramTypes[i] == Short.TYPE) && (params[i] instanceof Byte)) newParam = (short)(byte)params[i];
087            else if ((paramTypes[i] == Integer.TYPE) && (params[i] instanceof Byte)) newParam = (int)(byte)params[i];
088            else if ((paramTypes[i] == Long.TYPE) && (params[i] instanceof Byte)) newParam = (long)(byte)params[i];
089            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Byte)) newParam = (float)(byte)params[i];
090            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Byte)) newParam = (double)(byte)params[i];
091
092            else if ((paramTypes[i] == Byte.TYPE) && (params[i] instanceof Short)) newParam = (byte)(short)params[i];
093            else if ((paramTypes[i] == Short.TYPE) && (params[i] instanceof Short)) newParam = params[i];
094            else if ((paramTypes[i] == Integer.TYPE) && (params[i] instanceof Short)) newParam = (int)(short)params[i];
095            else if ((paramTypes[i] == Long.TYPE) && (params[i] instanceof Short)) newParam = (long)(short)params[i];
096            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Short)) newParam = (float)(short)params[i];
097            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Short)) newParam = (double)(short)params[i];
098
099            else if ((paramTypes[i] == Byte.TYPE) && (params[i] instanceof Integer)) newParam = (byte)(int)params[i];
100            else if ((paramTypes[i] == Short.TYPE) && (params[i] instanceof Integer)) newParam = (short)(int)params[i];
101            else if ((paramTypes[i] == Integer.TYPE) && (params[i] instanceof Integer)) newParam = params[i];
102            else if ((paramTypes[i] == Long.TYPE) && (params[i] instanceof Integer)) newParam = (long)(int)params[i];
103            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Integer)) newParam = (float)(int)params[i];
104            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Integer)) newParam = (double)(int)params[i];
105
106            else if ((paramTypes[i] == Byte.TYPE) && (params[i] instanceof Long)) newParam = (byte)(long)params[i];
107            else if ((paramTypes[i] == Short.TYPE) && (params[i] instanceof Long)) newParam = (short)(long)params[i];
108            else if ((paramTypes[i] == Integer.TYPE) && (params[i] instanceof Long)) newParam = (int)(long)params[i];
109            else if ((paramTypes[i] == Long.TYPE) && (params[i] instanceof Long)) newParam = params[i];
110            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Long)) newParam = (float)(long)params[i];
111            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Long)) newParam = (double)(long)params[i];
112
113            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Float)) newParam = params[i];
114            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Float)) newParam = (double)(float)params[i];
115
116            else if ((paramTypes[i] == Float.TYPE) && (params[i] instanceof Double)) newParam = (float)(double)params[i];
117            else if ((paramTypes[i] == Double.TYPE) && (params[i] instanceof Double)) newParam = params[i];
118
119            else throw new RuntimeException(String.format("%s can not be assigned to %s", params[i].getClass().getName(), paramTypes[i].getName()));
120
121            newParams[i] = newParam;
122        }
123        try {
124            return method.invoke(obj, newParams);
125        } catch (IllegalAccessException ex) {
126            // https://stackoverflow.com/questions/50306093/java-9-calling-map-entrygetvalue-via-reflection#comment87628501_50306192
127            // https://stackoverflow.com/a/12038265
128            if (obj instanceof Map.Entry && newParams.length == 0) {
129                switch (method.getName()) {
130                    case "toString": return obj.toString();
131                    case "getKey": return ((Map.Entry)obj).getKey();
132                    case "getValue": return ((Map.Entry)obj).getValue();
133                    default: throw ex;
134                }
135            }
136            throw ex;
137        }
138    }
139
140    @Override
141    public Object calculate(Object parameter, SymbolTable symbolTable) throws JmriException {
142        if (parameter == null) throw new NullPointerException("Parameter is null");
143
144        Method[] methods = parameter.getClass().getMethods();
145        List<Object> parameters = new ArrayList<>();
146        for (ExpressionNode exprNode : _parameterList) {
147            parameters.add(exprNode.calculate(symbolTable));
148        }
149        Object[] params = parameters.toArray();
150
151        Exception exception = null;
152        for (Method m : methods) {
153            if (!m.getName().equals(_method)) continue;
154            try {
155                if (canCall(m, params)) return callMethod(m, parameter, params);
156            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
157                exception = ex;
158            }
159        }
160
161        if (exception != null) throw new ReflectionException("Reflection exception", exception);
162
163        List<String> paramList = new ArrayList<>();
164        for (Object o : params) paramList.add(String.format("%s:%s", o, o != null ? o.getClass().getName() : "null"));
165        throw new CannotCallMethodException(String.format("Can not call method %s(%s) on object %s", _method, String.join(", ", paramList), parameter), _method);
166    }
167
168    /** {@inheritDoc} */
169    @Override
170    public String getDefinitionString() {
171        StringBuilder str = new StringBuilder();
172        str.append("Method:");
173        str.append(_method);
174        str.append("(");
175        for (int i=0; i < _parameterList.size(); i++) {
176            if (i > 0) {
177                str.append(",");
178            }
179            str.append(_parameterList.get(i).getDefinitionString());
180        }
181        str.append(")");
182        return str.toString();
183    }
184
185}