001/*
002 * Copyright 2002-2017 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.util.ClassUtils;
031import org.springframework.util.ObjectUtils;
032
033/**
034 * Represents projection, where a given operation is performed on all elements in some
035 * input sequence, returning a new sequence of the same size. For example:
036 * "{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}" returns "[n, y, n, y, n, y, n, y, n, y]"
037 *
038 * @author Andy Clement
039 * @author Mark Fisher
040 * @author Juergen Hoeller
041 * @since 3.0
042 */
043public class Projection extends SpelNodeImpl {
044
045        private final boolean nullSafe;
046
047
048        public Projection(boolean nullSafe, int pos, SpelNodeImpl expression) {
049                super(pos, expression);
050                this.nullSafe = nullSafe;
051        }
052
053
054        @Override
055        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
056                return getValueRef(state).getValue();
057        }
058
059        @Override
060        protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
061                TypedValue op = state.getActiveContextObject();
062
063                Object operand = op.getValue();
064                boolean operandIsArray = ObjectUtils.isArray(operand);
065                // TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor();
066
067                // When the input is a map, we push a special context object on the stack
068                // before calling the specified operation. This special context object
069                // has two fields 'key' and 'value' that refer to the map entries key
070                // and value, and they can be referenced in the operation
071                // eg. {'a':'y','b':'n'}.![value=='y'?key:null]" == ['a', null]
072                if (operand instanceof Map) {
073                        Map<?, ?> mapData = (Map<?, ?>) operand;
074                        List<Object> result = new ArrayList<Object>();
075                        for (Map.Entry<?, ?> entry : mapData.entrySet()) {
076                                try {
077                                        state.pushActiveContextObject(new TypedValue(entry));
078                                        state.enterScope();
079                                        result.add(this.children[0].getValueInternal(state).getValue());
080                                }
081                                finally {
082                                        state.popActiveContextObject();
083                                        state.exitScope();
084                                }
085                        }
086                        return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this);  // TODO unable to build correct type descriptor
087                }
088
089                if (operand instanceof Iterable || operandIsArray) {
090                        Iterable<?> data = (operand instanceof Iterable ?
091                                        (Iterable<?>) operand : Arrays.asList(ObjectUtils.toObjectArray(operand)));
092
093                        List<Object> result = new ArrayList<Object>();
094                        int idx = 0;
095                        Class<?> arrayElementType = null;
096                        for (Object element : data) {
097                                try {
098                                        state.pushActiveContextObject(new TypedValue(element));
099                                        state.enterScope("index", idx);
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                                idx++;
111                        }
112
113                        if (operandIsArray) {
114                                if (arrayElementType == null) {
115                                        arrayElementType = Object.class;
116                                }
117                                Object resultArray = Array.newInstance(arrayElementType, result.size());
118                                System.arraycopy(result.toArray(), 0, resultArray, 0, result.size());
119                                return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultArray),this);
120                        }
121
122                        return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this);
123                }
124
125                if (operand == null) {
126                        if (this.nullSafe) {
127                                return ValueRef.NullValueRef.INSTANCE;
128                        }
129                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, "null");
130                }
131
132                throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE,
133                                operand.getClass().getName());
134        }
135
136        @Override
137        public String toStringAST() {
138                return "![" + getChild(0).toStringAST() + "]";
139        }
140
141        private Class<?> determineCommonType(Class<?> oldType, Class<?> newType) {
142                if (oldType == null) {
143                        return newType;
144                }
145                if (oldType.isAssignableFrom(newType)) {
146                        return oldType;
147                }
148                Class<?> nextType = newType;
149                while (nextType != Object.class) {
150                        if (nextType.isAssignableFrom(oldType)) {
151                                return nextType;
152                        }
153                        nextType = nextType.getSuperclass();
154                }
155                for (Class<?> nextInterface : ClassUtils.getAllInterfacesForClassAsSet(newType)) {
156                        if (nextInterface.isAssignableFrom(oldType)) {
157                                return nextInterface;
158                        }
159                }
160                return Object.class;
161        }
162
163}