Vault repository support.

We now support Spring Data Repositories via Spring Data's KeyValue module. Domain objects can be mapped to JSON using a custom converter and created, update, deleted and queried using the repository abstraction. Vault repositories support query derivation limited to predicates on the Id property with paging and sorting.

@Configuration
@EnableVaultRepositories
public class ApplicationConfig {

  @Bean
  public VaultTemplate vaultTemplate() {
    return new VaultTemplate(…);
  }
}

@Test
public void loadAndSave() {

	Credentials heisenberg = new Credentials();
	heisenberg.setId("heisenberg");
	heisenberg.setPassword("327215");

	vaultRepository.save(heisenberg);

	Iterable<Credentials> all = vaultRepository.findAll();
  //
}

interface CredentialsRepository extends PagingAndSortingRepository<Credentials, String> {

}

@Data
public class Credentials {

	@Id String id;
	String password;
}

GET https://localhost:8200/v1/secret/credentials/heisenberg

HTTP/1.1 200 OK
Content-Type: application/json

{
  // …
  "renewable": false,
  "lease_duration": …,
  "data": {
    "_class": "com.example.Credentials",
    "password": "327215"
  },
  // …
}

See gh-128.
This commit is contained in:
Mark Paluch
2017-08-04 15:21:20 +02:00
parent 0ca5de03df
commit b916d4c6e7
43 changed files with 5064 additions and 37 deletions

71
pom.xml
View File

