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.context.expression;
018
019import java.util.Map;
020
021import org.springframework.asm.MethodVisitor;
022import org.springframework.expression.AccessException;
023import org.springframework.expression.EvaluationContext;
024import org.springframework.expression.TypedValue;
025import org.springframework.expression.spel.CodeFlow;
026import org.springframework.expression.spel.CompilablePropertyAccessor;
027import org.springframework.lang.Nullable;
028import org.springframework.util.Assert;
029
030/**
031 * EL property accessor that knows how to traverse the keys
032 * of a standard {@link java.util.Map}.
033 *
034 * @author Juergen Hoeller
035 * @author Andy Clement
036 * @since 3.0
037 */
038public class MapAccessor implements CompilablePropertyAccessor {
039
040        @Override
041        public Class<?>[] getSpecificTargetClasses() {
042                return new Class<?>[] {Map.class};
043        }
044
045        @Override
046        public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
047                return (target instanceof Map && ((Map<?, ?>) target).containsKey(name));
048        }
049
050        @Override
051        public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
052                Assert.state(target instanceof Map, "Target must be of type Map");
053                Map<?, ?> map = (Map<?, ?>) target;
054                Object value = map.get(name);
055                if (value == null && !map.containsKey(name)) {
056                        throw new MapAccessException(name);
057                }
058                return new TypedValue(value);
059        }
060
061        @Override
062        public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
063                return true;
064        }
065
066        @Override
067        @SuppressWarnings("unchecked")
068        public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue)
069                        throws AccessException {
070
071                Assert.state(target instanceof Map, "Target must be a Map");
072                Map<Object, Object> map = (Map<Object, Object>) target;
073                map.put(name, newValue);
074        }
075
076        @Override
077        public boolean isCompilable() {
078                return true;
079        }
080
081        @Override
082        public Class<?> getPropertyType() {
083                return Object.class;
084        }
085
086        @Override
087        public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
088                String descriptor = cf.lastDescriptor();
089                if (descriptor == null || !descriptor.equals("Ljava/util/Map")) {
090                        if (descriptor == null) {
091                                cf.loadTarget(mv);
092                        }
093                        CodeFlow.insertCheckCast(mv, "Ljava/util/Map");
094                }
095                mv.visitLdcInsn(propertyName);
096                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true);
097        }
098
099
100        /**
101         * Exception thrown from {@code read} in order to reset a cached
102         * PropertyAccessor, allowing other accessors to have a try.
103         */
104        @SuppressWarnings("serial")
105        private static class MapAccessException extends AccessException {
106
107                private final String key;
108
109                public MapAccessException(String key) {
110                        super("");
111                        this.key = key;
112                }
113
114                @Override
115                public String getMessage() {
116                        return "Map does not contain a value for key '" + this.key + "'";
117                }
118        }
119
120}