diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 61c2ee591..36b76186b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -37,6 +37,9 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.data.domain.PageRequest; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoModule; +import org.springframework.data.geo.Point; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; @@ -60,6 +63,8 @@ import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping; import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; +import org.springframework.data.rest.webmvc.convert.StringToDistanceConverter; +import org.springframework.data.rest.webmvc.convert.StringToPointConverter; import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module; @@ -167,6 +172,15 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); conversionService.addConverter(UUIDConverter.INSTANCE); configureConversionService(conversionService); + + if (!conversionService.canConvert(String.class, Point.class)) { + conversionService.addConverter(StringToPointConverter.INSTANCE); + } + + if (!conversionService.canConvert(String.class, Distance.class)) { + conversionService.addConverter(StringToDistanceConverter.INSTANCE); + } + return conversionService; } @@ -562,12 +576,15 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon peraResolver, backendIdHandlerMethodArgumentResolver()); } + @Autowired GeoModule geoModule; + private ObjectMapper basicObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); // Our special PersistentEntityResource Module objectMapper.registerModule(persistentEntityJackson2Module()); + objectMapper.registerModule(geoModule); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Jackson2DatatypeHelper.configureObjectMapper(objectMapper); // Configure custom Modules diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverter.java new file mode 100644 index 000000000..baed06f76 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.convert; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Metrics; +import org.springframework.util.StringUtils; + +/** + * Converter to create {@link Distance} instances from {@link String} representations. The supported format is a decimal + * followed by whitespace and a metric abbreviation. We currently support the following abbreviations: + * {@value #SUPPORTED_METRICS}. + * + * @author Oliver Gierke + */ +public enum StringToDistanceConverter implements Converter { + + INSTANCE; + + private static final Map SUPPORTED_METRICS; + private static final String INVALID_DISTANCE = "Expected double amount optionally followed by a metrics abbreviation (%s) but got '%s'!"; + + static { + + Map metrics = new LinkedHashMap(); + metrics.put("km", Metrics.KILOMETERS); + metrics.put("miles", Metrics.MILES); + metrics.put("mile", Metrics.MILES); + + SUPPORTED_METRICS = Collections.unmodifiableMap(metrics); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Distance convert(String source) { + + source = source.trim(); + + for (Entry metric : SUPPORTED_METRICS.entrySet()) { + if (source.endsWith(metric.getKey())) { + return fromString(source, metric); + } + } + + try { + return new Distance(Double.parseDouble(source)); + } catch (NumberFormatException o_O) { + throw new IllegalArgumentException(String.format(INVALID_DISTANCE, + StringUtils.collectionToCommaDelimitedString(SUPPORTED_METRICS.keySet()), source)); + } + } + + private Distance fromString(String source, Entry metric) { + + String amountString = source.substring(0, source.indexOf(metric.getKey())); + + try { + return new Distance(Double.parseDouble(amountString), metric.getValue()); + } catch (NumberFormatException o_O) { + throw new IllegalArgumentException(String.format(INVALID_DISTANCE, + StringUtils.collectionToCommaDelimitedString(SUPPORTED_METRICS.keySet()), source)); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToPointConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToPointConverter.java new file mode 100644 index 000000000..cbe12a7da --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/StringToPointConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.convert; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.data.geo.Point; + +/** + * Converter to parse two comma-separated doubles into a {@link Point}. + * + * @author Oliver Gierke + */ +public enum StringToPointConverter implements Converter { + + INSTANCE; + + public static final ConvertiblePair CONVERTIBLE = new ConvertiblePair(String.class, Point.class); + + private static final String INVALID_FORMAT = "Expected two doubles separated by a semicolon but got '%s'!"; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Point convert(String source) { + + String[] parts = source.split(","); + + if (parts.length != 2) { + throw new IllegalArgumentException(String.format(INVALID_FORMAT, source)); + } + + try { + + double latitude = Double.parseDouble(parts[0]); + double longitude = Double.parseDouble(parts[1]); + + return new Point(latitude, longitude); + + } catch (NumberFormatException o_O) { + throw new IllegalArgumentException(String.format(INVALID_FORMAT, source), o_O); + } + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverterUnitTests.java new file mode 100644 index 000000000..fa2c978e0 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToDistanceConverterUnitTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.convert; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.rest.webmvc.convert.StringToDistanceConverter.*; + +import org.junit.Test; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; + +/** + * Unit tests for {@link StringToDistanceConverter}. + * + * @author Oliver Gierke + */ +public class StringToDistanceConverterUnitTests { + + /** + * @see DATAREST-279 + */ + @Test + public void parsesDistanceFromString() { + + Distance reference = new Distance(10.8, Metrics.KILOMETERS); + + assertThat(INSTANCE.convert("10.8km"), is(reference)); + assertThat(INSTANCE.convert(" 10.8km"), is(reference)); + assertThat(INSTANCE.convert(" 10.8 km"), is(reference)); + assertThat(INSTANCE.convert(" 10.8 km "), is(reference)); + } + + /** + * @see DATAREST-279 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsArbitraryNonsense() { + INSTANCE.convert("foo"); + } + + /** + * @see DATAREST-279 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsUnsupportedMetric() { + INSTANCE.convert("10.8cm"); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToPointConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToPointConverterUnitTests.java new file mode 100644 index 000000000..43aeda912 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/convert/StringToPointConverterUnitTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.convert; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.rest.webmvc.convert.StringToPointConverter.*; + +import org.junit.Test; +import org.springframework.data.geo.Point; + +/** + * Unit tests for {@link StringToPointConverter}. + * + * @author Oliver Gierke + */ +public class StringToPointConverterUnitTests { + + /** + * @see DATAREST-279 + */ + @Test + public void parsesPointFromString() { + + Point reference = new Point(10.8, 20.9); + + assertThat(INSTANCE.convert("10.8,20.9"), is(reference)); + assertThat(INSTANCE.convert(" 10.8,20.9 "), is(reference)); + assertThat(INSTANCE.convert(" 10.8 ,20.9"), is(reference)); + assertThat(INSTANCE.convert(" 10.8 , 20.9 "), is(reference)); + } + + /** + * @see DATAREST-279 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsArbitraryNonsense() { + INSTANCE.convert("foo"); + } + + /** + * @see DATAREST-279 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsMoreThanTwoCoordinates() { + INSTANCE.convert("10.8,20.9,30.10"); + } + + /** + * @see DATAREST-279 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidCoordinate() { + INSTANCE.convert("10.8,foo"); + } +}