mapper initial commit

This commit is contained in:
Keith Donald
2009-10-02 16:28:53 +00:00
parent 5c055ed6dd
commit 341835a142
14 changed files with 1076 additions and 37 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2002-2009 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.mapping;
/**
* Maps between a source and target.
* @author Keith Donald
* @param <S> the source type mapped from
* @param <T> the target type mapped to
*/
public interface Mapper<S, T> {
/**
* Map the source to the target.
* @param source the source to map from
* @param target the target to map to
* @throws MappingException if the mapping process failed
*/
void map(S source, T target);
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2002-2009 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.mapping;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
/**
* Thrown in a map operation fails.
* @see Mapper#map(Object, Object)
* @author Keith Donald
*/
public class MappingException extends RuntimeException {
private List<MappingFailure> mappingFailures;
public MappingException(List<MappingFailure> mappingFailures) {
super((String) null);
this.mappingFailures = mappingFailures;
}
public int getMappingFailureCount() {
return this.mappingFailures.size();
}
public List<MappingFailure> getMappingFailures() {
return this.mappingFailures;
}
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder(getMappingFailureCount() + " mapping failure(s) occurred:");
int i = 1;
for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) {
MappingFailure failure = it.next();
sb.append(" #").append(i + ") ").append(failure.getMessage());
if (it.hasNext()) {
sb.append(",");
}
}
return sb.toString();
}
@Override
public void printStackTrace(PrintStream ps) {
super.printStackTrace(ps);
synchronized (ps) {
ps.println("Failure cause traces:");
int i = 1;
for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) {
MappingFailure failure = it.next();
ps.println("- MappingFailure #" + i + " Cause: ");
Throwable t = failure.getCause();
if (t != null) {
t.printStackTrace(ps);
} else {
ps.println("null");
}
}
}
}
@Override
public void printStackTrace(PrintWriter pw) {
super.printStackTrace(pw);
synchronized (pw) {
pw.println("Failure cause traces:");
int i = 1;
for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) {
MappingFailure failure = it.next();
pw.println("- MappingFailure #" + i + " Cause: ");
Throwable t = failure.getCause();
if (t != null) {
t.printStackTrace(pw);
} else {
pw.println("null");
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2002-2009 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.mapping;
/**
* Indicates an individual mapping failed.
* @author Keith Donald
*/
public class MappingFailure {
private final Throwable cause;
/**
* Create a new mapping failure caused by the exception.
* @param cause the failure cause
*/
public MappingFailure(Throwable cause) {
this.cause = cause;
}
/**
* The failure message.
*/
public String getMessage() {
return getCause().getMessage();
}
/**
* The cause of this failure.
*/
public Throwable getCause() {
return cause;
}
public String toString() {
return "[MappingFailure cause = " + cause + "]";
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2002-2009 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.mapping.support;
import java.beans.PropertyDescriptor;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.BeanUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
class BeanMappableType implements MappableType<Object> {
public Set<String> getFields(Object object) {
Set<String> fields = new LinkedHashSet<String>();
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(object.getClass());
for (PropertyDescriptor descriptor : descriptors) {
String propertyName = descriptor.getName();
if (propertyName.equals("class")) {
continue;
}
fields.add(descriptor.getName());
}
return fields;
}
public EvaluationContext getEvaluationContext(Object instance, ConversionService conversionService) {
StandardEvaluationContext context = new StandardEvaluationContext(instance);
context.setTypeConverter(new StandardTypeConverter(conversionService));
return context;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2009 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.mapping.support;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
class MapMappableType implements MappableType<Map<? extends Object, ? extends Object>> {
public Set<String> getFields(Map<? extends Object, ? extends Object> object) {
LinkedHashSet<String> fields = new LinkedHashSet<String>(object.size(), 1);
for (Object key : object.keySet()) {
if (key != null && key instanceof String) {
fields.add((String) key);
}
}
return fields;
}
public EvaluationContext getEvaluationContext(Map<? extends Object, ? extends Object> object,
ConversionService conversionService) {
StandardEvaluationContext context = new StandardEvaluationContext(object);
context.setTypeConverter(new StandardTypeConverter(conversionService));
context.addPropertyAccessor(new MapAccessor());
return context;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2009 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.mapping.support;
import java.util.Set;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationContext;
/**
* Encapsulates mapping context for a type of object.
* @param <T> the object type
* @author Keith Donald
*/
interface MappableType<T> {
/**
* The fields of the object that are eligible for mapping, including any nested fields.
*/
Set<String> getFields(T object);
/**
* A evaluation context for accessing the object.
*/
EvaluationContext getEvaluationContext(T object, ConversionService conversionService);
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2002-2009 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.mapping.support;
import java.util.Collection;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.ConverterFactoryGenericConverter;
import org.springframework.core.convert.support.ConverterGenericConverter;
import org.springframework.core.convert.support.GenericConverter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.mapping.MappingFailure;
class Mapping implements MappingConfiguration {
private Expression source;
private Expression target;
private GenericConverter converter;
public Mapping(Expression source, Expression target) {
this.source = source;
this.target = target;
}
public String getSourceExpressionString() {
return this.source.getExpressionString();
}
public String getTargetExpressionString() {
return this.target.getExpressionString();
}
public MappingConfiguration setConverter(Converter<?, ?> converter) {
return setGenericConverter(new ConverterGenericConverter(converter));
}
public MappingConfiguration setConverterFactory(ConverterFactory<?, ?> converter) {
return setGenericConverter(new ConverterFactoryGenericConverter(converter));
}
public MappingConfiguration setGenericConverter(GenericConverter converter) {
this.converter = converter;
return this;
}
public void map(EvaluationContext sourceContext, EvaluationContext targetContext,
Collection<MappingFailure> failures) {
try {
Object value = this.source.getValue(sourceContext);
if (this.converter != null) {
value = this.converter.convert(value, this.source.getValueTypeDescriptor(sourceContext), this.target
.getValueTypeDescriptor(targetContext));
}
this.target.setValue(targetContext, value);
} catch (Exception e) {
failures.add(new MappingFailure(e));
}
}
public int hashCode() {
return getSourceExpressionString().hashCode() + getTargetExpressionString().hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Mapping)) {
return false;
}
Mapping m = (Mapping) o;
return getSourceExpressionString().equals(m.getSourceExpressionString())
&& getTargetExpressionString().equals(m.getTargetExpressionString());
}
public String toString() {
return "[Mapping<" + getSourceExpressionString() + " -> " + getTargetExpressionString() + ">]";
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2002-2009 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.mapping.support;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.GenericConverter;
/**
* A fluent API for configuring a mapping.
* @see SpelMapper#addMapping(String)
* @see SpelMapper#addMapping(String, String)
* @author Keith Donald
*/
public interface MappingConfiguration {
/**
* Set the type converter to use during this mapping.
* @param converter the converter
* @return this, for call chaining
*/
MappingConfiguration setConverter(Converter<?, ?> converter);
/**
* Set the type converter factory to use during this mapping.
* @param converter the converter factory
* @return this, for call chaining
*/
MappingConfiguration setConverterFactory(ConverterFactory<?, ?> converterFactory);
/**
* Set the generic converter to use during this mapping.
* A generic converter allows access to source and target field type descriptors.
* These descriptors provide additional context that can be used during type conversion.
* @param converter the generic converter
* @return this, for call chaining
*/
MappingConfiguration setGenericConverter(GenericConverter converter);
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2002-2009 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.mapping.support;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration;
import org.springframework.mapping.Mapper;
import org.springframework.mapping.MappingException;
import org.springframework.mapping.MappingFailure;
/**
* A generic object mapper implementation based on the Spring Expression Language (SpEL).
* @author Keith Donald
* @see #setAutoMappingEnabled(boolean)
* @see #addMapping(String)
* @see #addMapping(String, String)
*/
public class SpelMapper implements Mapper<Object, Object> {
private static final MappableTypeFactory mappableTypeFactory = new MappableTypeFactory();
private static final SpelExpressionParser sourceExpressionParser = new SpelExpressionParser();
private static final SpelExpressionParser targetExpressionParser = new SpelExpressionParser(
SpelExpressionParserConfiguration.CreateObjectIfAttemptToReferenceNull
| SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize);
private final Set<Mapping> mappings = new LinkedHashSet<Mapping>();
private boolean autoMappingEnabled = true;
private final DefaultConversionService conversionService = new DefaultConversionService();
public void setAutoMappingEnabled(boolean autoMappingEnabled) {
this.autoMappingEnabled = autoMappingEnabled;
}
public MappingConfiguration addMapping(String expression) {
return addMapping(expression, expression);
}
public ConverterRegistry getConverterRegistry() {
return conversionService;
}
public MappingConfiguration addMapping(String sourceExpression, String targetExpression) {
Expression sourceExp;
try {
sourceExp = sourceExpressionParser.parseExpression(sourceExpression);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping source '" + sourceExpression
+ "' is not a parseable value expression", e);
}
Expression targetExp;
try {
targetExp = targetExpressionParser.parseExpression(targetExpression);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping target '" + targetExpression
+ "' is not a parseable property expression", e);
}
Mapping mapping = new Mapping(sourceExp, targetExp);
this.mappings.add(mapping);
return mapping;
}
public void map(Object source, Object target) {
EvaluationContext sourceContext = getMappingContext(source);
EvaluationContext targetContext = getMappingContext(target);
List<MappingFailure> failures = new LinkedList<MappingFailure>();
for (Mapping mapping : this.mappings) {
mapping.map(sourceContext, targetContext, failures);
}
Set<Mapping> autoMappings = getAutoMappings(source, target);
for (Mapping mapping : autoMappings) {
mapping.map(sourceContext, targetContext, failures);
}
if (!failures.isEmpty()) {
throw new MappingException(failures);
}
}
private EvaluationContext getMappingContext(Object object) {
return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService);
}
private Set<Mapping> getAutoMappings(Object source, Object target) {
if (this.autoMappingEnabled) {
Set<Mapping> autoMappings = new LinkedHashSet<Mapping>();
Set<String> sourceFields = getMappableFields(source);
Set<String> targetFields = getMappableFields(target);
for (String field : sourceFields) {
if (!explicitlyMapped(field) && targetFields.contains(field)) {
Expression sourceExpression;
Expression targetExpression;
try {
sourceExpression = sourceExpressionParser.parseExpression(field);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping source '" + field
+ "' is not a parseable value expression", e);
}
try {
targetExpression = targetExpressionParser.parseExpression(field);
} catch (ParseException e) {
throw new IllegalArgumentException("The mapping target '" + field
+ "' is not a parseable value expression", e);
}
Mapping mapping = new Mapping(sourceExpression, targetExpression);
autoMappings.add(mapping);
}
}
return autoMappings;
} else {
return Collections.emptySet();
}
}
private Set<String> getMappableFields(Object object) {
return mappableTypeFactory.getMappableType(object).getFields(object);
}
private boolean explicitlyMapped(String field) {
for (Mapping mapping : this.mappings) {
if (mapping.getSourceExpressionString().equals(field)) {
return true;
}
}
return false;
}
private static class MappableTypeFactory {
private static final MapMappableType MAP_MAPPABLE_TYPE = new MapMappableType();
private static final BeanMappableType BEAN_MAPPABLE_TYPE = new BeanMappableType();
@SuppressWarnings("unchecked")
public <T> MappableType<T> getMappableType(T object) {
if (object instanceof Map<?, ?>) {
return (MappableType<T>) MAP_MAPPABLE_TYPE;
} else {
return (MappableType<T>) BEAN_MAPPABLE_TYPE;
}
}
}
}