Skip to content

Commit

Permalink
IGNITE-23756 Add custom converters support for Spring Data (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
myskov authored Nov 27, 2024
1 parent 78478dc commit a51fb6d
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.ignite.springdata.repository.query;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder;

/**
* Custom conversions implementation.
* An application can define its own converter by defining the following bean:
* <pre>
* {@code
* @Bean
* public CustomConversions customConversions() {
* return new IgniteCustomConversions(Arrays.asList(new LocalDateTimeWriteConverter()));
* }
* }
* </pre>
*/
public class IgniteCustomConversions extends CustomConversions {

private static final StoreConversions STORE_CONVERSIONS;

static {
List<Object> converters = new ArrayList<>();
converters.add(new TimestampToLocalDateTimeConverter());
converters.add(new TimestampToDateConverter());

List<Object> storeConverters = Collections.unmodifiableList(converters);
STORE_CONVERSIONS = StoreConversions.of(SimpleTypeHolder.DEFAULT, storeConverters);
}

public IgniteCustomConversions() {
this(Collections.emptyList());
}

public IgniteCustomConversions(List<?> converters) {
super(STORE_CONVERSIONS, converters);
}

@WritingConverter
static class TimestampToLocalDateTimeConverter implements Converter<Timestamp, LocalDateTime> {
@Override public LocalDateTime convert(Timestamp source) {
return source.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}

@WritingConverter
static class TimestampToDateConverter implements Converter<Timestamp, Date> {
@Override public Date convert(Timestamp source) {
return new Date((source).getTime());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.apache.ignite.springdata.repository.query.StringQuery.ParameterBinding;
import org.apache.ignite.springdata.repository.query.StringQuery.ParameterBindingParser;
import org.jetbrains.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -249,6 +250,8 @@ private enum ReturnStrategy {
/** Static query configuration. */
private final DynamicQueryConfig staticQueryConfiguration;

private final ConversionService conversionService;

/**
* Instantiates a new Ignite repository query.
*
Expand All @@ -259,6 +262,7 @@ private enum ReturnStrategy {
* @param cache Cache.
* @param staticQueryConfiguration the query configuration
* @param queryMethodEvaluationContextProvider the query method evaluation context provider
* @param conversionService Conversion service.
*/
public IgniteRepositoryQuery(
RepositoryMetadata metadata,
Expand All @@ -267,7 +271,8 @@ public IgniteRepositoryQuery(
ProjectionFactory factory,
IgniteCacheProxy<?, ?> cache,
@Nullable DynamicQueryConfig staticQueryConfiguration,
QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider) {
QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider,
ConversionService conversionService) {
this.metadata = metadata;
this.mtd = mtd;
this.factory = factory;
Expand All @@ -288,6 +293,7 @@ public IgniteRepositoryQuery(

expressionParser = new SpelExpressionParser();
this.queryMethodEvaluationContextProvider = queryMethodEvaluationContextProvider;
this.conversionService = conversionService;

qMethod = getQueryMethod();

Expand Down Expand Up @@ -460,19 +466,6 @@ private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
return false;
}

/**
* When select fields by query H2 returns Timestamp for types java.util.Date and java.qryStr.Timestamp
*
* @see org.apache.ignite.internal.processors.query.h2.H2DatabaseType map.put(Timestamp.class, TIMESTAMP)
* map.put(java.util.Date.class, TIMESTAMP) map.put(java.qryStr.Date.class, DATE)
*/
private static <T> T fixExpectedType(final Object object, final Class<T> expected) {
if (expected != null && object instanceof java.sql.Timestamp && expected.equals(java.util.Date.class))
return (T)new java.util.Date(((java.sql.Timestamp)object).getTime());

return (T)object;
}

/**
* @param cfg Config.
*/
Expand Down Expand Up @@ -892,7 +885,7 @@ private <V> V rowToEntity(List<?> row, FieldsQueryCursor<?> cursor) {
Field entityField = domainEntitiyFields.get(cursor.getFieldName(i).toLowerCase());

if (entityField != null)
FieldUtils.writeField(entityField, res, fixExpectedType(row.get(i), entityField.getType()), true);
FieldUtils.writeField(entityField, res, convert(row.get(i), entityField.getType()), true);
}

return (V)res;
Expand All @@ -902,6 +895,14 @@ private <V> V rowToEntity(List<?> row, FieldsQueryCursor<?> cursor) {
}
}

private <T> T convert(Object source, Class<?> targetType) {
if (conversionService.canConvert(source.getClass(), targetType)) {
return (T) conversionService.convert(source, targetType);
} else {
return (T) source;
}
}

/**
* Validates operations that requires Pageable parameter
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@
import org.apache.ignite.springdata.repository.config.DynamicQueryConfig;
import org.apache.ignite.springdata.repository.config.Query;
import org.apache.ignite.springdata.repository.config.RepositoryConfig;
import org.apache.ignite.springdata.repository.query.IgniteCustomConversions;
import org.apache.ignite.springdata.repository.query.IgniteQuery;
import org.apache.ignite.springdata.repository.query.IgniteQueryGenerator;
import org.apache.ignite.springdata.repository.query.IgniteRepositoryQuery;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
Expand Down Expand Up @@ -62,17 +68,31 @@ public class IgniteRepositoryFactory extends RepositoryFactorySupport {
/** Ignite proxy instance associated with the current repository. */
private final IgniteProxy ignite;

private final ConversionService conversionService;

/**
* @param ctx Spring Application context.
* @param repoInterface Repository interface.
*/
public IgniteRepositoryFactory(ApplicationContext ctx, Class<?> repoInterface) {
ignite = ctx.getBean(IgniteProxy.class, repoInterface);

ConfigurableApplicationContext configurableCtx = (ConfigurableApplicationContext) ctx;
if (configurableCtx.getBeanNamesForType(CustomConversions.class).length == 0) {
configurableCtx.getBeanFactory().registerSingleton(CustomConversions.class.getCanonicalName(), new IgniteCustomConversions());
}

beanExpressionContext = new BeanExpressionContext(
new DefaultListableBeanFactory(ctx.getAutowireCapableBeanFactory()),
null);

CustomConversions customConversions = configurableCtx.getBean(CustomConversions.class);
DefaultConversionService defaultConversionService = new DefaultConversionService();
if (defaultConversionService instanceof GenericConversionService) {
customConversions.registerConvertersIn(defaultConversionService);
}
conversionService = defaultConversionService;

RepositoryConfig cfg = getRepositoryConfiguration(repoInterface);

String cacheName = evaluateExpression(cfg.cacheName());
Expand Down Expand Up @@ -152,7 +172,7 @@ private String evaluateExpression(String spelExpression) {
if (key != QueryLookupStrategy.Key.CREATE) {
return new IgniteRepositoryQuery(metadata, qry, mtd, factory, cache,
annotatedIgniteQry ? DynamicQueryConfig.fromQueryAnnotation(annotation) : null,
evaluationContextProvider);
evaluationContextProvider, conversionService);
}
}

Expand All @@ -163,7 +183,8 @@ private String evaluateExpression(String spelExpression) {
}

return new IgniteRepositoryQuery(metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd, factory,
cache, DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider);
cache, DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider,
conversionService);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.ignite.springdata;

import java.time.LocalDateTime;
import org.apache.ignite.springdata.misc.ApplicationConfiguration;
import org.apache.ignite.springdata.misc.CustomConvertersApplicationConfiguration;
import org.apache.ignite.springdata.misc.Person;
import org.apache.ignite.springdata.misc.PersonRepository;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IgniteSpringDataConversionsTest extends GridCommonAbstractTest {
/** Repository. */
protected PersonRepository repo;

/** Context. */
protected static AnnotationConfigApplicationContext ctx;

@Override protected void beforeTest() {
ctx = new AnnotationConfigApplicationContext();
}

@Test
public void testPutGetWithDefaultConverters() {
init(ApplicationConfiguration.class);

Person person = new Person("some_name", "some_surname", LocalDateTime.now());

assertEquals(person, savePerson(person));
}

@Test
public void testPutGetWithCustomConverters() {
init(CustomConvertersApplicationConfiguration.class);

Person person = new Person("some_name", "some_surname", LocalDateTime.now());

assertNull(savePerson(person).getCreatedAt());
}

private void init(Class<? extends ApplicationConfiguration> applicationConfiguration) {
ctx.register(applicationConfiguration);
ctx.refresh();

repo = ctx.getBean(PersonRepository.class);
}

private Person savePerson(Person person) {
int id = 1;

assertEquals(person, repo.save(id, person));
assertTrue(repo.existsById(id));

return repo.selectByFirstNameWithCreatedAt(person.getFirstName()).get(0);
}

@Override protected void afterTest() {
ctx.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.ignite.springdata.misc;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Arrays;
import org.apache.ignite.springdata.repository.query.IgniteCustomConversions;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;

/**
* Test configuration that overrides default converter for LocalDateTime type.
*/
public class CustomConvertersApplicationConfiguration extends ApplicationConfiguration {
@Bean
public CustomConversions customConversions() {
return new IgniteCustomConversions(Arrays.asList(new LocalDateTimeWriteConverter()));
}

static class LocalDateTimeWriteConverter implements Converter<Timestamp, LocalDateTime> {
@Override public LocalDateTime convert(Timestamp source) {
return null;
}
}
}
Loading

0 comments on commit a51fb6d

Please sign in to comment.