001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.expression.spel.ast;
018
019import java.lang.reflect.Array;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.List;
023import java.util.Map;
024
025import org.springframework.expression.EvaluationException;
026import org.springframework.expression.TypedValue;
027import org.springframework.expression.spel.ExpressionState;
028import org.springframework.expression.spel.SpelEvaluationException;
029import org.springframework.expression.spel.SpelMessage;
030import org.springframework.lang.Nullable;
031import org.springframework.util.ClassUtils;
032import org.springframework.util.ObjectUtils;
033
034/**
035 * Represents projection, where a given operation is performed on all elements in some
036 * input sequence, returning a new sequence of the same size. For example:
037 * "{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}" returns "[n, y, n, y, n, y, n, y, n, y]"
038 *
039 * @author Andy Clement
040 * @author Mark Fisher
041 * @author Juergen Hoeller
042 * @since 3.0
043 */
044public class Projection extends SpelNodeImpl {
045
046        private final boolean nullSafe;
047
048
049        public Projection(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expression) {
050                super(startPos, endPos, expression);
051                this.nullSafe = nullSafe;
052        }
053
054
055        @Override
056        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
057                return getValueRef(state).getValue();
058        }
059
060        @Override
061        protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
062                TypedValue op = state.getActiveContextObject();
063
064                Object operand = op.getValue();
065                boolean operandIsArray = ObjectUtils.isArray(operand);
066                // TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor();
067
068                // When the input is a map, we push a special context object on the stack
069                // before calling the specified operation. This special context object
070                // has two fields 'key' and 'value' that refer to the map entries key
071                // and value, and they can be referenced in the operation
072                // eg. {'a':'y','b':'n'}.![value=='y'?key:null]" == ['a', null]
073                if (operand instanceof Map) {
074                        Map<?, ?> mapData = (Map<?, ?>) operand;
075                        List<Object> result = new ArrayList<>();
076                        for (Map.Entry<?, ?> entry : mapData.entrySet()) {
077                                try {
078                                        state.pushActiveContextObject(new TypedValue(entry));
079                                        state.enterScope();
080                                        result.add(this.children[0].getValueInternal(state).getValue());
081                                }
082                                finally {
083                                        state.popActiveContextObject();
084                                        state.exitScope();
085                                }
086                        }
087                        return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this);  // TODO unable to build correct type descriptor
088                }
089
090                if (operand instanceof Iterable || operandIsArray) {
091                        Iterable<?> data = (operand instanceof Iterable ?
092                                        (Iterable<?>) operand : Arrays.asList(ObjectUtils.toObjectArray(operand)));
093
094                        List<Object> result = new ArrayList<>();
095                        Class<?> arrayElementType = null;
096                        for (Object element : data) {
097                                try {
098                                        state.pushActiveContextObject(new TypedValue(element));
099                                        state.enterScope("index", result.size());
100                                        Object value = this.children[0].getValueInternal(state).getValue();
101                                        if (value != null && operandIsArray) {
102                                                arrayElementType = determineCommonType(arrayElementType, value.getClass());
103                                        }
104                                        result.add(value);
105                                }
106                                finally {
107                                        state.exitScope();
108                                        state.popActiveContextObject();
109                                }
110                        }
111
112                        if (operandIsArray) {
113                                if (arrayElementType == null) {
114                                        arrayElementType = Object.class;
115                                }
116                                Object resultArray = Array.newInstance(arrayElementType, result.size());
117                                System.arraycopy(result.toArray(), 0, resultArray, 0, result.size());
118                                return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultArray),this);
119                        }
120
121                        return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this);
122                }
123
124                if (operand == null) {
125                        if (this.nullSafe) {
126                                return ValueRef.NullValueRef.INSTANCE;
127                        }
128                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, "null");
129                }
130
131                throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE,
132                                operand.getClass().getName());
133        }
134
135        @Override
136        public String toStringAST() {
137                return "![" + getChild(0).toStringAST() + "]";
138        }
139
140        private Class<?> determineCommonType(@Nullable Class<?> oldType, Class<?> newType) {
141                if (oldType == null) {
142                        return newType;
143                }
144                if (oldType.isAssignableFrom(newType)) {
145                        return oldType;
146                }
147                Class<?> nextType = newType;
148                while (nextType != Object.class) {
149                        if (nextType.isAssignableFrom(oldType)) {
150                                return nextType;
151                        }
152                        nextType = nextType.getSuperclass();
153                }
154                for (Class<?> nextInterface : ClassUtils.getAllInterfacesForClassAsSet(newType)) {
155                        if (nextInterface.isAssignableFrom(oldType)) {
156                                return nextInterface;
157                        }
158                }
159                return Object.class;
160        }
161
162}