From 059ac9fabbc8ce4ffa08b5365a2ed3894b03bdd6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 14 Dec 2015 22:32:48 -0600 Subject: [PATCH] DATAREDIS-425 - Add SpelIndexResolver This commit adds support for adding an index using SpEL expressions. Original PR #160 --- .../redis/core/convert/SpelIndexResolver.java | 120 +++++++++ .../redis/core/index/IndexConfiguration.java | 21 +- .../convert/SpelIndexResolverUnitTests.java | 236 ++++++++++++++++++ 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java create mode 100644 src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java diff --git a/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java new file mode 100644 index 000000000..8dd1ce9c8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.convert; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +/** + * An {@link IndexResolver} that resolves {@link IndexedData} using a {@link SpelExpressionParser}. + * + * @author Rob Winch + * @since 1.7 + */ +public class SpelIndexResolver implements IndexResolver { + + private final IndexConfiguration settings; + private final SpelExpressionParser parser; + private final RedisMappingContext mappingContext; + + private BeanResolver beanResolver; + + /** + * Creates a new instance using a default {@link SpelExpressionParser}. + * + * @param mappingContext the {@link RedisMappingContext} to use. Cannot be null. + */ + public SpelIndexResolver(RedisMappingContext mappingContext) { + this(mappingContext, new SpelExpressionParser()); + } + + /** + * Creates a new instance + * + * @param mappingContext the {@link RedisMappingContext} to use. Cannot be null. + * @param parser the {@link SpelExpressionParser} to use. Cannot be null. + */ + public SpelIndexResolver(RedisMappingContext mappingContext, SpelExpressionParser parser) { + + Assert.notNull(mappingContext, "RedisMappingContext must not be null!"); + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + this.mappingContext = mappingContext; + this.settings = mappingContext.getMappingConfiguration().getIndexConfiguration(); + this.parser = parser; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object) + */ + public Set resolveIndexesFor(TypeInformation typeInformation, Object value) { + + if (value == null) { + return Collections.emptySet(); + } + + KeyValuePersistentEntity entity = mappingContext.getPersistentEntity(typeInformation); + + if (entity == null) { + return Collections.emptySet(); + } + + String keyspace = entity.getKeySpace(); + + Set indexes = new HashSet(); + + for (RedisIndexSetting setting : settings.getIndexDefinitionsFor(keyspace)) { + + Expression expression = parser.parseExpression(setting.getPath()); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(value); + context.setVariable("this", value); + + if (beanResolver != null) { + context.setBeanResolver(beanResolver); + } + + Object index = expression.getValue(context); + if (index != null) { + indexes.add(new SimpleIndexedPropertyValue(keyspace, setting.getIndexName(), index)); + } + } + + return indexes; + } + + /** + * Allows setting the BeanResolver + * + * @param beanResolver can be {@literal null}. + * @see BeanFactoryResolver + */ + public void setBeanResolver(BeanResolver beanResolver) { + this.beanResolver = beanResolver; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java index ff59472cb..da33933b6 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,6 +86,25 @@ public class IndexConfiguration { return indexDefinitions; } + /** + * Gets all of the {@link RedisIndexSetting} for a given keyspace. + * + * @param keyspace the keyspace to get + * @return never {@literal null} + */ + public List getIndexDefinitionsFor(Serializable keyspace) { + + List indexDefinitions = new ArrayList(); + + for (RedisIndexSetting indexDef : definitions) { + if (indexDef.getKeyspace().equals(keyspace)) { + indexDefinitions.add(indexDef); + } + } + + return indexDefinitions; + } + /** * Add given {@link RedisIndexSetting}. * diff --git a/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java new file mode 100644 index 000000000..939105599 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.convert; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting; +import org.springframework.data.redis.core.index.IndexType; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.SpelEvaluationException; + +/** + * @author Rob Winch + * @author Christoph Strobl + */ +public class SpelIndexResolverUnitTests { + + String keyspace; + + String indexName; + + String username; + + SpelIndexResolver resolver; + + Session session; + + ClassTypeInformation typeInformation; + + String securityContextAttrName; + + RedisMappingContext mappingContext; + + KeyValuePersistentEntity entity; + + @Before + public void setup() { + + username = "rob"; + keyspace = "spring:session:sessions"; + indexName = "principalName"; + securityContextAttrName = "SPRING_SECURITY_CONTEXT"; + + typeInformation = ClassTypeInformation.from(Session.class); + session = createSession(); + + resolver = createWithExpression("getAttribute('" + securityContextAttrName + "')?.authentication?.name"); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void constructorNullRedisMappingContext() { + + mappingContext = null; + new SpelIndexResolver(mappingContext); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = IllegalArgumentException.class) + public void constructorNullSpelExpressionParser() { + new SpelIndexResolver(mappingContext, null); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void nullValue() { + + Set indexes = resolver.resolveIndexesFor(typeInformation, null); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void wrongKeyspace() { + + typeInformation = ClassTypeInformation.from(String.class); + Set indexes = resolver.resolveIndexesFor(typeInformation, ""); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void sessionAttributeNull() { + + session = new Session(); + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes.size(), equalTo(0)); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void resolvePrincipalName() { + + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(keyspace, indexName, username))); + } + + /** + * @see DATAREDIS-425 + */ + @Test(expected = SpelEvaluationException.class) + public void spelError() { + + session.setAttribute(securityContextAttrName, ""); + + resolver.resolveIndexesFor(typeInformation, session); + } + + /** + * @see DATAREDIS-425 + */ + @Test + public void withBeanAndThis() { + + this.resolver = createWithExpression("@bean.run(#this)"); + this.resolver.setBeanResolver(new BeanResolver() { + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + return new Object() { + @SuppressWarnings("unused") + public Object run(Object arg) { + return arg; + } + }; + } + }); + + Set indexes = resolver.resolveIndexesFor(typeInformation, session); + + assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(keyspace, indexName, session))); + } + + private SpelIndexResolver createWithExpression(String expression) { + + RedisIndexSetting principalIndex = new RedisIndexSetting(keyspace, expression, indexName, IndexType.SIMPLE); + IndexConfiguration configuration = new IndexConfiguration(); + configuration.addIndexDefinition(principalIndex); + + KeyspaceSettings keyspaceSettings = new KeyspaceSettings(Session.class, keyspace); + KeyspaceConfiguration keyspaceConfiguration = new KeyspaceConfiguration(); + keyspaceConfiguration.addKeyspaceSettings(keyspaceSettings); + + MappingConfiguration mapping = new MappingConfiguration(configuration, keyspaceConfiguration); + + mappingContext = new RedisMappingContext(mapping); + + return new SpelIndexResolver(mappingContext); + } + + private Session createSession() { + + Session session = new Session(); + session.setAttribute(securityContextAttrName, new SecurityContextImpl(new Authentication(username))); + return session; + } + + static class Session { + + private Map sessionAttrs = new HashMap(); + + public void setAttribute(String attrName, Object attrValue) { + this.sessionAttrs.put(attrName, attrValue); + } + + public Object getAttribute(String attributeName) { + return sessionAttrs.get(attributeName); + } + } + + static class SecurityContextImpl { + private final Authentication authentication; + + public SecurityContextImpl(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + } + + public static class Authentication { + private final String principalName; + + public Authentication(String principalName) { + this.principalName = principalName; + } + + public String getName() { + return principalName; + } + } +}