@@ -15,17 +15,20 @@
<modules>
<module>spring-vault-dependencies</module>
<module>spring-vault-core</module>
<module>spring-vault-repository</module>
<module>spring-vault-distribution</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.0.0.RC3</spring.version>
<spring-data-releasetrain.version>Kay-RC2</spring-data-releasetrain.version>
<reactor.version>Bismuth-M3</reactor.version>
<java.version>1.8</java.version>
<project.type>multi</project.type>
<dist.id>spring-vault</dist.id>
<project.root>${basedir}</project.root>
<shared.resources>${project.build.directory}/shared-resources</shared.resources>
</properties>
<inceptionYear>2016</inceptionYear>
@@ -116,6 +119,15 @@
<scope>import</scope>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-releasetrain</artifactId>
<version>${spring-data-releasetrain.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Testing -->
<dependency>
@@ -231,6 +243,12 @@
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
@@ -247,6 +265,26 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<configuration>
<breakiterator>true</breakiterator>
<header>${project.name}</header>
<source>${java.version}</source>
<quiet>true</quiet>
<javadocDirectory>${shared.resources}/javadoc</javadocDirectory>
<overview>${shared.resources}/javadoc/overview.html</overview>
<stylesheetfile>
${shared.resources}/javadoc/spring-javadoc.css
</stylesheetfile>
<!-- copies doc-files subdirectory which contains image resources -->
<docfilessubdirs>true</docfilessubdirs>
<additionalparam>-Xdoclint:none</additionalparam>
<links>
<link>https://docs.spring.io/spring-data/commons/docs/current/api</link>
<link>https://docs.spring.io/spring-data/keyvalue/docs/current/api</link>
<link>http://docs.spring.io/spring/docs/current/javadoc-api</link>
<link>http://docs.oracle.com/javase/8/docs/api</link>
</links>
</configuration>
</plugin>
<plugin>
@@ -471,8 +509,6 @@
<id>distribute</id>
<properties>
<shared.resources>${project.build.directory}/shared-resources
</shared.resources>
<maven.install.skip>true</maven.install.skip>
<skipTests>true</skipTests>
</properties>
@@ -612,37 +648,6 @@
<pluginManagement>
<plugins>
<!--
JavaDoc
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<breakiterator>true</breakiterator>
<header>${project.name}</header>
<source>${java.version}</source>
<quiet>true</quiet>
<javadocDirectory>${shared.resources}/javadoc
</javadocDirectory>
<overview>${shared.resources}/javadoc/overview.html
</overview>
<stylesheetfile>
${shared.resources}/javadoc/spring-javadoc.css
</stylesheetfile>
<!-- copies doc-files subdirectory which contains image resources -->
<docfilessubdirs>true</docfilessubdirs>
<additionalparam>-Xdoclint:none</additionalparam>
<links>
<link>
http://docs.spring.io/spring/docs/current/javadoc-api/
</link>
<link>http://docs.oracle.com/javase/6/docs/api</link>
</links>
</configuration>
</plugin>
<!--
Asciidoctor
-->

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -20,6 +22,20 @@
<targetPath>META-INF</targetPath>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>test-jar</id>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>

View File

@@ -53,7 +53,7 @@ public class AwsEc2Authentication implements ClientAuthentication,
private static final Log logger = LogFactory.getLog(AwsEc2Authentication.class);
private final static char[] EMPTY = new char[0];
private static final char[] EMPTY = new char[0];
private final AwsEc2AuthenticationOptions options;

View File

@@ -34,7 +34,7 @@ import org.springframework.vault.support.VaultToken;
*/
public class CachingVaultTokenSupplier implements VaultTokenSupplier {
private final static Mono<VaultToken> EMPTY = Mono.empty();
private static final Mono<VaultToken> EMPTY = Mono.empty();
private final VaultTokenSupplier clientAuthentication;

View File

@@ -40,7 +40,7 @@ import org.springframework.util.Assert;
*/
public class SslConfiguration {
private final static String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType();
private static final String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType();
private final KeyStoreConfiguration keyStoreConfiguration;

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-vault-repository</artifactId>
<name>Spring Vault Repository</name>
<description>Spring Vault Repository support</description>
<packaging>jar</packaging>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<targetPath>META-INF</targetPath>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2017 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.vault.repository.configuration;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.vault.repository.query.VaultQueryCreator;
import org.springframework.vault.repository.support.VaultRepositoryFactoryBean;
/**
* Annotation to activate Vault repositories. If no base package is configured through
* either {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it
* will trigger scanning of the package of annotated class.
*
* @author Mark Paluch
* @since 2.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(VaultRepositoriesRegistrar.class)
@QueryCreatorType(VaultQueryCreator.class)
public @interface EnableVaultRepositories {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @EnableVaultRepositories("org.my.pkg")} instead of
* {@code @EnableVaultRepositories(basePackages="org.my.pkg")}.
*/
String[] value() default {};
/**
* Base packages to scan for annotated components. {@link #value()} is an alias for
* (and mutually exclusive with) this attribute. Use {@link #basePackageClasses()} for
* a type-safe alternative to String-based package names.
*/
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
*/
Class<?>[] basePackageClasses() default {};
/**
* Specifies which types are not eligible for component scanning.
*/
Filter[] excludeFilters() default {};
/**
* Specifies which types are eligible for component scanning. Further narrows the set
* of candidate components from everything in {@link #basePackages()} to everything in
* the base packages that matches the given filter or filters.
*/
Filter[] includeFilters() default {};
/**
* Returns the postfix to be used when looking up custom repository implementations.
* Defaults to {@literal Impl}. So for a repository named {@code PersonRepository} the
* corresponding implementation class will be looked up scanning for
* {@code PersonRepositoryImpl}.
*/
String repositoryImplementationPostfix() default "Impl";
/**
* Configures the location of where to find the Spring Data named queries properties
* file.
*/
String namedQueriesLocation() default "";
/**
* Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries
* for query methods. Defaults to {@link Key#CREATE_IF_NOT_FOUND}.
*/
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
/**
* Returns the {@link FactoryBean} class to be used for each repository instance.
* Defaults to {@link VaultRepositoryFactoryBean}.
*/
Class<?> repositoryFactoryBeanClass() default VaultRepositoryFactoryBean.class;
/**
* Configure the repository base class to be used to create repository proxies for
* this particular configuration.
*/
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
/**
* Configures the name of the {@link KeyValueOperations} bean to be used with the
* repositories detected.
*/
String keyValueTemplateRef() default "vaultKeyValueTemplate";
/**
* Configures whether nested repository-interfaces (e.g. defined as inner classes)
* should be discovered by the repositories infrastructure.
*/
boolean considerNestedRepositories() default false;
/**
* Configures the bean name of the
* {@link org.springframework.vault.core.VaultOperations} to be used. Defaulted to
* {@literal vaultTemplate}.
*/
String vaultTemplateRef() default "vaultTemplate";
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2017 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.vault.repository.configuration;
import java.lang.annotation.Annotation;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
/**
* Vault specific {@link ImportBeanDefinitionRegistrar}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableVaultRepositories.class;
}
@Override
protected RepositoryConfigurationExtension getExtension() {
return new VaultRepositoryConfigurationExtension();
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2017 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.vault.repository.configuration;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension;
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.vault.repository.core.VaultKeyValueAdapter;
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
import org.springframework.vault.repository.mapping.Secret;
import org.springframework.vault.repository.mapping.VaultMappingContext;
/**
* {@link RepositoryConfigurationExtension} for Vault.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultRepositoryConfigurationExtension extends
KeyValueRepositoryConfigurationExtension {
private static final String VAULT_ADAPTER_BEAN_NAME = "vaultKeyValueAdapter";
private static final String VAULT_MAPPING_CONTEXT_BEAN_NAME = "vaultMappingContext";
@Override
public String getModuleName() {
return "Vault";
}
@Override
protected String getModulePrefix() {
return "vault";
}
@Override
protected String getDefaultKeyValueTemplateRef() {
return "vaultKeyValueTemplate";
}
// TODO: Remove this as soon as KeyValue provides overrides for the mapping context
// name.
@Override
public void postProcess(BeanDefinitionBuilder builder,
AnnotationRepositoryConfigurationSource config) {
AnnotationAttributes attributes = config.getAttributes();
builder.addPropertyReference("keyValueOperations",
attributes.getString(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE));
builder.addPropertyValue("queryCreator", getQueryCreatorType(config));
builder.addPropertyValue("queryType", getQueryType(config));
builder.addPropertyReference("mappingContext", VAULT_MAPPING_CONTEXT_BEAN_NAME);
}
/**
* Detects the query creator type to be used for the factory to set. Will lookup a
* {@link QueryCreatorType} annotation on the {@code @Enable}-annotation or use
* {@link SpelQueryCreator} if not found.
*
* @param config
* @return
*/
private static Class<?> getQueryCreatorType(
AnnotationRepositoryConfigurationSource config) {
AnnotationMetadata metadata = config.getEnableAnnotationMetadata();
Map<String, Object> queryCreatorAnnotationAttributes = metadata
.getAnnotationAttributes(QueryCreatorType.class.getName());
if (queryCreatorAnnotationAttributes == null) {
return SpelQueryCreator.class;
}
AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes(
queryCreatorAnnotationAttributes);
return queryCreatorAttributes.getClass("value");
}
/**
* Detects the query creator type to be used for the factory to set. Will lookup a
* {@link QueryCreatorType} annotation on the {@code @Enable}-annotation or use
* {@link SpelQueryCreator} if not found.
*
* @param config
* @return
*/
private static Class<?> getQueryType(AnnotationRepositoryConfigurationSource config) {
AnnotationMetadata metadata = config.getEnableAnnotationMetadata();
Map<String, Object> queryCreatorAnnotationAttributes = metadata
.getAnnotationAttributes(QueryCreatorType.class.getName());
if (queryCreatorAnnotationAttributes == null) {
return KeyValuePartTreeQuery.class;
}
AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes(
queryCreatorAnnotationAttributes);
return queryCreatorAttributes.getClass("repositoryQueryType");
}
@Override
public void registerBeansForRoot(BeanDefinitionRegistry registry,
RepositoryConfigurationSource configurationSource) {
Optional<String> vaultTemplateRef = configurationSource
.getAttribute("vaultTemplateRef");
RootBeanDefinition mappingContextDefinition = createVaultMappingContext(configurationSource);
mappingContextDefinition.setSource(configurationSource.getSource());
registerIfNotAlreadyRegistered(mappingContextDefinition, registry,
VAULT_MAPPING_CONTEXT_BEAN_NAME, configurationSource);
// register Adapter
RootBeanDefinition vaultKeyValueAdapterDefinition = new RootBeanDefinition(
VaultKeyValueAdapter.class);
ConstructorArgumentValues constructorArgumentValuesForVaultKeyValueAdapter = new ConstructorArgumentValues();
constructorArgumentValuesForVaultKeyValueAdapter.addIndexedArgumentValue(0,
new RuntimeBeanReference(vaultTemplateRef.orElse("vaultTemplate")));
vaultKeyValueAdapterDefinition
.setConstructorArgumentValues(constructorArgumentValuesForVaultKeyValueAdapter);
registerIfNotAlreadyRegistered(vaultKeyValueAdapterDefinition, registry,
VAULT_ADAPTER_BEAN_NAME, configurationSource);
Optional<String> keyValueTemplateName = configurationSource
.getAttribute(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE);
// No custom template reference configured and no matching bean definition found
if (keyValueTemplateName.isPresent()
&& getDefaultKeyValueTemplateRef().equals(keyValueTemplateName.get())
&& !registry.containsBeanDefinition(keyValueTemplateName.get())) {
AbstractBeanDefinition beanDefinition = getDefaultKeyValueTemplateBeanDefinition(configurationSource);
if (beanDefinition != null) {
registerIfNotAlreadyRegistered(beanDefinition, registry,
keyValueTemplateName.get(), configurationSource.getSource());
}
}
}
private RootBeanDefinition createVaultMappingContext(
RepositoryConfigurationSource configurationSource) {
ConstructorArgumentValues mappingContextArgs = new ConstructorArgumentValues();
RootBeanDefinition mappingContextBeanDef = new RootBeanDefinition(
VaultMappingContext.class);
mappingContextBeanDef.setConstructorArgumentValues(mappingContextArgs);
return mappingContextBeanDef;
}
@Override
protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(
RepositoryConfigurationSource configurationSource) {
RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(
VaultKeyValueTemplate.class);
ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();
constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0,
new RuntimeBeanReference(VAULT_ADAPTER_BEAN_NAME));
constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1,
new RuntimeBeanReference(VAULT_MAPPING_CONTEXT_BEAN_NAME));
keyValueTemplateDefinition
.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate);
return keyValueTemplateDefinition;
}
@Override
protected Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
return Collections.<Class<? extends Annotation>> singleton(Secret.class);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2017 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.vault.repository.convert;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityInstantiators;
/**
* Base class for {@link VaultConverter} implementations. Sets up a
* {@link GenericConversionService} and populates basic converters.
*
* @author Mark Paluch
* @since 2.0
*/
public abstract class AbstractVaultConverter implements VaultConverter, InitializingBean {
protected final GenericConversionService conversionService;
protected CustomConversions conversions = new VaultCustomConversions();
protected EntityInstantiators instantiators = new EntityInstantiators();
/**
* Creates a new {@link AbstractVaultConverter} using the given
* {@link GenericConversionService}.
*
* @param conversionService must not be {@literal null}.
*/
public AbstractVaultConverter(GenericConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Registers the given custom conversions with the converter.
*
* @param conversions
*/
public void setCustomConversions(CustomConversions conversions) {
this.conversions = conversions;
}
/**
* Registers {@link EntityInstantiators} to customize entity instantiation.
*
* @param instantiators
*/
public void setInstantiators(EntityInstantiators instantiators) {
this.instantiators = instantiators;
}
@Override
public ConversionService getConversionService() {
return conversionService;
}
@Override
public void afterPropertiesSet() {
initializeConverters();
}
/**
* Registers additional converters that will be available when using the
* {@link ConversionService} directly (e.g. for id conversion).
*/
private void initializeConverters() {
conversions.registerConvertersIn(conversionService);
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.convert.TypeInformationMapper;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* Default implementation of {@link VaultTypeMapper} allowing configuration of the key to
* lookup and store type information in {@link SecretDocument}. The key defaults to
* {@link #DEFAULT_TYPE_KEY}. Actual type-to-{@link String} conversion and back is done in
* {@link #readType(Object)} or {@link #getDefaultedTypeToBeUsed(Object)}. respectively.
*
* @author Mark Paluch
* @since 2.0
*/
public class DefaultVaultTypeMapper extends DefaultTypeMapper<Map<String, Object>>
implements VaultTypeMapper {
public static final String DEFAULT_TYPE_KEY = "_class";
@SuppressWarnings("rawtypes")
private static final TypeInformation<Map> MAP_TYPE_INFO = ClassTypeInformation
.from(Map.class);
private final @Nullable String typeKey;
/**
* Creates a default {@link VaultTypeMapper} that exchanges types using the type key
* {@literal _class}.
*/
public DefaultVaultTypeMapper() {
this(DEFAULT_TYPE_KEY);
}
/**
* Creates a default {@link VaultTypeMapper} that exchanges types using the given
* {@code typeKey}.
*
* @param typeKey may not be {@literal null} to disable type hinting.
*/
public DefaultVaultTypeMapper(@Nullable String typeKey) {
this(typeKey, Collections.singletonList(new SimpleTypeInformationMapper()));
}
/**
* Creates a default {@link VaultTypeMapper} that exchanges types using the given
* {@code typeKey} and {@link MappingContext}.
*
* @param typeKey may not be {@literal null} to disable type hinting.
* @param mappingContext must not be {@literal null} or empty.
*/
public DefaultVaultTypeMapper(@Nullable String typeKey,
MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext) {
this(typeKey, new SecretDocumentTypeAliasAccessor(typeKey), mappingContext,
Collections.singletonList(new SimpleTypeInformationMapper()));
}
public DefaultVaultTypeMapper(@Nullable String typeKey,
List<? extends TypeInformationMapper> mappers) {
this(typeKey, new SecretDocumentTypeAliasAccessor(typeKey), null, mappers);
}
private DefaultVaultTypeMapper(@Nullable String typeKey,
TypeAliasAccessor<Map<String, Object>> accessor,
MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
List<? extends TypeInformationMapper> mappers) {
super(accessor, mappingContext, mappers);
this.typeKey = typeKey;
}
/**
* Checks whether the given key name matches the {@literal typeKey}.
*
* @param key
* @return {@literal true} if {@code key} matches the {@literal typeKey}.
*/
public boolean isTypeKey(String key) {
return typeKey != null && typeKey.equals(key);
}
@Override
protected TypeInformation<?> getFallbackTypeFor(Map<String, Object> source) {
return MAP_TYPE_INFO;
}
/**
* {@link TypeAliasAccessor} to store aliases in a {@link SecretDocument}.
*
* @author Mark Paluch
*/
static class SecretDocumentTypeAliasAccessor implements
TypeAliasAccessor<Map<String, Object>> {
private final @Nullable String typeKey;
SecretDocumentTypeAliasAccessor(@Nullable String typeKey) {
this.typeKey = typeKey;
}
public Alias readAliasFrom(Map<String, Object> source) {
return typeKey == null ? Alias.NONE : Alias.ofNullable(source.get(typeKey));
}
public void writeTypeTo(Map<String, Object> sink, Object alias) {
if (typeKey != null) {
sink.put(typeKey, alias);
}
}
}
}

View File

@@ -0,0 +1,757 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
import org.springframework.vault.repository.mapping.VaultPersistentProperty;
/**
* {@link VaultConverter} that uses a {@link MappingContext} to do sophisticated mapping
* of domain objects to {@link SecretDocument}. This converter converts between Map-typed
* representations and domain objects without use of a JSON library.
* {@link SecretDocument} is the input to JSON mapping to exchange secrets with Vault.
*
* @author Mark Paluch
* @since 2.0
*/
public class MappingVaultConverter extends AbstractVaultConverter {
private final MappingContext<? extends VaultPersistentEntity<?>, VaultPersistentProperty> mappingContext;
private VaultTypeMapper typeMapper;
public MappingVaultConverter(
MappingContext<? extends VaultPersistentEntity<?>, VaultPersistentProperty> mappingContext) {
super(new DefaultConversionService());
Assert.notNull(mappingContext, "MappingContext must not be null");
this.mappingContext = mappingContext;
this.typeMapper = new DefaultVaultTypeMapper(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, mappingContext);
}
/**
* Configures the {@link VaultTypeMapper} to be used to add type information to
* {@link SecretDocument}s created by the converter and how to lookup type information
* from {@link SecretDocument}s when reading them. Uses a
* {@link DefaultVaultTypeMapper} by default. Setting this to {@literal null} will
* reset the {@link org.springframework.data.convert.TypeMapper} to the default one.
*
* @param typeMapper the typeMapper to set, must not be {@literal null}.
*/
public void setTypeMapper(VaultTypeMapper typeMapper) {
Assert.notNull(typeMapper, "VaultTypeMapper must not be null");
this.typeMapper = typeMapper;
}
@Override
public MappingContext<? extends VaultPersistentEntity<?>, VaultPersistentProperty> getMappingContext() {
return mappingContext;
}
@Override
public <S> S read(Class<S> type, SecretDocument source) {
return read(ClassTypeInformation.from(type), source);
}
@SuppressWarnings("unchecked")
private <S> S read(TypeInformation<S> type, Object source) {
SecretDocument secretDocument = getSecretDocument(source);
TypeInformation<? extends S> typeToUse = secretDocument != null ? typeMapper
.readType(secretDocument.getBody(), type)
: (TypeInformation) ClassTypeInformation.OBJECT;
Class<? extends S> rawType = typeToUse.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return conversionService.convert(source, rawType);
}
if (SecretDocument.class.isAssignableFrom(rawType)) {
return (S) source;
}
if (Map.class.isAssignableFrom(rawType) && secretDocument != null) {
return (S) secretDocument.getBody();
}
if (typeToUse.isMap() && secretDocument != null) {
return (S) readMap(typeToUse, secretDocument.getBody());
}
if (typeToUse.equals(ClassTypeInformation.OBJECT)) {
return (S) source;
}
return read(
(VaultPersistentEntity<S>) mappingContext
.getRequiredPersistentEntity(typeToUse),
secretDocument);
}
@Nullable
@SuppressWarnings("unchecked")
private SecretDocument getSecretDocument(Object source) {
SecretDocument secretDocument = null;
if (source instanceof Map) {
secretDocument = new SecretDocument((Map) source);
}
else if (source instanceof SecretDocument) {
secretDocument = (SecretDocument) source;
}
return secretDocument;
}
private ParameterValueProvider<VaultPersistentProperty> getParameterProvider(
VaultPersistentEntity<?> entity, SecretDocument source) {
VaultPropertyValueProvider provider = new VaultPropertyValueProvider(source);
PersistentEntityParameterValueProvider<VaultPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
entity, provider, source);
return new ParameterValueProvider<VaultPersistentProperty>() {
@Nullable
@Override
public <T> T getParameterValue(Parameter<T, VaultPersistentProperty> parameter) {
Object value = parameterProvider.getParameterValue(parameter);
return value != null ? readValue(value, parameter.getType()) : null;
}
};
}
private <S> S read(VaultPersistentEntity<S> entity, SecretDocument source) {
ParameterValueProvider<VaultPersistentProperty> provider = getParameterProvider(
entity, source);
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
S instance = instantiator.createInstance(entity, provider);
PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(
entity.getPropertyAccessor(instance), conversionService);
VaultPersistentProperty idProperty = entity.getIdProperty();
SecretDocumentAccessor documentAccessor = new SecretDocumentAccessor(source);
// make sure id property is set before all other properties
Object idValue;
if (idProperty != null && documentAccessor.hasValue(idProperty)) {
idValue = readIdValue(idProperty, documentAccessor);
accessor.setProperty(idProperty, idValue);
}
VaultPropertyValueProvider valueProvider = new VaultPropertyValueProvider(
documentAccessor);
readProperties(entity, accessor, idProperty, documentAccessor, valueProvider);
return instance;
}
@Nullable
private Object readIdValue(VaultPersistentProperty idProperty,
SecretDocumentAccessor documentAccessor) {
Object resolvedValue = documentAccessor.get(idProperty);
return resolvedValue != null ? readValue(resolvedValue,
idProperty.getTypeInformation()) : null;
}
private void readProperties(VaultPersistentEntity<?> entity,
PersistentPropertyAccessor accessor,
@Nullable VaultPersistentProperty idProperty,
SecretDocumentAccessor documentAccessor,
VaultPropertyValueProvider valueProvider) {
for (VaultPersistentProperty prop : entity) {
// we skip the id property since it was already set
if (idProperty != null && idProperty.equals(prop)) {
continue;
}
if (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop)) {
continue;
}
accessor.setProperty(prop, valueProvider.getPropertyValue(prop));
}
}
@Nullable
@SuppressWarnings("unchecked")
private <T> T readValue(Object value, TypeInformation<?> type) {
Class<?> rawType = type.getType();
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
return (T) conversionService.convert(value, rawType);
}
else if (value instanceof List) {
return (T) readCollectionOrArray(type, (List) value);
}
else if (value instanceof Map) {
return (T) read(type, (Map) value);
}
else {
return (T) getPotentiallyConvertedSimpleRead(value, rawType);
}
}
/**
* Reads the given {@link List} into a collection of the given {@link TypeInformation}
* .
*
* @param targetType must not be {@literal null}.
* @param sourceValue must not be {@literal null}.
* @return the converted {@link Collection} or array, will never be {@literal null}.
*/
@Nullable
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object readCollectionOrArray(TypeInformation<?> targetType, List sourceValue) {
Assert.notNull(targetType, "Target type must not be null!");
Class<?> collectionType = targetType.getType();
TypeInformation<?> componentType = targetType.getComponentType() != null ? targetType
.getComponentType() : ClassTypeInformation.OBJECT;
Class<?> rawComponentType = componentType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType
: List.class;
Collection<Object> items = targetType.getType().isArray() ? new ArrayList<>(
sourceValue.size()) : CollectionFactory.createCollection(collectionType,
rawComponentType, sourceValue.size());
if (sourceValue.isEmpty()) {
return getPotentiallyConvertedSimpleRead(items, collectionType);
}
for (Object obj : sourceValue) {
if (obj instanceof Map) {
items.add(read(componentType, (Map) obj));
}
else if (obj instanceof List) {
items.add(readCollectionOrArray(ClassTypeInformation.OBJECT, (List) obj));
}
else {
items.add(getPotentiallyConvertedSimpleRead(obj, rawComponentType));
}
}
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
}
/**
* Reads the given {@link Map} into a {@link Map}. will recursively resolve nested
* {@link Map}s as well.
*
* @param type the {@link Map} {@link TypeInformation} to be used to unmarshall this
* {@link Map}.
* @param sourceMap must not be {@literal null}
* @return
*/
@SuppressWarnings("unchecked")
protected Map<Object, Object> readMap(TypeInformation<?> type,
Map<String, Object> sourceMap) {
Assert.notNull(sourceMap, "Source map must not be null!");
Class<?> mapType = typeMapper.readType(sourceMap, type).getType();
TypeInformation<?> keyType = type.getComponentType();
TypeInformation<?> valueType = type.getMapValueType();
Class<?> rawKeyType = keyType != null ? keyType.getType() : null;
Class<?> rawValueType = valueType != null ? valueType.getType() : null;
Map<Object, Object> map = CollectionFactory.createMap(mapType, rawKeyType,
sourceMap.keySet().size());
for (Entry<String, Object> entry : sourceMap.entrySet()) {
if (typeMapper.isTypeKey(entry.getKey())) {
continue;
}
Object key = entry.getKey();
if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) {
key = conversionService.convert(key, rawKeyType);
}
Object value = entry.getValue();
TypeInformation<?> defaultedValueType = valueType != null ? valueType
: ClassTypeInformation.OBJECT;
if (value instanceof Map) {
map.put(key, read(defaultedValueType, (Map) value));
}
else if (value instanceof List) {
map.put(key,
readCollectionOrArray(valueType != null ? valueType
: ClassTypeInformation.LIST, (List) value));
}
else {
map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType));
}
}
return map;
}
/**
* Checks whether we have a custom conversion for the given simple object. Converts
* the given value if so, applies {@link Enum} handling or returns the value as is.
*
* @param value
* @param target must not be {@literal null}.
* @return
*/
@Nullable
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object getPotentiallyConvertedSimpleRead(@Nullable Object value,
@Nullable Class<?> target) {
if (value == null || target == null || target.isAssignableFrom(value.getClass())) {
return value;
}
if (Enum.class.isAssignableFrom(target)) {
return Enum.valueOf((Class<Enum>) target, value.toString());
}
return conversionService.convert(value, target);
}
@Override
public void write(Object source, SecretDocument sink) {
Class<?> entityType = ClassUtils.getUserClass(source.getClass());
TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType);
SecretDocumentAccessor documentAccessor = new SecretDocumentAccessor(sink);
writeInternal(source, documentAccessor, type);
boolean handledByCustomConverter = conversions.hasCustomWriteTarget(entityType,
SecretDocument.class);
if (!handledByCustomConverter) {
typeMapper.writeType(type, sink.getBody());
}
}
/**
* Internal write conversion method which should be used for nested invocations.
*
* @param obj
* @param sink
* @param typeHint
*/
@SuppressWarnings("unchecked")
protected void writeInternal(Object obj, SecretDocumentAccessor sink,
@Nullable TypeInformation<?> typeHint) {
Class<?> entityType = obj.getClass();
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(entityType,
SecretDocument.class);
if (customTarget.isPresent()) {
SecretDocument result = conversionService.convert(obj, SecretDocument.class);
if (result.getId() != null) {
sink.setId(result.getId());
}
sink.getBody().putAll(result.getBody());
return;
}
if (Map.class.isAssignableFrom(entityType)) {
writeMapInternal((Map<Object, Object>) obj, sink.getBody(),
ClassTypeInformation.MAP);
return;
}
VaultPersistentEntity<?> entity = mappingContext
.getRequiredPersistentEntity(entityType);
writeInternal(obj, sink, entity);
addCustomTypeKeyIfNecessary(typeHint, obj, sink);
}
protected void writeInternal(Object obj, SecretDocumentAccessor sink,
VaultPersistentEntity<?> entity) {
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(obj);
VaultPersistentProperty idProperty = entity.getIdProperty();
if (idProperty != null && !sink.hasValue(idProperty)) {
Object value = accessor.getProperty(idProperty);
if (value != null) {
sink.put(idProperty, value);
}
}
writeProperties(entity, accessor, sink, idProperty);
}
private void writeProperties(VaultPersistentEntity<?> entity,
PersistentPropertyAccessor accessor, SecretDocumentAccessor sink,
@Nullable VaultPersistentProperty idProperty) {
// Write the properties
for (VaultPersistentProperty prop : entity) {
if (prop.equals(idProperty) || !prop.isWritable()) {
continue;
}
Object value = accessor.getProperty(prop);
if (value == null) {
continue;
}
if (!conversions.isSimpleType(value.getClass())) {
writePropertyInternal(value, sink, prop);
}
else {
sink.put(prop, getPotentiallyConvertedSimpleWrite(value));
}
}
}
@SuppressWarnings({ "unchecked" })
protected void writePropertyInternal(@Nullable Object obj,
SecretDocumentAccessor accessor, VaultPersistentProperty prop) {
if (obj == null) {
return;
}
TypeInformation<?> valueType = ClassTypeInformation.from(obj.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (valueType.isCollectionLike()) {
List<Object> collectionInternal = createCollection(asCollection(obj), prop);
accessor.put(prop, collectionInternal);
return;
}
if (valueType.isMap()) {
Map<String, Object> mapDbObj = createMap((Map<Object, Object>) obj, prop);
accessor.put(prop, mapDbObj);
return;
}
// Lookup potential custom target type
Optional<Class<?>> basicTargetType = conversions.getCustomWriteTarget(obj
.getClass());
if (basicTargetType.isPresent()) {
accessor.put(prop, conversionService.convert(obj, basicTargetType.get()));
return;
}
VaultPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass()) ? mappingContext
.getRequiredPersistentEntity(obj.getClass()) : mappingContext
.getRequiredPersistentEntity(type);
SecretDocumentAccessor nested = accessor.writeNested(prop);
writeInternal(obj, nested, entity);
addCustomTypeKeyIfNecessary(ClassTypeInformation.from(prop.getRawType()), obj,
nested);
}
private static boolean isSubtype(Class<?> left, Class<?> right) {
return left.isAssignableFrom(right) && !left.equals(right);
}
/**
* Writes the given {@link Collection} using the given {@link VaultPersistentProperty}
* information.
*
* @param collection must not be {@literal null}.
* @param property must not be {@literal null}.
* @return
*/
protected List<Object> createCollection(Collection<?> collection,
VaultPersistentProperty property) {
return writeCollectionInternal(collection, property.getTypeInformation(),
new ArrayList<>());
}
/**
* Populates the given {@link List} with values from the given {@link Collection}.
*
* @param source the collection to create a {@link List} for, must not be
* {@literal null}.
* @param type the {@link TypeInformation} to consider or {@literal null} if unknown.
* @param sink the {@link List} to write to.
* @return
*/
private List<Object> writeCollectionInternal(Collection<?> source,
@Nullable TypeInformation<?> type, List<Object> sink) {
TypeInformation<?> componentType = null;
if (type != null) {
componentType = type.getComponentType();
}
for (Object element : source) {
Class<?> elementType = element == null ? null : element.getClass();
if (elementType == null || conversions.isSimpleType(elementType)) {
sink.add(getPotentiallyConvertedSimpleWrite(element));
}
else if (element instanceof Collection || elementType.isArray()) {
sink.add(writeCollectionInternal(asCollection(element), componentType,
new ArrayList<>()));
}
else {
SecretDocumentAccessor accessor = new SecretDocumentAccessor(
new SecretDocument());
writeInternal(element, accessor, componentType);
sink.add(accessor.getBody());
}
}
return sink;
}
/**
* Writes the given {@link Map} using the given {@link VaultPersistentProperty}
* information.
*
* @param map must not {@literal null}.
* @param property must not be {@literal null}.
* @return
*/
protected Map<String, Object> createMap(Map<Object, Object> map,
VaultPersistentProperty property) {
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(property, "PersistentProperty must not be null!");
return writeMapInternal(map, new LinkedHashMap<>(), property.getTypeInformation());
}
/**
* Writes the given {@link Map} to the given {@link Map} considering the given
* {@link TypeInformation}.
*
* @param obj must not be {@literal null}.
* @param bson must not be {@literal null}.
* @param propertyType must not be {@literal null}.
* @return
*/
protected Map<String, Object> writeMapInternal(Map<Object, Object> obj,
Map<String, Object> bson, TypeInformation<?> propertyType) {
for (Map.Entry<Object, Object> entry : obj.entrySet()) {
Object key = entry.getKey();
Object val = entry.getValue();
if (conversions.isSimpleType(key.getClass())) {
String simpleKey = key.toString();
if (val == null || conversions.isSimpleType(val.getClass())) {
bson.put(simpleKey, val);
}
else if (val instanceof Collection || val.getClass().isArray()) {
bson.put(
simpleKey,
writeCollectionInternal(asCollection(val),
propertyType.getMapValueType(), new ArrayList<>()));
}
else {
SecretDocumentAccessor nested = new SecretDocumentAccessor(
new SecretDocument());
TypeInformation<?> valueTypeInfo = propertyType.isMap() ? propertyType
.getMapValueType() : ClassTypeInformation.OBJECT;
writeInternal(val, nested, valueTypeInfo);
bson.put(simpleKey, nested.getBody());
}
}
else {
throw new MappingException("Cannot use a complex object as a key value.");
}
}
return bson;
}
/**
* Adds custom type information to the given {@link SecretDocument} if necessary. That
* is if the value is not the same as the one given. This is usually the case if you
* store a subtype of the actual declared type of the property.
*
* @param type
* @param value must not be {@literal null}.
* @param accessor must not be {@literal null}.
*/
protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation<?> type,
Object value, SecretDocumentAccessor accessor) {
Class<?> reference = type != null ? type.getActualType().getType() : Object.class;
Class<?> valueType = ClassUtils.getUserClass(value.getClass());
boolean notTheSameClass = !valueType.equals(reference);
if (notTheSameClass) {
typeMapper.writeType(valueType, accessor.getBody());
}
}
/**
* Checks whether we have a custom conversion registered for the given value into an
* arbitrary simple Vault type. Returns the converted value if so. If not, we perform
* special enum handling or simply return the value as is.
*
* @param value
* @return
*/
@Nullable
private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) {
if (value == null) {
return null;
}
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(value
.getClass());
if (customTarget.isPresent()) {
return conversionService.convert(value, customTarget.get());
}
if (ObjectUtils.isArray(value)) {
if (value instanceof byte[]) {
return value;
}
return asCollection(value);
}
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name()
: value;
}
/**
* Returns given object as {@link Collection}. Will return the {@link Collection} as
* is if the source is a {@link Collection} already, will convert an array into a
* {@link Collection} or simply create a single element collection for everything
* else.
*
* @param source
* @return
*/
private static Collection<?> asCollection(Object source) {
if (source instanceof Collection) {
return (Collection<?>) source;
}
return source.getClass().isArray() ? CollectionUtils.arrayToList(source)
: Collections.singleton(source);
}
/**
* {@link PropertyValueProvider} to evaluate a SpEL expression if present on the
* property or simply accesses the field of the configured source
* {@link SecretDocument}.
*
*/
class VaultPropertyValueProvider implements
PropertyValueProvider<VaultPersistentProperty> {
private final SecretDocumentAccessor source;
VaultPropertyValueProvider(SecretDocument source) {
Assert.notNull(source, "Source document must no be null!");
this.source = new SecretDocumentAccessor(source);
}
VaultPropertyValueProvider(SecretDocumentAccessor accessor) {
Assert.notNull(accessor, "SecretDocumentAccessor must no be null!");
this.source = accessor;
}
@Nullable
public <T> T getPropertyValue(VaultPersistentProperty property) {
Object value = source.get(property);
if (value == null) {
return null;
}
return readValue(value, property.getTypeInformation());
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.vault.support.VaultResponse;
/**
* Vault database exchange object containing data before/after it's exchanged with Vault.
* A {@link SecretDocument} is basically an object with an {@code id} and a body
* represented as {@link Map} of {@link String} and {@link Object}. It can be created
* {@link #from(String, VaultResponse) from} an Id and {@link VaultResponse}.
* <p>
* A secret document can hold simple properties, {@link java.util.Collection list}
* properties and nested objects as {@link Map}s.
*
* @author Mark Paluch
* @since 2.0
*/
@EqualsAndHashCode
@ToString
public class SecretDocument {
private @Nullable String id;
private final Map<String, Object> body;
/**
* Create a new, empty {@link SecretDocument}.
*/
public SecretDocument() {
this(null, new LinkedHashMap<>());
}
/**
* Create a new {@link SecretDocument} given a {@link Map body map}.
* @param body must not be {@literal null}.
*/
public SecretDocument(Map<String, Object> body) {
this(null, body);
}
/**
* Create a new {@link SecretDocument} given an {@code id} and {@link Map body map}.
* @param id may be {@literal null}.
* @param body must not be {@literal null}.
*/
public SecretDocument(@Nullable String id, Map<String, Object> body) {
Assert.notNull(body, "Body must not be null");
this.id = id;
this.body = body;
}
public SecretDocument(String id) {
this(id, new LinkedHashMap<>());
}
/**
* Factory method to create a {@link SecretDocument} from an {@code id} and
* {@link VaultResponse}.
*
* @param id must not be {@literal null}.
* @param vaultResponse must not be {@literal null}.
* @return the {@link SecretDocument}.
*/
@SuppressWarnings("ConstantConditions")
public static SecretDocument from(@Nullable String id, VaultResponse vaultResponse) {
return new SecretDocument(id, vaultResponse.getData());
}
/**
* @return the Id or {@literal null} if the Id is not set.
*/
@Nullable
public String getId() {
return id;
}
/**
* Set the Id.
*
* @param id may be {@literal null}.
*/
public void setId(@Nullable String id) {
this.id = id;
}
/**
* @return the body of this {@link SecretDocument}
*/
public Map<String, Object> getBody() {
return body;
}
/**
* Retrieve a value from the secret document by its {@code key}.
*
* @param key must not be {@literal null}.
* @return the value or {@literal null}, if the value is not present.
*/
@Nullable
public Object get(String key) {
return body.get(key);
}
/**
* Set a value in the secret document.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
*/
public void put(String key, Object value) {
this.body.put(key, value);
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.repository.mapping.VaultPersistentProperty;
/**
* Wrapper value object for a {@link SecretDocument} to be able to access raw values by
* {@link VaultPersistentProperty} references. The accessors will transparently resolve
* nested document values that a {@link VaultPersistentProperty} might refer to through a
* path expression in field names.
*
* @author Mark Paluch
* @since 2.0
*/
class SecretDocumentAccessor {
private final SecretDocument document;
private final Map<String, Object> body;
/**
* Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument}.
*
* @param document must be a {@link SecretDocument} effectively, must not be
* {@literal null}.
*/
SecretDocumentAccessor(SecretDocument document) {
Assert.notNull(document, "SecretDocument must not be null!");
this.document = document;
this.body = document.getBody();
}
/**
* Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument}
* and {@link Map body}.
*
* @param document must be a {@link SecretDocument} effectively, must not be
* {@literal null}
* @param body must not be {@literal null}.
*/
private SecretDocumentAccessor(SecretDocument document, Map<String, Object> body) {
Assert.notNull(document, "SecretDocument must not be null!");
Assert.notNull(body, "Body must not be null!");
this.document = document;
this.body = body;
}
/**
* Puts the given value into the backing {@link SecretDocument} based on the
* coordinates defined through the given {@link VaultPersistentProperty}. By default
* this will be the plain field name. But field names might also consist of path
* traversals so we might need to create intermediate {@link Map}s.
*
* @param prop must not be {@literal null}.
* @param value
*/
void put(VaultPersistentProperty prop, @Nullable Object value) {
Assert.notNull(prop, "VaultPersistentProperty must not be null!");
String fieldName = prop.getName();
if (prop.isIdProperty()) {
this.document.setId((String) value);
return;
}
if (!fieldName.contains(".")) {
this.body.put(fieldName, value);
return;
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> document = this.body;
while (parts.hasNext()) {
String part = parts.next();
if (parts.hasNext()) {
document = getOrCreateNestedDocument(part, document);
}
else {
document.put(fieldName, value);
}
}
}
/**
* Returns the value the given {@link VaultPersistentProperty} refers to. By default
* this will be a direct field but the method will also transparently resolve nested
* values the {@link VaultPersistentProperty} might refer to through a path expression
* in the field name metadata.
*
* @param property must not be {@literal null}.
* @return
*/
@Nullable
Object get(VaultPersistentProperty property) {
String fieldName = property.getName();
if (property.isIdProperty()) {
return this.document.getId();
}
if (!fieldName.contains(".")) {
return this.body.get(fieldName);
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = this.body;
Object result = null;
while (source != null && parts.hasNext()) {
result = source.get(parts.next());
if (parts.hasNext()) {
source = getAsMap(result);
}
}
return result;
}
/**
* Returns whether the underlying {@link SecretDocument} has a value ({@literal null}
* or non-{@literal null}) for the given {@link VaultPersistentProperty}.
*
* @param property must not be {@literal null}.
* @return
*/
boolean hasValue(VaultPersistentProperty property) {
Assert.notNull(property, "Property must not be null!");
if (property.isIdProperty()) {
return StringUtils.hasText(this.document.getId());
}
String fieldName = property.getName();
if (!fieldName.contains(".")) {
return this.body.containsKey(fieldName);
}
String[] parts = fieldName.split("\\.");
Map<String, Object> source = this.body;
Object result = null;
for (int i = 1; i < parts.length; i++) {
result = source.get(parts[i - 1]);
source = getAsMap(result);
if (source == null) {
return false;
}
}
return source.containsKey(parts[parts.length - 1]);
}
/**
* Returns the given source object as map, i.e. maps as is or {@literal null}
* otherwise.
*
* @param source can be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
@Nullable
private static Map<String, Object> getAsMap(Object source) {
if (source instanceof Map) {
return (Map<String, Object>) source;
}
return null;
}
/**
* Returns the {@link Map} which either already exists in the given source under the
* given key, or creates a new nested one, registers it with the source and returns
* it.
*
* @param key must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> getOrCreateNestedDocument(String key,
Map<String, Object> source) {
Object existing = source.get(key);
if (existing instanceof Map) {
return (Map<String, Object>) existing;
}
Map<String, Object> nested = new LinkedHashMap<>();
source.put(key, nested);
return nested;
}
public Map<String, Object> getBody() {
return body;
}
public void setId(String id) {
this.document.setId(id);
}
/**
* Obtains a nested {@link SecretDocumentAccessor} for a
* {@link VaultPersistentProperty}. Nested accessors allows mapping of structured
* hierarchies and represent accessors to nested maps.
*
* @param property must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public SecretDocumentAccessor writeNested(VaultPersistentProperty property) {
Map<String, Object> body = (Map) get(property);
if (body == null) {
body = new LinkedHashMap<>();
put(property, body);
}
return new SecretDocumentAccessor(document, body);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2017 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.vault.repository.convert;
import org.springframework.data.convert.EntityConverter;
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
import org.springframework.vault.repository.mapping.VaultPersistentProperty;
/**
* Central Vault-specific converter interface.
*
* @since 2.0
*/
public interface VaultConverter
extends
EntityConverter<VaultPersistentEntity<?>, VaultPersistentProperty, Object, SecretDocument> {
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.convert.JodaTimeConverters;
import org.springframework.data.convert.WritingConverter;
import org.springframework.vault.repository.mapping.VaultSimpleTypes;
/**
* Value object to capture custom conversion. {@link VaultCustomConversions} also act as
* factory for {@link org.springframework.data.mapping.model.SimpleTypeHolder}
*
* @author Mark Paluch
* @since 2.0
* @see org.springframework.data.convert.CustomConversions
* @see org.springframework.data.mapping.model.SimpleTypeHolder
* @see VaultSimpleTypes
*/
public class VaultCustomConversions extends
org.springframework.data.convert.CustomConversions {
private static final StoreConversions STORE_CONVERSIONS;
private static final List<Object> STORE_CONVERTERS;
static {
List<Object> converters = new ArrayList<>();
converters.add(CustomToStringConverter.INSTANCE);
converters.addAll(JodaTimeConverters.getConvertersToRegister());
STORE_CONVERTERS = Collections.unmodifiableList(converters);
STORE_CONVERSIONS = StoreConversions
.of(VaultSimpleTypes.HOLDER, STORE_CONVERTERS);
}
/**
* Creates an empty {@link VaultCustomConversions} object.
*/
VaultCustomConversions() {
this(Collections.emptyList());
}
/**
* Create a new {@link VaultCustomConversions} instance registering the given
* converters.
*
* @param converters must not be {@literal null}.
*/
public VaultCustomConversions(List<?> converters) {
super(STORE_CONVERSIONS, converters);
}
@WritingConverter
private enum CustomToStringConverter implements GenericConverter {
INSTANCE;
public Set<ConvertiblePair> getConvertibleTypes() {
ConvertiblePair localeToString = new ConvertiblePair(Locale.class,
String.class);
ConvertiblePair booleanToString = new ConvertiblePair(Character.class,
String.class);
return new HashSet<>(Arrays.asList(localeToString, booleanToString));
}
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return source.toString();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.Map;
import org.springframework.data.convert.TypeMapper;
/**
* Vault-specific {@link TypeMapper} exposing that {@link SecretDocument}s might contain a
* type key.
*
* @author Mark Paluch
* @since 2.0
*/
public interface VaultTypeMapper extends TypeMapper<Map<String, Object>> {
/**
* Returns whether the given key is the type key.
*
* @return
*/
boolean isTypeKey(String key);
}

View File

@@ -0,0 +1,8 @@
/**
* Spring Vault specific converter infrastructure.
*/
@NonNullApi
package org.springframework.vault.repository.convert;
import org.springframework.lang.NonNullApi;

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017 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.vault.repository.core;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.repository.core.support.PersistentEntityInformation;
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
/**
* {@link VaultEntityInformation} implementation using a {@link VaultPersistentEntity}
* instance to lookup the necessary information. Can be configured with a custom
* collection to be returned which will trump the one returned by the
* {@link VaultPersistentEntity} if given.
*
* @author Mark Paluch
* @since 2.0
*/
public class MappingVaultEntityInformation<T, ID> extends
PersistentEntityInformation<T, ID> implements VaultEntityInformation<T, ID> {
/**
* @param entity
*/
public MappingVaultEntityInformation(VaultPersistentEntity<T> entity) {
super(entity);
if (!entity.hasIdProperty()) {
throw new MappingException(
String.format(
"Entity %s requires to have an explicit id field. Did you forget to provide one using @Id?",
entity.getName()));
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2017 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.vault.repository.core;
import org.springframework.data.repository.core.EntityInformation;
/**
* Vault-specific {@link EntityInformation}.
*
* @param <T> Domain type.
* @param <ID> Id type.
* @since 2.0
*/
public interface VaultEntityInformation<T, ID> extends EntityInformation<T, ID> {
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2017 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.vault.repository.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.repository.convert.MappingVaultConverter;
import org.springframework.vault.repository.convert.SecretDocument;
import org.springframework.vault.repository.convert.VaultConverter;
import org.springframework.vault.repository.mapping.VaultMappingContext;
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
import org.springframework.vault.repository.mapping.VaultPersistentProperty;
import org.springframework.vault.support.VaultResponse;
/**
* Vault-specific {@link org.springframework.data.keyvalue.core.KeyValueAdapter}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultKeyValueAdapter extends AbstractKeyValueAdapter {
private final VaultOperations vaultOperations;
private final VaultConverter vaultConverter;
/**
* Create a new {@link VaultKeyValueAdapter} given {@link VaultOperations}.
*
* @param vaultOperations must not be {@literal null}.
*/
public VaultKeyValueAdapter(VaultOperations vaultOperations) {
this(vaultOperations, new MappingVaultConverter(new VaultMappingContext()));
}
/**
* Create a new {@link VaultKeyValueAdapter} given {@link VaultOperations} and
* {@link VaultConverter}.
*
* @param vaultOperations must not be {@literal null}.
* @param vaultConverter must not be {@literal null}.
*/
public VaultKeyValueAdapter(VaultOperations vaultOperations,
VaultConverter vaultConverter) {
super(VaultQueryEngine.INSTANCE);
Assert.notNull(vaultOperations, "VaultOperations must not be null");
Assert.notNull(vaultConverter, "VaultConverter must not be null");
this.vaultOperations = vaultOperations;
this.vaultConverter = vaultConverter;
}
@Override
public Object put(Object id, Object item, String keyspace) {
SecretDocument secretDocument = new SecretDocument(id.toString());
vaultConverter.write(item, secretDocument);
vaultOperations.write(createKey(id, keyspace), secretDocument.getBody());
return secretDocument;
}
@Override
public boolean contains(Object id, String keyspace) {
return doList(keyspace).contains(id.toString());
}
@Nullable
@Override
public Object get(Object id, String keyspace) {
return get(id, keyspace, Object.class);
}
@Nullable
@Override
public <T> T get(Object id, String keyspace, Class<T> type) {
VaultResponse response = vaultOperations.read(createKey(id, keyspace));
if (response == null) {
return null;
}
SecretDocument document = SecretDocument.from(id.toString(), response);
return vaultConverter.read(type, document);
}
@Nullable
@Override
public Object delete(Object id, String keyspace) {
return delete(id, keyspace, Object.class);
}
@Nullable
@Override
public <T> T delete(Object id, String keyspace, Class<T> type) {
T entity = get(id, keyspace, type);
if (entity == null) {
return null;
}
vaultOperations.delete(createKey(id, keyspace));
return entity;
}
@Override
public Iterable<?> getAllOf(String keyspace) {
List<String> list = doList(keyspace);
List<Object> items = new ArrayList<>(list.size());
for (String id : list) {
items.add(get(id, keyspace));
}
return items;
}
@Override
public CloseableIterator<Entry<Object, Object>> entries(String keyspace) {
List<String> list = doList(keyspace);
Iterator<String> iterator = list.iterator();
return new CloseableIterator<Entry<Object, Object>>() {
@Override
public void close() {
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Entry<Object, Object> next() {
final String key = iterator.next();
return new Entry<Object, Object>() {
@Override
public Object getKey() {
return key;
}
@Nullable
@Override
public Object getValue() {
return get(key, keyspace);
}
@Override
public Object setValue(Object value) {
throw new UnsupportedOperationException();
}
};
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public void deleteAllOf(String keyspace) {
List<String> ids = doList(keyspace);
for (String id : ids) {
vaultOperations.delete(createKey(id, keyspace));
}
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public long count(String keyspace) {
List<String> list = doList(keyspace);
return list.size();
}
@Override
public void destroy() throws Exception {
}
List<String> doList(String keyspace) {
List<String> list = vaultOperations.list(keyspace);
return list == null ? Collections.emptyList() : list;
}
private String createKey(Object id, String keyspace) {
return String.format("%s/%s", keyspace, id);
}
MappingContext<? extends VaultPersistentEntity<?>, VaultPersistentProperty> getMappingContext() {
return vaultConverter.getMappingContext();
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017 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.vault.repository.core;
import org.springframework.data.keyvalue.core.KeyValueAdapter;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.vault.repository.mapping.VaultMappingContext;
/**
* Vault-specific {@link KeyValueTemplate}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultKeyValueTemplate extends KeyValueTemplate {
/**
* Create a new {@link VaultKeyValueTemplate} given {@link KeyValueAdapter} and
* {@link VaultMappingContext}.
*
* @param adapter must not be {@literal null}.
*/
public VaultKeyValueTemplate(KeyValueAdapter adapter) {
this(adapter, new VaultMappingContext());
}
/**
* Create a new {@link VaultKeyValueTemplate} given {@link KeyValueAdapter} and
* {@link VaultMappingContext}.
*
* @param adapter must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
*/
public VaultKeyValueTemplate(KeyValueAdapter adapter,
VaultMappingContext mappingContext) {
super(adapter, mappingContext);
}
@Override
public void destroy() throws Exception {
// no-op to prevent clear() call.
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2017 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.vault.repository.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.NullHandling;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.keyvalue.core.CriteriaAccessor;
import org.springframework.data.keyvalue.core.QueryEngine;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.keyvalue.core.SpelPropertyComparator;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.vault.repository.query.VaultQuery;
/**
* Query engine for Vault repository query methods. This engine queries Vault for all
* elements in the keyspace and applies {@link java.util.function.Predicate}s to the
* object id. Queries can contain only predicate subjects pointing to the
* {@link org.springframework.data.annotation.Id} property.
*
* @author Mark Paluch
* @since 2.0
* @see VaultQuery
* @see org.springframework.vault.repository.query.VaultQueryCreator
*/
class VaultQueryEngine extends
QueryEngine<VaultKeyValueAdapter, VaultQuery, Comparator<?>> {
static final VaultQueryEngine INSTANCE = new VaultQueryEngine();
private VaultQueryEngine() {
super(VaultCriteriaAccessor.INSTANCE, SpelSortAccessor.INSTANCE);
}
@Override
@SuppressWarnings("unchecked")
public Collection<?> execute(VaultQuery vaultQuery, Comparator<?> comparator,
long offset, int rows, String keyspace) {
return execute(vaultQuery, comparator, offset, rows, keyspace, Object.class);
}
@Override
@SuppressWarnings("unchecked")
public <T> Collection<T> execute(VaultQuery vaultQuery, Comparator<?> comparator,
long offset, int rows, String keyspace, Class<T> type) {
validatePropertyPaths(vaultQuery);
Stream<String> stream = getAdapter().doList(keyspace).stream();
if (vaultQuery != null) {
stream = stream.filter(vaultQuery::test);
}
if (comparator == null) {
if (offset > 0) {
stream = stream.skip(offset);
}
if (rows > 0) {
stream = stream.limit(rows);
}
}
Stream<T> typed = stream.map(it -> getAdapter().get(it, keyspace, type));
if (comparator != null) {
typed = typed.sorted((Comparator) comparator);
if (offset > 0) {
typed = typed.skip(offset);
}
if (rows > 0) {
typed = typed.limit(rows);
}
}
return typed.collect(Collectors.toCollection(ArrayList::new));
}
@Override
public long count(VaultQuery vaultQuery, String keyspace) {
validatePropertyPaths(vaultQuery);
Stream<String> stream = getAdapter().doList(keyspace).stream();
if (vaultQuery != null) {
stream = stream.filter(vaultQuery::test);
}
return stream.count();
}
private void validatePropertyPaths(VaultQuery vaultQuery) {
if (vaultQuery == null) {
return;
}
Stream<PropertyPath> stream = vaultQuery.getPropertyPaths().stream();
stream.map(it -> getAdapter().getMappingContext().getPersistentPropertyPath(it))
.filter(it -> it.getLeafProperty() != null)
.map(PersistentPropertyPath::getLeafProperty)
.filter(it -> !it.isIdProperty())
.forEach(
property -> {
throw new InvalidDataAccessApiUsageException(String.format(
"Cannot create criteria for non-@Id property %s",
property));
});
}
enum VaultCriteriaAccessor implements CriteriaAccessor<VaultQuery> {
INSTANCE;
@Override
public VaultQuery resolve(KeyValueQuery<?> query) {
return (VaultQuery) query.getCriteria();
}
}
/**
* {@link SortAccessor} implementation capable of creating
* {@link SpelPropertyComparator}.
*/
enum SpelSortAccessor implements SortAccessor<Comparator<?>> {
INSTANCE;
private final SpelExpressionParser parser = new SpelExpressionParser();
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Comparator<?> resolve(KeyValueQuery<?> query) {
if (query == null || query.getSort() == null || query.getSort().isUnsorted()) {
return null;
}
Optional<Comparator<?>> comparator = Optional.empty();
for (Order order : query.getSort()) {
SpelPropertyComparator<Object> spelSort = new SpelPropertyComparator<>(
order.getProperty(), parser);
if (Direction.DESC.equals(order.getDirection())) {
spelSort.desc();
if (!NullHandling.NATIVE.equals(order.getNullHandling())) {
spelSort = NullHandling.NULLS_FIRST.equals(order
.getNullHandling()) ? spelSort.nullsFirst() : spelSort
.nullsLast();
}
}
if (!comparator.isPresent()) {
comparator = Optional.of(spelSort);
}
else {
SpelPropertyComparator<Object> spelSortToUse = spelSort;
comparator = comparator.map(it -> it.thenComparing(spelSortToUse));
}
}
return comparator
.orElseThrow(() -> new IllegalStateException(
"No sort definitions have been added to this CompoundComparator to compare"));
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity;
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.StringUtils;
/**
* {@link VaultPersistentEntity} implementation.
*
* @author Mark Paluch
* @since 2.0
*/
public class BasicVaultPersistentEntity<T> extends
BasicKeyValuePersistentEntity<T, VaultPersistentProperty> implements
VaultPersistentEntity<T> {
private final String keyspace;
private final String secretBackend;
/**
* Creates new {@link BasicVaultPersistentEntity}.
*
* @param information must not be {@literal null}.
* @param fallbackKeySpaceResolver can be {@literal null}.
*/
public BasicVaultPersistentEntity(TypeInformation<T> information,
KeySpaceResolver fallbackKeySpaceResolver) {
super(information, fallbackKeySpaceResolver);
Secret annotation = findAnnotation(Secret.class);
String keyspace = super.getKeySpace();
String secretBackend = "secret";
if (annotation != null) {
if (StringUtils.hasText(annotation.backend())) {
secretBackend = annotation.backend();
}
}
this.secretBackend = secretBackend;
this.keyspace = String.format("%s/%s", secretBackend, keyspace);
}
@Override
public String getKeySpace() {
return keyspace;
}
@Override
public String getSecretBackend() {
return secretBackend;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017 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.vault.repository.mapping;
/**
* @author Mark Paluch
*/
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.keyvalue.annotation.KeySpace;
/**
* {@link Secret} marks objects as aggregate roots to be stored in Vault.
*
* @author Mark Paluch
*/
@Persistent
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE })
@KeySpace
public @interface Secret {
/**
* The prefix to distinguish between domain types.
* @see KeySpace
*/
@AliasFor(annotation = KeySpace.class, attribute = "value")
String value() default "";
/**
* Secret backend mount, defaults to {@literal secret}.
*/
String backend() default "secret";
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Mapping context for {@link VaultPersistentEntity Vault-specific entities}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultMappingContext extends
KeyValueMappingContext<VaultPersistentEntity<?>, VaultPersistentProperty> {
private KeySpaceResolver fallbackKeySpaceResolver = SimpleClassNameKeySpaceResolver.INSTANCE;
public KeySpaceResolver getFallbackKeySpaceResolver() {
return fallbackKeySpaceResolver;
}
@Override
public void setFallbackKeySpaceResolver(KeySpaceResolver fallbackKeySpaceResolver) {
this.fallbackKeySpaceResolver = fallbackKeySpaceResolver;
}
@Override
protected <T> VaultPersistentEntity<?> createPersistentEntity(
TypeInformation<T> typeInformation) {
return new BasicVaultPersistentEntity<>(typeInformation, fallbackKeySpaceResolver);
}
@Override
protected VaultPersistentProperty createPersistentProperty(Property property,
VaultPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
return new VaultPersistentProperty(property, owner, simpleTypeHolder);
}
/**
* Most trivial implementation of {@link KeySpaceResolver} returning the
* {@link Class#getName()}.
*
* @author Mark Paluch
*/
enum SimpleClassNameKeySpaceResolver implements KeySpaceResolver {
INSTANCE;
@Override
public String resolveKeySpace(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return StringUtils
.uncapitalize(ClassUtils.getUserClass(type).getSimpleName());
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
import org.springframework.data.mapping.PersistentEntity;
/**
* Vault specific {@link PersistentEntity}.
*
* @author Mark Paluch
* @since 2.0
*/
public interface VaultPersistentEntity<T> extends
KeyValuePersistentEntity<T, VaultPersistentProperty> {
/**
* @return the secret backend in which this {@link PersistentEntity} is stored.
*/
String getSecretBackend();
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* Vault-specific {@link KeyValuePersistentProperty}. By default, if a property is named
* {@code id} it's used as Id property.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultPersistentProperty extends
KeyValuePersistentProperty<VaultPersistentProperty> {
private static final Set<String> SUPPORTED_ID_PROPERTY_NAMES = new HashSet<String>();
static {
SUPPORTED_ID_PROPERTY_NAMES.add("id");
}
/**
* Create a new {@link VaultPersistentProperty}.
*
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
* @param simpleTypeHolder must not be {@literal null}.
*/
public VaultPersistentProperty(Property property,
PersistentEntity<?, VaultPersistentProperty> owner,
SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
}
@Override
public boolean isIdProperty() {
return super.isIdProperty() || SUPPORTED_ID_PROPERTY_NAMES.contains(getName());
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.vault.repository.convert.SecretDocument;
/**
* Simple constant holder for a {@link SimpleTypeHolder} enriched with Vault-specific
* simple (JSON) types.
*
* @author Mark Paluch
* @since 2.0
*/
public abstract class VaultSimpleTypes {
static {
Set<Class<?>> simpleTypes = new HashSet<Class<?>>();
simpleTypes.add(SecretDocument.class);
VAULT_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
}
private static final Set<Class<?>> VAULT_SIMPLE_TYPES;
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(
VAULT_SIMPLE_TYPES, true);
private VaultSimpleTypes() {
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2017 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.vault.repository.query;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.util.Assert;
import org.springframework.vault.repository.convert.SecretDocument;
/**
* Vault query consisting of a single {@link Predicate}. A new (empty) query evaluates
* unconditionally to {@literal true} and can be composed using {@link #and(VaultQuery)}
* and {@link #or(VaultQuery)}.
* <p />
* A query can express predicates only against the
* {@link org.springframework.data.annotation.Id} field of a {@link SecretDocument}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultQuery {
private final Predicate<String> predicate;
private final Set<PropertyPath> propertyPaths;
/**
* Create a new {@link VaultQuery} that evaluates unconditionally to {@literal true}.
*/
public VaultQuery() {
this(s -> true);
}
/**
* Create a new {@link VaultQuery} given {@link Predicate}.
*
* @param predicate must not be {@literal null}.
*/
public VaultQuery(Predicate<String> predicate) {
this(predicate, Collections.emptySet());
}
/**
* Create a new {@link VaultQuery} given {@link Predicate} and {@link PropertyPath}.
*
* @param predicate must not be {@literal null}.
*/
public VaultQuery(Predicate<String> predicate, PropertyPath propertyPath) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(propertyPath, "PropertyPath must not be null");
this.predicate = predicate;
this.propertyPaths = Collections.singleton(propertyPath);
}
private VaultQuery(Predicate<String> predicate, Set<PropertyPath> propertyPaths) {
Assert.notNull(propertyPaths, "PropertyPaths must not be null");
Assert.notNull(predicate, "Predicate must not be null");
this.predicate = predicate;
this.propertyPaths = propertyPaths;
}
/**
* Evaluate the query against a {@link SecretDocument}.
*
* @param document must not be {@literal null}.
* @return {@literal true} if the predicate matches, {@literal false} otherwise.
*/
public boolean test(SecretDocument document) {
Assert.notNull(predicate, "Predicate must not be null");
return predicate.test(document.getId());
}
/**
* Evaluate the query against a {@link String}.
*
* @param id must not be {@literal null}.
* @return {@literal true} if the predicate matches, {@literal false} otherwise.
*/
public boolean test(String id) {
Assert.notNull(id, "Id to test must not be null");
return predicate.test(id);
}
/**
* Compose a new {@link VaultQuery} using predicates of {@literal this} and the
* {@code other} query using logical {@code AND}.
*
* @param other must not be {@literal null}.
* @return a new composed {@link VaultQuery}.
* @see Predicate#and(Predicate)
*/
public VaultQuery and(VaultQuery other) {
Set<PropertyPath> propertyPaths = new HashSet<>(this.propertyPaths.size()
+ other.propertyPaths.size(), 1);
propertyPaths.addAll(this.propertyPaths);
propertyPaths.addAll(other.propertyPaths);
return new VaultQuery(this.predicate.and(other.predicate), propertyPaths);
}
/**
* Compose a new {@link VaultQuery} using predicates of {@literal this} and the
* {@code other} query using logical {@code OR}.
*
* @param other must not be {@literal null}.
* @return a new composed {@link VaultQuery}.
* @see Predicate#and(Predicate)
*/
public VaultQuery or(VaultQuery other) {
Set<PropertyPath> propertyPaths = new HashSet<>(this.propertyPaths.size()
+ other.propertyPaths.size(), 1);
propertyPaths.addAll(this.propertyPaths);
propertyPaths.addAll(other.propertyPaths);
return new VaultQuery(this.predicate.or(other.predicate), propertyPaths);
}
/**
* Compose a new {@link VaultQuery} using predicates of {@literal this} query and the
* {@code other} {@link Predicate} using logical {@code AND}.
*
* @param other must not be {@literal null}.
* @return a new composed {@link VaultQuery}.
* @see Predicate#and(Predicate)
*/
public VaultQuery and(Predicate<String> predicate, PropertyPath propertyPath) {
Set<PropertyPath> propertyPaths = new HashSet<>(this.propertyPaths.size() + 1, 1);
propertyPaths.addAll(this.propertyPaths);
propertyPaths.add(propertyPath);
return new VaultQuery(this.predicate.and(predicate), propertyPaths);
}
/**
* @return the underlying predicate.
*/
public Predicate<String> getPredicate() {
return predicate;
}
/**
* @return constrained {@link PropertyPath}s for this {@link VaultQuery}.
*/
public Set<PropertyPath> getPropertyPaths() {
return Collections.unmodifiableSet(propertyPaths);
}
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2017 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.vault.repository.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import lombok.Value;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
import org.springframework.data.repository.query.parser.Part.Type;
/**
* Query creator for Vault queries. Vault queries are limited to criterias constraining
* the {@link org.springframework.data.annotation.Id} property. A query consists of
* chained {@link Predicate}s that are evaluated for each Id value.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultQueryCreator extends
AbstractQueryCreator<KeyValueQuery<VaultQuery>, VaultQuery> {
/**
* Create a new {@link VaultQueryCreator} given {@link PartTree}.
*
* @param tree must not be {@literal null}.
*/
public VaultQueryCreator(PartTree tree) {
super(tree);
}
/**
* Create a new {@link VaultQueryCreator} given {@link PartTree} and
* {@link ParameterAccessor}.
*
* @param tree must not be {@literal null}.
* @param parameters must not be {@literal null}.
*/
public VaultQueryCreator(PartTree tree, ParameterAccessor parameters) {
super(tree, parameters);
}
@Override
protected VaultQuery create(Part part, Iterator<Object> parameters) {
return new VaultQuery(createPredicate(part, parameters), part.getProperty());
}
@Override
protected VaultQuery and(Part part, VaultQuery base, Iterator<Object> parameters) {
if (base == null) {
return create(part, parameters);
}
return base.and(createPredicate(part, parameters), part.getProperty());
}
private static Predicate<String> createPredicate(Part part,
Iterator<Object> parameters) {
VariableAccessor accessor = getVariableAccessor(part);
Predicate<String> predicate = from(part, accessor, parameters);
return it -> predicate.test(accessor.toString(it));
}
/**
* Return a {@link Predicate} depending on the {@link Part} given.
*
* @param part
* @param parameters
* @return
*/
private static Predicate<String> from(Part part, VariableAccessor accessor,
Iterator<Object> parameters) {
Type type = part.getType();
switch (type) {
case AFTER:
case GREATER_THAN:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.compareTo(value) > 0);
case GREATER_THAN_EQUAL:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.compareTo(value) >= 0);
case BEFORE:
case LESS_THAN:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.compareTo(value) < 0);
case LESS_THAN_EQUAL:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.compareTo(value) <= 0);
case BETWEEN:
String from = accessor.nextString(parameters);
String to = accessor.nextString(parameters);
return it -> it.compareTo(from) >= 0 && it.compareTo(to) <= 0;
case NOT_IN:
return new Criteria<>(accessor.nextAsArray(parameters),
(value, it) -> Arrays.binarySearch(value, it) < 0);
case IN:
return new Criteria<>(accessor.nextAsArray(parameters),
(value, it) -> Arrays.binarySearch(value, it) >= 0);
case STARTING_WITH:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.startsWith(value));
case ENDING_WITH:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.endsWith(value));
case CONTAINING:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.contains(value));
case NOT_CONTAINING:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> !it.contains(value));
case REGEX:
return Pattern.compile((String) parameters.next(),
isIgnoreCase(part) ? Pattern.CASE_INSENSITIVE : 0).asPredicate();
case TRUE:
return it -> it.equalsIgnoreCase("true");
case FALSE:
return it -> it.equalsIgnoreCase("false");
case SIMPLE_PROPERTY:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> it.equals(value));
case NEGATING_SIMPLE_PROPERTY:
return new Criteria<>(accessor.nextString(parameters),
(value, it) -> !it.equals(value));
default:
throw new IllegalArgumentException("Unsupported keyword!");
}
}
@Override
protected VaultQuery or(VaultQuery vaultQuery, VaultQuery other) {
return vaultQuery.or(other);
}
@Override
protected KeyValueQuery<VaultQuery> complete(VaultQuery vaultQuery, Sort sort) {
KeyValueQuery<VaultQuery> query = new KeyValueQuery<>(vaultQuery);
if (sort != null) {
query.orderBy(sort);
}
return query;
}
private static VariableAccessor getVariableAccessor(Part part) {
return isIgnoreCase(part) ? VariableAccessor.Lowercase : VariableAccessor.AsIs;
}
private static boolean isIgnoreCase(Part part) {
return part.shouldIgnoreCase() != IgnoreCaseType.NEVER;
}
@Value
static class Criteria<T> implements Predicate<String> {
private T value;
private BiPredicate<T, String> predicate;
@Override
public boolean test(String s) {
return predicate.test(value, s);
}
}
enum VariableAccessor {
AsIs {
@Override
String nextString(Iterator<Object> parameters) {
return parameters.next().toString();
}
@Override
String[] nextAsArray(Iterator<Object> iterator) {
Object next = iterator.next();
if (next instanceof Collection) {
return ((Collection<?>) next).toArray(new String[0]);
}
else if (next != null && next.getClass().isArray()) {
return (String[]) next;
}
return new String[] { (String) next };
}
@Override
String toString(String value) {
return value;
}
},
Lowercase {
@Override
String nextString(Iterator<Object> parameters) {
return AsIs.nextString(parameters).toLowerCase();
}
@Override
String[] nextAsArray(Iterator<Object> iterator) {
String[] original = AsIs.nextAsArray(iterator);
String[] lowercase = new String[original.length];
for (int i = 0; i < original.length; i++) {
lowercase[i] = original[i].toLowerCase();
}
return lowercase;
}
@Override
String toString(String value) {
return value.toLowerCase();
}
};
abstract String[] nextAsArray(Iterator<Object> iterator);
abstract String nextString(Iterator<Object> iterator);
abstract String toString(String value);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2017 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.vault.repository.support;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.vault.repository.core.MappingVaultEntityInformation;
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
import org.springframework.vault.repository.query.VaultQueryCreator;
/**
* {@link RepositoryFactorySupport} specific of handing Vault
* {@link org.springframework.data.keyvalue.repository.KeyValueRepository}.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultRepositoryFactory extends KeyValueRepositoryFactory {
private final KeyValueOperations operations;
public VaultRepositoryFactory(KeyValueOperations keyValueOperations) {
this(keyValueOperations, VaultQueryCreator.class);
}
public VaultRepositoryFactory(KeyValueOperations keyValueOperations,
Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class);
}
public VaultRepositoryFactory(KeyValueOperations keyValueOperations,
Class<? extends AbstractQueryCreator<?, ?>> queryCreator,
Class<? extends RepositoryQuery> repositoryQueryType) {
super(keyValueOperations, queryCreator, repositoryQueryType);
this.operations = keyValueOperations;
}
@Override
@SuppressWarnings("unchecked")
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
VaultPersistentEntity<T> entity = (VaultPersistentEntity<T>) operations
.getMappingContext().getPersistentEntity(domainClass);
return new MappingVaultEntityInformation<>(entity);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017 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.vault.repository.support;
import java.io.Serializable;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
/**
* Adapter for Springs {@link FactoryBean} interface to allow easy setup of
* {@link VaultRepositoryFactory} via Spring configuration.
*
* @param <T> The repository type.
* @param <S> The repository domain type.
* @param <ID> The repository id type.
* @since 2.0
*/
public class VaultRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends KeyValueRepositoryFactoryBean<T, S, ID> {
/**
* Creates a new {@link VaultRepositoryFactoryBean} for the given repository
* interface.
*
* @param repositoryInterface must not be {@literal null}.
*/
public VaultRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
@Override
protected VaultRepositoryFactory createRepositoryFactory(
KeyValueOperations operations,
Class<? extends AbstractQueryCreator<?, ?>> queryCreator,
Class<? extends RepositoryQuery> repositoryQueryType) {
return new VaultRepositoryFactory(operations, queryCreator, repositoryQueryType);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 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.vault.repository;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.config.AbstractVaultConfiguration;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.vault.util.Settings;
/**
* Test configuration for Vault integration tests.
*
* @author Mark Paluch
*/
@Configuration
public class VaultIntegrationTestConfiguration extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
return new VaultEndpoint();
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication(Settings.token());
}
@Override
public SslConfiguration sslConfiguration() {
return Settings.createSslConfiguration();
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2017 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.vault.repository;
import java.util.List;
import lombok.Data;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.repository.VaultRepositoryIntegrationTests.VaultRepositoryTestConfiguration;
import org.springframework.vault.repository.configuration.EnableVaultRepositories;
import org.springframework.vault.util.IntegrationTestSupport;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.domain.Sort.Order.asc;
/**
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = VaultRepositoryTestConfiguration.class)
public class VaultRepositoryIntegrationTests extends IntegrationTestSupport {
@Configuration
@EnableVaultRepositories(considerNestedRepositories = true)
static class VaultRepositoryTestConfiguration extends
VaultIntegrationTestConfiguration {
}
@Autowired
VaultRepository vaultRepository;
@Autowired
VaultTemplate vaultTemplate;
@Before
public void before() {
vaultRepository.deleteAll();
}
@Test
public void loadAndSave() {
Person person = new Person();
person.setId("foo-key");
person.setName("bar");
vaultRepository.save(person);
Iterable<Person> all = vaultRepository.findAll();
assertThat(all).contains(person);
assertThat(vaultRepository.findById("foo-key")).contains(person);
}
@Test
public void shouldApplyQueryMethod() {
Person walter = new Person();
walter.setId("walter");
walter.setName("Walter");
vaultRepository.save(walter);
Person skyler = new Person();
skyler.setId("skyler");
skyler.setName("Skyler");
vaultRepository.save(skyler);
Iterable<Person> all = vaultRepository.findByIdStartsWith("walt");
assertThat(all).contains(walter).doesNotContain(skyler);
}
@Test
public void shouldApplyQueryMethodWithSorting() {
Person walter = new Person();
walter.setId("walter");
walter.setName("Walter");
vaultRepository.save(walter);
Person skyler = new Person();
skyler.setId("skyler");
skyler.setName("Skyler");
vaultRepository.save(skyler);
assertThat(vaultRepository.findAllByOrderByNameAsc()).containsSequence(skyler,
walter);
assertThat(vaultRepository.findAllByOrderByNameDesc()).containsSequence(walter,
skyler);
}
@Test
public void shouldApplyLimiting() {
Person walter = new Person();
walter.setId("walter");
walter.setName("Walter");
vaultRepository.save(walter);
Person skyler = new Person();
skyler.setId("skyler");
skyler.setName("Skyler");
vaultRepository.save(skyler);
assertThat(vaultRepository.findTop1By(Sort.by(asc("name")))).containsOnly(skyler);
}
@Test(expected = InvalidDataAccessApiUsageException.class)
public void shouldFailForNonIdCriteria() {
vaultRepository.findInvalidByName("foo");
}
interface VaultRepository extends CrudRepository<Person, String> {
List<Person> findByIdStartsWith(String prefix);
List<Person> findAllByOrderByNameAsc();
List<Person> findAllByOrderByNameDesc();
List<Person> findTop1By(Sort sort);
List<Person> findInvalidByName(String name);
}
@Data
static class Person {
String id, name;
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.data.convert.ConfigurableTypeInformationMapper;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DefaultVaultTypeMapper}.
*
* @author Mark Paluch
*/
public class DefaultVaultTypeMapperUnitTests {
ConfigurableTypeInformationMapper configurableTypeInformationMapper = new ConfigurableTypeInformationMapper(
Collections.singletonMap(String.class, "1"));
SimpleTypeInformationMapper simpleTypeInformationMapper = new SimpleTypeInformationMapper();
DefaultVaultTypeMapper typeMapper = new DefaultVaultTypeMapper();
@Test
public void defaultInstanceWritesClasses() {
writesTypeToField(new LinkedHashMap<>(), String.class, String.class.getName());
}
@Test
public void defaultInstanceReadsClasses() {
Map<String, Object> document = new LinkedHashMap<>();
document.put(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class.getName());
readsTypeFromField(document, String.class);
}
@Test
public void writesMapKeyForType() {
typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY,
Collections.singletonList(configurableTypeInformationMapper));
writesTypeToField(new LinkedHashMap<>(), String.class, "1");
writesTypeToField(new LinkedHashMap<>(), Object.class, null);
}
@Test
public void writesClassNamesForUnmappedValuesIfConfigured() {
typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY,
Arrays.asList(configurableTypeInformationMapper,
simpleTypeInformationMapper));
writesTypeToField(new LinkedHashMap<>(), String.class, "1");
writesTypeToField(new LinkedHashMap<>(), Object.class, Object.class.getName());
}
@Test
public void readsTypeForMapKey() {
typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY,
Collections.singletonList(configurableTypeInformationMapper));
readsTypeFromField(
Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "1"),
String.class);
readsTypeFromField(Collections.singletonMap(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "unmapped"), null);
}
@Test
public void readsTypeLoadingClassesForUnmappedTypesIfConfigured() {
typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY,
Arrays.asList(configurableTypeInformationMapper,
simpleTypeInformationMapper));
readsTypeFromField(
Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "1"),
String.class);
readsTypeFromField(Collections.singletonMap(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, Object.class.getName()),
Object.class);
}
@Test
public void addsFullyQualifiedClassNameUnderDefaultKeyByDefault() {
writesTypeToField(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, new LinkedHashMap<>(),
String.class);
}
@Test
public void writesTypeToCustomFieldIfConfigured() {
typeMapper = new DefaultVaultTypeMapper("_custom");
writesTypeToField("_custom", new LinkedHashMap<>(), String.class);
}
@Test
public void doesNotWriteTypeInformationInCaseKeyIsSetToNull() {
typeMapper = new DefaultVaultTypeMapper(null);
writesTypeToField(null, new LinkedHashMap<>(), String.class);
}
@Test
public void readsTypeFromDefaultKeyByDefault() {
readsTypeFromField(Collections.singletonMap(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class.getName()),
String.class);
}
@Test
public void readsTypeFromCustomFieldConfigured() {
typeMapper = new DefaultVaultTypeMapper("_custom");
readsTypeFromField(Collections.singletonMap("_custom", String.class.getName()),
String.class);
}
@Test
public void returnsNullIfNoTypeInfoInDocument() {
readsTypeFromField(new LinkedHashMap<>(), null);
readsTypeFromField(
Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, ""),
null);
}
@Test
public void returnsNullIfClassCannotBeLoaded() {
readsTypeFromField(Collections.singletonMap(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "fooBar"), null);
}
@Test
public void returnsNullIfTypeKeySetToNull() {
typeMapper = new DefaultVaultTypeMapper(null);
readsTypeFromField(Collections.singletonMap(
DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class), null);
}
@Test
public void returnsCorrectTypeKey() {
assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY))
.isTrue();
typeMapper = new DefaultVaultTypeMapper("_custom");
assertThat(typeMapper.isTypeKey("_custom")).isTrue();
assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY))
.isFalse();
typeMapper = new DefaultVaultTypeMapper(null);
assertThat(typeMapper.isTypeKey("_custom")).isFalse();
assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY))
.isFalse();
}
private void readsTypeFromField(Map<String, Object> document, @Nullable Class<?> type) {
TypeInformation<?> typeInfo = typeMapper.readType(document);
if (type != null) {
assertThat(typeInfo).isNotNull();
assertThat(typeInfo.getType()).isAssignableFrom(type);
}
else {
assertThat(typeInfo).isNull();
}
}
private void writesTypeToField(@Nullable String field, Map<String, Object> document,
Class<?> type) {
typeMapper.writeType(type, document);
if (field == null) {
assertThat(document.keySet()).isEmpty();
}
else {
assertThat(document).containsKey(field);
assertThat(document).containsEntry(field, type.getName());
}
}
private void writesTypeToField(Map<String, Object> document, Class<?> type,
@Nullable Object value) {
typeMapper.writeType(type, document);
if (value == null) {
assertThat(document.keySet()).isEmpty();
}
else {
assertThat(document).containsKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY);
assertThat(document).containsEntry(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY,
value);
}
}
}

View File

@@ -0,0 +1,380 @@
/*
* Copyright 2017 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.vault.repository.convert;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.vault.repository.mapping.VaultMappingContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link MappingVaultConverter}.
*
* @author Mark Paluch
*/
public class MappingVaultConverterUnitTests {
VaultMappingContext context = new VaultMappingContext();
MappingVaultConverter converter = new MappingVaultConverter(context);
@Before
public void before() {
VaultCustomConversions conversions = new VaultCustomConversions(Arrays.asList(
DocumentToPersonConverter.INSTANCE, PersonToDocumentConverter.INSTANCE));
converter.setCustomConversions(conversions);
converter.afterPropertiesSet();
}
@Test
public void shouldReadSimpleEntity() {
SecretDocument document = new SecretDocument("heisenberg");
document.put("username", "walter");
document.put("password", "hb");
SimpleEntity entity = converter.read(SimpleEntity.class, document);
assertThat(entity.getId()).isEqualTo("heisenberg");
assertThat(entity.getUsername()).isEqualTo("walter");
assertThat(entity.getPassword()).isEqualTo("hb");
}
@Test
public void shouldReadConvertedEntity() {
SecretDocument document = new SecretDocument("heisenberg");
document.put("the_name", "walter");
Person entity = converter.read(Person.class, document);
assertThat(entity.getName()).isEqualTo("walter");
}
@Test
public void shouldReadSubtype() {
SecretDocument document = new SecretDocument("heisenberg");
document.put("_class", ExtendedEntity.class.getName());
document.put("username", "walter");
document.put("password", "hb");
document.put("location", "Albuquerque");
SimpleEntity entity = converter.read(SimpleEntity.class, document);
assertThat(entity).isInstanceOf(ExtendedEntity.class);
assertThat(entity.getId()).isEqualTo("heisenberg");
assertThat(entity.getUsername()).isEqualTo("walter");
assertThat(entity.getPassword()).isEqualTo("hb");
assertThat(((ExtendedEntity) entity).getLocation()).isEqualTo("Albuquerque");
}
@Test
public void shouldReadEntityWithEnum() {
SecretDocument document = new SecretDocument();
document.put("condition", "BAD");
EntityWithEnum entity = converter.read(EntityWithEnum.class, document);
assertThat(entity.getCondition()).isEqualTo(Condition.BAD);
}
@Test
public void shouldReadSimpleEntityWithConstructorCreation() {
SecretDocument document = new SecretDocument("heisenberg");
document.put("username", "walter");
document.put("password", "hb");
ConstructorCreation entity = converter.read(ConstructorCreation.class, document);
assertThat(entity.getId()).isEqualTo("heisenberg");
assertThat(entity.getUsername()).isEqualTo("walter");
assertThat(entity.getPassword()).isEqualTo("hb");
}
@Test
public void shouldReadEntityWithList() {
SecretDocument document = new SecretDocument(Collections.singletonMap(
"usernames", Arrays.asList("walter", "heisenberg")));
EntityWithListOfStrings entity = converter.read(EntityWithListOfStrings.class,
document);
assertThat(entity.getUsernames()).containsSequence("walter", "heisenberg");
}
@Test
public void shouldReadEntityWithMap() {
Map<String, Integer> keyVersions = new LinkedHashMap<>();
keyVersions.put("foo", 1);
keyVersions.put("bar", 2);
SecretDocument document = new SecretDocument(Collections.singletonMap(
"keyVersions", keyVersions));
EntityWithMap entity = converter.read(EntityWithMap.class, document);
assertThat(entity.getKeyVersions()).containsAllEntriesOf(keyVersions);
}
@Test
public void shouldReadEntityWithNesting() {
Map<String, String> walter = new LinkedHashMap<>();
walter.put("username", "heisenberg");
walter.put("password", "hb");
SecretDocument document = new SecretDocument();
document.put("nested", walter);
EntityWithNestedType entity = converter
.read(EntityWithNestedType.class, document);
assertThat(entity.getNested()).isEqualTo(new NestedType("heisenberg", "hb"));
}
@Test
public void shouldReadEntityWithListOfEntities() {
Map<String, String> walter = new LinkedHashMap<>();
walter.put("username", "heisenberg");
walter.put("password", "hb");
Map<String, String> skyler = new LinkedHashMap<>();
skyler.put("username", "skyler");
skyler.put("password", "marie");
SecretDocument document = new SecretDocument();
document.put("nested", Arrays.asList(walter, skyler));
EntityWithListOfEntities entity = converter.read(EntityWithListOfEntities.class,
document);
assertThat(entity.getNested()).contains(new NestedType("heisenberg", "hb"),
new NestedType("skyler", "marie"));
}
@Test
public void shouldWriteSimpleEntity() {
SimpleEntity entity = new SimpleEntity();
entity.setId("heisenberg");
entity.setUsername("walter");
entity.setPassword("hb");
SecretDocument expected = new SecretDocument("heisenberg");
expected.put("username", "walter");
expected.put("password", "hb");
expected.put("_class", entity.getClass().getName());
SecretDocument sink = new SecretDocument();
converter.write(entity, sink);
assertThat(sink).isEqualTo(expected);
}
@Test
public void shouldWriteConvertedEntity() {
SecretDocument expected = new SecretDocument();
expected.put("the_name", "walter");
SecretDocument sink = new SecretDocument();
converter.write(new Person("walter"), sink);
assertThat(sink).isEqualTo(expected);
}
@Test
public void shouldWriteEntityWithEnum() {
EntityWithEnum entity = new EntityWithEnum();
entity.setCondition(Condition.BAD);
SecretDocument sink = new SecretDocument();
converter.write(entity, sink);
assertThat(sink.getBody()).containsEntry("condition", "BAD");
}
@Test
public void shouldWriteEntityWithList() {
EntityWithListOfStrings entity = new EntityWithListOfStrings();
entity.setUsernames(Arrays.asList("walter", "heisenberg"));
SecretDocument sink = new SecretDocument();
converter.write(entity, sink);
assertThat(sink.getBody()).containsEntry("usernames",
Arrays.asList("walter", "heisenberg"));
}
@Test
public void shouldWriteEntityWithMap() {
Map<String, Integer> keyVersions = new LinkedHashMap<>();
keyVersions.put("foo", 1);
keyVersions.put("bar", 2);
EntityWithMap entity = new EntityWithMap();
entity.setKeyVersions(keyVersions);
SecretDocument sink = new SecretDocument();
converter.write(entity, sink);
assertThat(sink.getBody()).containsEntry("keyVersions", keyVersions);
}
@Test
@SuppressWarnings("unchecked")
public void shouldWriteEntityWithListOfEntities() {
EntityWithListOfEntities entity = new EntityWithListOfEntities();
entity.setNested(Arrays.asList(new NestedType("heisenberg", "hb"),
new NestedType("skyler", "marie")));
Map<String, Object> walter = new LinkedHashMap<>();
walter.put("username", "heisenberg");
walter.put("password", "hb");
Map<String, Object> skyler = new LinkedHashMap<>();
skyler.put("username", "skyler");
skyler.put("password", "marie");
SecretDocument sink = new SecretDocument();
converter.write(entity, sink);
assertThat((List<Map<String, Object>>) sink.get("nested")).contains(walter,
skyler);
}
@Data
static class SimpleEntity {
String id;
String username;
String password;
}
@Data
static class ExtendedEntity extends SimpleEntity {
String location;
}
@Data
static class EntityWithNestedType {
NestedType nested;
}
@Data
static class EntityWithEnum {
Condition condition;
}
@Data
@RequiredArgsConstructor
static class ConstructorCreation {
final String id;
final String username;
String password;
}
@Data
static class EntityWithListOfStrings {
List<String> usernames;
}
@Data
static class EntityWithListOfEntities {
List<NestedType> nested;
}
@Data
static class EntityWithMap {
Map<String, Integer> keyVersions;
}
@Data
@AllArgsConstructor
static class NestedType {
String username;
String password;
}
enum Condition {
GOOD, BAD
}
@Data
static class Person {
final String name;
}
enum DocumentToPersonConverter implements Converter<SecretDocument, Person> {
INSTANCE;
@Override
public Person convert(SecretDocument secretDocument) {
return new Person((String) secretDocument.get("the_name"));
}
}
enum PersonToDocumentConverter implements Converter<Person, SecretDocument> {
INSTANCE;
@Override
public SecretDocument convert(Person person) {
SecretDocument document = new SecretDocument();
document.put("the_name", person.getName());
return document;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link BasicVaultPersistentEntity} via {@link VaultMappingContext}.
*
* @author Mark Paluch
*/
public class BasicVaultPersistentEntityUnitTests {
private VaultMappingContext mappingContext = new VaultMappingContext();
@Test
public void shouldSetIdPropertyThroughName() {
VaultPersistentEntity<?> persistentEntity = mappingContext
.getPersistentEntity(IdProperty.class);
assertThat(persistentEntity.getIdProperty()).isNotNull();
}
@Test
public void shouldSetIdPropertyThroughAnnotation() {
VaultPersistentEntity<?> persistentEntity = mappingContext
.getPersistentEntity(ExplicitId.class);
assertThat(persistentEntity.getIdProperty()).isNotNull();
}
static class IdProperty {
String id, username;
}
static class ExplicitId {
@Id
String username;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2017 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.vault.repository.mapping;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link VaultMappingContext}.
*
* @author Mark Paluch
*/
public class VaultMappingContextUnitTests {
VaultMappingContext context = new VaultMappingContext();
@Test
public void shouldCreatePersistentEntity() {
VaultPersistentEntity<?> entity = context.getPersistentEntity(Person.class);
assertThat(entity).isNotNull();
assertThat(entity.getSecretBackend()).isEqualTo("secret");
assertThat(entity.getKeySpace()).isEqualTo("secret/person");
}
@Test
public void shouldDetermineKeyspace() {
assertThat(context.getRequiredPersistentEntity(Login.class).getSecretBackend())
.isEqualTo("secret");
assertThat(context.getRequiredPersistentEntity(Login.class).getKeySpace())
.isEqualTo("secret/login");
assertThat(
context.getRequiredPersistentEntity(Credentials.class).getSecretBackend())
.isEqualTo("shared");
assertThat(context.getRequiredPersistentEntity(Credentials.class).getKeySpace())
.isEqualTo("shared/Email");
}
private static class Person {
}
@Secret
private static class Login {
}
@Secret(value = "Email", backend = "shared")
private static class Credentials {
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2017 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.vault.repository.query;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit test for {@link VaultQueryCreator}.
*
* @author Mark Paluch
*/
public class VaultQueryCreatorUnitTests {
@Test
public void greaterThan() {
VaultQuery query = createQuery("findByIdGreaterThan", "5");
assertThat(query.getPredicate()).accepts("6", "7").rejects("4", "5");
}
@Test
public void greaterThanOrEqual() {
VaultQuery query = createQuery("findByIdGreaterThanEqual", "5");
assertThat(query.getPredicate()).accepts("5", "6", "7").rejects("4");
}
@Test
public void lessThan() {
VaultQuery query = createQuery("findByIdLessThan", "5");
assertThat(query.getPredicate()).accepts("4").rejects("5", "6", "7");
}
@Test
public void lessThanOrEqual() {
VaultQuery query = createQuery("findByIdLessThanEqual", "5");
assertThat(query.getPredicate()).accepts("4", "5").rejects("6", "7");
}
@Test
public void between() {
VaultQuery query = createQuery("findByIdIsBetween", "5", "7");
assertThat(query.getPredicate()).accepts("5", "6", "7").rejects("4", "8");
}
@Test
public void in() {
VaultQuery query = createQuery("findByIdIn", Arrays.asList("2", "3"));
assertThat(query.getPredicate()).accepts("2", "3").rejects("4", "8");
}
@Test
public void negateIn() {
VaultQuery query = createQuery("findByIdNotIn", Arrays.asList("2", "3"));
assertThat(query.getPredicate()).accepts("4", "8").rejects("2", "3");
}
@Test
public void startingWith() {
VaultQuery query = createQuery("findByIdStartsWith", "Walter");
assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler");
}
@Test
public void endingWith() {
VaultQuery query = createQuery("findByIdEndsWith", "White");
assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler");
}
@Test
public void containing() {
VaultQuery query = createQuery("findByIdContaining", "er Wh");
assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler");
}
@Test
public void negateContaining() {
VaultQuery query = createQuery("findByIdNotContaining", "er Wh");
assertThat(query.getPredicate()).accepts("Skyler").rejects("Walter White");
}
@Test
public void regex() {
VaultQuery query = createQuery("findByIdMatches", "Wa(.*)r");
assertThat(query.getPredicate()).accepts("Walter", "Water").rejects("Skyler");
}
@Test
public void isTrue() {
VaultQuery query = createQuery("findByIdIsTrue", "");
assertThat(query.getPredicate()).accepts("true", "True").rejects("false");
}
@Test
public void isFalse() {
VaultQuery query = createQuery("findByIdIsFalse", "");
assertThat(query.getPredicate()).accepts("false", "False").rejects("true");
}
@Test
public void simpleProperty() {
VaultQuery query = createQuery("findById", "Walter");
assertThat(query.getPredicate()).accepts("Walter").rejects("Skyler");
}
@Test
public void negateSimpleProperty() {
VaultQuery query = createQuery("findByIdNot", "Walter");
assertThat(query.getPredicate()).accepts("Skyler").rejects("Walter");
}
@Test
public void greaterThanOrEquals() {
VaultQuery query = createQuery("findByIdGreaterThanOrIdIs", "5", "2");
assertThat(query.getPredicate()).accepts("6", "7", "2").rejects("3", "4", "5");
}
@Test
public void greaterThanAndLessThan() {
VaultQuery query = createQuery("findByIdGreaterThanAndIdLessThan", "2", "5");
assertThat(query.getPredicate()).accepts("3", "4").rejects("2", "5", "6");
}
VaultQuery createQuery(String methodName, String value) {
DefaultParameters defaultParameters = new DefaultParameters(
ReflectionUtils.findMethod(dummy.class, "someUnrelatedMethod",
String.class));
PartTree partTree = new PartTree(methodName, Credentials.class);
VaultQueryCreator queryCreator = new VaultQueryCreator(
partTree,
new ParametersParameterAccessor(defaultParameters, new Object[] { value }));
return queryCreator.createQuery().getCriteria();
}
VaultQuery createQuery(String methodName, List<String> value) {
DefaultParameters defaultParameters = new DefaultParameters(
ReflectionUtils
.findMethod(dummy.class, "someUnrelatedMethod", List.class));
PartTree partTree = new PartTree(methodName, Credentials.class);
VaultQueryCreator queryCreator = new VaultQueryCreator(
partTree,
new ParametersParameterAccessor(defaultParameters, new Object[] { value }));
return queryCreator.createQuery().getCriteria();
}
VaultQuery createQuery(String methodName, String value, String anotherValue) {
DefaultParameters defaultParameters = new DefaultParameters(
ReflectionUtils.findMethod(dummy.class, "someUnrelatedMethod",
String.class, String.class));
PartTree partTree = new PartTree(methodName, Credentials.class);
VaultQueryCreator queryCreator = new VaultQueryCreator(partTree,
new ParametersParameterAccessor(defaultParameters, new Object[] { value,
anotherValue }));
return queryCreator.createQuery().getCriteria();
}
static interface dummy {
Object someUnrelatedMethod(String arg);
Object someUnrelatedMethod(List<String> arg);
Object someUnrelatedMethod(String from, String to);
}
static class Credentials {
String id;
}
}

View File

@@ -6,6 +6,7 @@
* Authentication steps DSL to <<vault.authentication.steps,compose authentication flows>>.
* Reactive Vault client via `ReactiveVaultOperations`.
* <<vault.repositories,Vault repository support>> based on Spring Data KeyValue.
[[new-features.1-0-0]]
=== What's new in Spring Vault 1.0

View File

@@ -0,0 +1,276 @@
[[vault.repositories]]
= Vault Repositories
Working with `VaultTemplate` and responses mapped to Java classes allows basic data operations like read, write
and delete. Vault repositories apply Spring Data's repository concept on top of Vault.
A Vault repository exposes basic CRUD functionality and supports query derivation with predicates constraining
the Id property, paging and sorting.
[[vault.repositories.usage]]
== Usage
To access domain entities stored in Vault you can leverage repository support that eases implementing those quite significantly.
.Sample Credentials Entity
====
[source,java]
----
@Secret
public class Credentials {
@Id String id;
String password;
String socialSecurityNumber;
Address address;
}
----
====
We have a pretty simple domain object here. Note that it has a property named `id` annotated with
`org.springframework.data.annotation.Id` and a `@Secret` annotation on its type.
Those two are responsible for creating the actual key used to persist the object as JSON inside Vault.
NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties.
Those with the annotation are favored over others.
The next step is to declare a repository interface that uses the domain object.
.Basic Repository Interface for `Credentials` entities
====
[source,java]
----
public interface CredentialsRepository extends CrudRepository<Credentials, String> {
}
----
====
As our repository extends `CrudRepository` it provides basic CRUD and query methods.
The thing we need in between to glue things together is the according Spring configuration.
.JavaConfig for Vault Repositories
====
[source,java]
----
@Configuration
@EnableVaultRepositories
public class ApplicationConfig {
@Bean
public VaultTemplate vaultTemplate() {
return new VaultTemplate(…);
}
}
----
====
Given the setup above we can go on and inject `CredentialsRepository` into our components.
.Access to Person Entities
====
[source,java]
----
@Autowired CredentialsRepository repo;
public void basicCrudOperations() {
Credentials creds = new Credentials("heisenberg", "327215", "AAA-GG-SSSS");
rand.setAddress(new Address("308 Negra Arroyo Lane", "Albuquerque", "New Mexico", "87104"));
repo.save(creds); <1>
repo.findOne(creds.getId()); <2>
repo.count(); <3>
repo.delete(creds); <4>
}
----
<1> Stores properties of `Credentials` inside Vault Hash with a key pattern `keyspace/id`,
in this case `credentials/heisenberg`, in the generic secret backend.
<2> Uses the provided id to retrieve the object stored at `keyspace/id`.
<3> Counts the total number of entities available within the keyspace _credentials_ defined by `@Secret` on `Credentials`.
<4> Removes the key for the given object from Vault.
====
[[vault.repositories.mapping]]
== Object to Vault JSON Mapping
Vault repositories store objects in Vault using JSON as interchange format. Object mapping between JSON and
the entity is done by `VaultConverter`. The converter reads and writes `SecretDocument` that contains the body
from a `VaultResponse`. ``VaultResponse``s are read from Vault and the body is deserialized by
Jackson into a `Map` of `String` and `Object`.
The default `VaultConverter` implementation reads the `Map` with nested values, `List` and `Map` objects and
converts these to entities and vice versa.
Given the `Credentials` type from the previous sections the default mapping is as follows:
====
[source,json]
----
{
"_class": "org.example.Credentials", <1>
"password", "327215", <2>
"socialSecurityNumber": "AAA-GG-SSSS",
"address": { <3>
"street": "308 Negra Arroyo Lane",
"city": "Albuquerque",
"state": "New Mexico",
"zip":"87104"
}
}
----
<1> The `_class` attribute is included on root level as well as on any nested interface or abstract types.
<2> Simple property values are mapped by path.
<3> Properties of complex types are mapped as nested objects.
====
NOTE: The `@Id` property must be mapped to `String`.
[cols="1,2,3", options="header"]
.Default Mapping Rules
|===
| Type
| Sample
| Mapped Value
| Simple Type +
(eg. String)
| String firstname = "Walter";
| firstname = "Walter"
| Complex Type +
(eg. Address)
| Address adress = new Address("308 Negra Arroyo Lane");
| address: { "street": "308 Negra Arroyo Lane" }
| List +
of Simple Type
| List<String> nicknames = asList("walt", "heisenberg");
| nicknames: ["walt", "heisenberg"]
| Map +
of Simple Type
| Map<String, Integer> atts = asMap("age", 51)
| atts : {"age" : 51}
| List +
of Complex Type
| List<Address> addresses = asList(new Address("308…
| address: [{ "street": "308 Negra Arroyo Lane" }, …]
|===
You can customize the mapping behavior by registering a `Converter` in `VaultCustomConversions`.
Those converters can take care of converting from/to a type such as `LocalDate` as well as `SecretDocument`
whereas the first one is suitable for converting simple properties and the last one complex types to their JSON
representation. The second option offers full control over the resulting `SecretDocument`. Writing objects to `Vault`
will delete the content and re-create the whole entry, so not mapped data will be lost.
[[vault.repositories.queries]]
== Queries and Query Methods
Query methods allow automatic derivation of simple queries from the method name. Vault has no query engine but
requires direct access of HTTP context paths. Vault query methods translate Vault's API possibilities to queries.
A query method execution lists children under a context path, applies filtering to the Id, optionally limits the
Id stream with offset/limit and applies sorting after fetching the results.
.Sample Repository Query Method
====
[source,java]
----
public interface CredentialsRepository extends CrudRepository<Credentials, String> {
List<Credentials> findByIdStartsWith(String prefix);
}
----
====
NOTE: Query methods for Vault repositories support only queries with predicates on the `@Id` property.
Here's an overview of the keywords supported for Vault.
[cols="1,2" options="header"]
.Supported keywords for query methods
|===
| Keyword
| Sample
| `After`, `GreaterThan`
| `findByIdGreaterThan(String id)`
| `GreaterThanEqual`
| `findByIdGreaterThanEqual(String id)`
| `Before`, `LessThan`
| `findByIdLessThan(String id)`
| `LessThanEqual`
| `findByIdLessThanEqual(String id)`
| `Between`
| `findByIdBetween(String from, String to)`
| `In`
| `findByIdIn(Collection ids)`
| `NotIn`
| `findByIdNotIn(Collection ids)`
| `Like`, `StartingWith`, `EndingWith`
| `findByIdLike(String id)`
| `NotLike`, `IsNotLike`
| `findByIdNotLike(String id)`
| `Containing`
| `findByFirstnameContaining(String id)`
| `NotContaining`
| `findByFirstnameNotContaining(String name)`
| `Regex`
| `findByIdRegex(String id)`
| `(No keyword)`
| `findById(String name)`
| `Not`
| `findByIdNot(String id)`
| `And`
| `findByLastnameAndFirstname`
| `Or`
| `findByLastnameOrFirstname`
| `Is,Equals`
| `findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals`
| `Top,First`
| `findFirst10ByFirstname`,`findTop5ByFirstname`
|===
=== Sorting and Paging
Query methods support sorting and paging by selecting in memory a sublist (offset/limit) Id's retrieved from
a Vault context path. Sorting has is not limited to a particular field, unlike query method predicates.
Unpaged sorting is applied after Id filtering and all resulting secrets are fetched from Vault. This way
a query method fetches only results that are also returned as part of the result.
Using paging and sorting requires secret fetching before filtering the Id's which impacts performance.
Sorting and paging guarantees to return the same result even if the natural order of Id returned by Vault changes.
Therefore, all Id's are fetched from Vault first, then sorting is applied and afterwards filtering and offset/limiting.
.Paging and Sorting Repository
====
[source,java]
----
public interface CredentialsRepository extends PagingAndSortingRepository<Credentials, String> {
List<Credentials> findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix);
List<Credentials> findByIdStarts(String prefix, Pageable pageRequest);
}
----
====

View File

@@ -25,6 +25,8 @@ include::reactive-template.adoc[]
include::propertysource.adoc[]
include::vault-repositories.adoc[]
include::client-support.adoc[]
include::authentication.adoc[]