DATACMNS-626 - Added formatters for Point and Distance.

Ported Spring Data REST's converters for Distance and Point and turned them into Formatter instances. Extended Metric interface to return an abbreviation. Added getUnit() to Distance and tweaked Jackson setup to ignore the additional property.

We now automatically register the news introduced Formatters when using @EnableSpringDataWebSupport so that Point and Distance values can be parsed and printed from and to Strings out of the box.
This commit is contained in:
Oliver Gierke
2015-01-04 16:42:05 +01:00
parent d4a09853f1
commit 23cad97b9a
12 changed files with 511 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2015 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.
@@ -15,6 +15,8 @@
*/
package org.springframework.data.geo;
import org.springframework.util.Assert;
/**
* Value object to create custom {@link Metric}s on the fly.
*
@@ -27,6 +29,7 @@ public class CustomMetric implements Metric {
private static final long serialVersionUID = -2972074177454114228L;
private final double multiplier;
private final String abbreviation;
/**
* Creates a custom {@link Metric} using the given multiplier.
@@ -34,7 +37,22 @@ public class CustomMetric implements Metric {
* @param multiplier
*/
public CustomMetric(double multiplier) {
this(multiplier, "");
}
/**
* Creates a custom {@link Metric} using the given multiplier and abbreviation.
*
* @param multiplier
* @param abbreviation must not be {@literal null}.
*/
public CustomMetric(double multiplier, String abbreviation) {
Assert.notNull(abbreviation, "Abbreviation must not be null!");
this.multiplier = multiplier;
this.abbreviation = abbreviation;
}
/*
@@ -44,4 +62,13 @@ public class CustomMetric implements Metric {
public double getMultiplier() {
return multiplier;
}
/*
* (non-Javadoc)
* @see org.springframework.data.geo.Metric#getAbbreviation()
*/
@Override
public String getAbbreviation() {
return abbreviation;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2014 the original author or authors.
* Copyright 2010-2015 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.
@@ -73,7 +73,7 @@ public class Distance implements Serializable {
}
/**
* Retruns the {@link Metric} of the {@link Distance}.
* Returns the {@link Metric} of the {@link Distance}.
*
* @return the metric
*/
@@ -81,6 +81,16 @@ public class Distance implements Serializable {
return metric;
}
/**
* Returns a {@link String} representation of the unit the distance is in.
*
* @return the unit
* @see Metric#getAbbreviation()
*/
public String getUnit() {
return metric.getAbbreviation();
}
/**
* Adds the given distance to the current one. The resulting {@link Distance} will be in the same metric as the
* current one.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 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.
@@ -18,6 +18,7 @@ package org.springframework.data.geo;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@@ -47,6 +48,7 @@ public class GeoModule extends SimpleModule {
setMixInAnnotation(Polygon.class, PolygonMixin.class);
}
@JsonIgnoreProperties("unit")
static abstract class DistanceMixin {
DistanceMixin(@JsonProperty("value") double value,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2014 the original author or authors.
* Copyright 2011-2015 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.
@@ -32,4 +32,11 @@ public interface Metric extends Serializable {
* @return
*/
double getMultiplier();
/**
* Returns the scientific abbreviation of the unit the {@link Metric} is in.
*
* @return
*/
String getAbbreviation();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2014 the original author or authors.
* Copyright 2011-2015 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.
@@ -24,17 +24,20 @@ package org.springframework.data.geo;
*/
public enum Metrics implements Metric {
KILOMETERS(6378.137), MILES(3963.191), NEUTRAL(1);
KILOMETERS(6378.137, "km"), MILES(3963.191, "mi"), NEUTRAL(1, "");
private final double multiplier;
private final String abbreviation;
/**
* Creates a new {@link Metrics} using the given muliplier.
*
* @param multiplier the earth radius at equator.
*/
private Metrics(double multiplier) {
private Metrics(double multiplier, String abbreviation) {
this.multiplier = multiplier;
this.abbreviation = abbreviation;
}
/*
@@ -44,4 +47,13 @@ public enum Metrics implements Metric {
public double getMultiplier() {
return multiplier;
}
/*
* (non-Javadoc)
* @see org.springframework.data.geo.Metric#getAbbreviation()
*/
@Override
public String getAbbreviation() {
return abbreviation;
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2014-2015 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.geo.format;
import java.text.ParseException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
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.format.Formatter;
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 DistanceFormatter implements Converter<String, Distance>, Formatter<Distance> {
INSTANCE;
private static final Map<String, Metric> SUPPORTED_METRICS;
private static final String INVALID_DISTANCE = "Expected double amount optionally followed by a metrics abbreviation (%s) but got '%s'!";
static {
Map<String, Metric> metrics = new LinkedHashMap<String, Metric>();
for (Metric metric : Metrics.values()) {
metrics.put(metric.getAbbreviation(), metric);
metrics.put(metric.toString().toLowerCase(Locale.US), metric);
}
SUPPORTED_METRICS = Collections.unmodifiableMap(metrics);
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public final Distance convert(String source) {
return source == null ? null : doConvert(source.trim().toLowerCase(Locale.US));
}
/*
* (non-Javadoc)
* @see org.springframework.format.Printer#print(java.lang.Object, java.util.Locale)
*/
@Override
public String print(Distance distance, Locale locale) {
return distance == null ? null : String.format("%s%s", distance.getValue(), distance.getUnit().toLowerCase(locale));
}
/*
* (non-Javadoc)
* @see org.springframework.format.Parser#parse(java.lang.String, java.util.Locale)
*/
@Override
public Distance parse(String text, Locale locale) throws ParseException {
return !StringUtils.hasText(text) ? null : doConvert(text.trim().toLowerCase(locale));
}
/**
* Converts the given {@link String} source into a distance. Expects the source to reflect the {@link Metric} as held
* in the {@link #SUPPORTED_METRICS} map.
*
* @param source must not be {@literal null}.
* @return
*/
private static Distance doConvert(String source) {
for (Entry<String, Metric> 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));
}
}
/**
* Creates a {@link Distance} from the given source String and the {@link Metric} detected.
*
* @param source the raw source {@link String}, must not be {@literal null} or empty.
* @param metric the {@link Metric} detected keyed by the keyword it was detected for, must not be {@literal null}.
* @return
*/
private static Distance fromString(String source, Entry<String, Metric> 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));
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2014-2015 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.geo.format;
import java.text.ParseException;
import java.util.Locale;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.data.geo.Point;
import org.springframework.format.Formatter;
import org.springframework.util.StringUtils;
/**
* Converter to parse two comma-separated doubles into a {@link Point}.
*
* @author Oliver Gierke
*/
public enum PointFormatter implements Converter<String, Point>, Formatter<Point> {
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(longitude, latitude);
} catch (NumberFormatException o_O) {
throw new IllegalArgumentException(String.format(INVALID_FORMAT, source), o_O);
}
}
/*
* (non-Javadoc)
* @see org.springframework.format.Printer#print(java.lang.Object, java.util.Locale)
*/
@Override
public String print(Point point, Locale locale) {
return point == null ? null : String.format("%s,%s", point.getY(), point.getX());
}
/*
* (non-Javadoc)
* @see org.springframework.format.Parser#parse(java.lang.String, java.util.Locale)
*/
@Override
public Point parse(String text, Locale locale) throws ParseException {
return !StringUtils.hasText(text) ? null : convert(text);
}
}