Support generic target types in the RestTemplate
This change makes it possible to use the RestTemplate to read an HTTP response into a target generic type object. The RestTemplate has three new exchange(...) methods that accept ParameterizedTypeReference -- a new class that enables capturing and passing generic type info. See the Javadoc of the three new methods in RestOperations for a short example. To support this feature, the HttpMessageConverter is now extended by GenericHttpMessageConverter, which adds a method for reading an HttpInputMessage to a specific generic type. The new interface is implemented by the MappingJacksonHttpMessageConverter and also by a new Jaxb2CollectionHttpMessageConverter that can read read a generic Collection where the generic type is a JAXB type annotated with @XmlRootElement or @XmlType. Issue: SPR-7023
This commit is contained in:
committed by
Rossen Stoyanchev
parent
789e12a0c7
commit
ed3823b045
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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.http.converter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* A specialization of {@link HttpMessageConverter} that can convert an HTTP
|
||||
* request into a target object of a specified generic type.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 3.2
|
||||
*
|
||||
* @see ParameterizedTypeReference
|
||||
*/
|
||||
public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
|
||||
|
||||
/**
|
||||
* Indicates whether the given type can be read by this converter.
|
||||
* @param type the type to test for readability
|
||||
* @param mediaType the media type to read, can be {@code null} if not specified.
|
||||
* Typically the value of a {@code Content-Type} header.
|
||||
* @return {@code true} if readable; {@code false} otherwise
|
||||
*/
|
||||
boolean canRead(Type type, MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Read an object of the given type form the given input message, and returns it.
|
||||
* @param clazz the type of object to return. This type must have previously
|
||||
* been passed to the {@link #canRead canRead} method of this interface,
|
||||
* which must have returned {@code true}.
|
||||
* @param type the type of the target object
|
||||
* @param inputMessage the HTTP input message to read from
|
||||
* @return the converted object
|
||||
* @throws IOException in case of I/O errors
|
||||
* @throws HttpMessageNotReadableException in case of conversion errors
|
||||
*/
|
||||
T read(Type type, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException;
|
||||
|
||||
}
|
||||
@@ -17,17 +17,10 @@
|
||||
package org.springframework.http.converter.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonEncoding;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -36,6 +29,15 @@ import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
|
||||
* that can read and write JSON using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
|
||||
@@ -50,7 +52,8 @@ import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
* @since 3.1.2
|
||||
* @see org.springframework.web.servlet.view.json.MappingJackson2JsonView
|
||||
*/
|
||||
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object> {
|
||||
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
|
||||
implements GenericHttpMessageConverter<Object> {
|
||||
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
@@ -63,7 +66,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new {@code BindingJacksonHttpMessageConverter}.
|
||||
* Construct a new {@code MappingJackson2HttpMessageConverter}.
|
||||
*/
|
||||
public MappingJackson2HttpMessageConverter() {
|
||||
super(new MediaType("application", "json", DEFAULT_CHARSET));
|
||||
@@ -125,7 +128,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
return canRead((Type) clazz, mediaType);
|
||||
}
|
||||
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type);
|
||||
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
|
||||
}
|
||||
|
||||
@@ -145,6 +152,17 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
public Object read(Type type, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(type);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
|
||||
try {
|
||||
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
|
||||
}
|
||||
@@ -153,6 +171,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
@@ -180,24 +199,24 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
|
||||
|
||||
/**
|
||||
* Return the Jackson {@link JavaType} for the specified class.
|
||||
* Return the Jackson {@link JavaType} for the specified type.
|
||||
* <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)},
|
||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
||||
* For instance:
|
||||
* <pre class="code">
|
||||
* protected JavaType getJavaType(Class<?> clazz) {
|
||||
* if (List.class.isAssignableFrom(clazz)) {
|
||||
* return objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
|
||||
* protected JavaType getJavaType(Type type) {
|
||||
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
|
||||
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
|
||||
* } else {
|
||||
* return super.getJavaType(clazz);
|
||||
* return super.getJavaType(type);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* @param clazz the class to return the java type for
|
||||
* @param type the type to return the java type for
|
||||
* @return the java type
|
||||
*/
|
||||
protected JavaType getJavaType(Class<?> clazz) {
|
||||
return objectMapper.constructType(clazz);
|
||||
protected JavaType getJavaType(Type type) {
|
||||
return this.objectMapper.constructType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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
|
||||
* 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,
|
||||
@@ -17,9 +17,11 @@
|
||||
package org.springframework.http.converter.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||
import org.codehaus.jackson.JsonEncoding;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.JsonProcessingException;
|
||||
@@ -27,16 +29,16 @@ import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.SerializationConfig;
|
||||
import org.codehaus.jackson.map.type.TypeFactory;
|
||||
import org.codehaus.jackson.type.JavaType;
|
||||
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||
|
||||
/**
|
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
|
||||
* that can read and write JSON using <a href="http://jackson.codehaus.org/">Jackson's</a> {@link ObjectMapper}.
|
||||
@@ -50,7 +52,8 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||
* @since 3.0
|
||||
* @see org.springframework.web.servlet.view.json.MappingJacksonJsonView
|
||||
*/
|
||||
public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
|
||||
public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
|
||||
implements GenericHttpMessageConverter<Object> {
|
||||
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
@@ -63,7 +66,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new {@code BindingJacksonHttpMessageConverter}.
|
||||
* Construct a new {@code MappingJacksonHttpMessageConverter}.
|
||||
*/
|
||||
public MappingJacksonHttpMessageConverter() {
|
||||
super(new MediaType("application", "json", DEFAULT_CHARSET));
|
||||
@@ -125,7 +128,11 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
return canRead((Type) clazz, mediaType);
|
||||
}
|
||||
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type);
|
||||
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
|
||||
}
|
||||
|
||||
@@ -145,6 +152,17 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
public Object read(Type type, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(type);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
|
||||
try {
|
||||
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
|
||||
}
|
||||
@@ -180,24 +198,24 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
|
||||
|
||||
/**
|
||||
* Return the Jackson {@link JavaType} for the specified class.
|
||||
* Return the Jackson {@link JavaType} for the specified type.
|
||||
* <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)},
|
||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
||||
* For instance:
|
||||
* <pre class="code">
|
||||
* protected JavaType getJavaType(Class<?> clazz) {
|
||||
* if (List.class.isAssignableFrom(clazz)) {
|
||||
* protected JavaType getJavaType(Type type) {
|
||||
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
|
||||
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
|
||||
* } else {
|
||||
* return super.getJavaType(clazz);
|
||||
* return super.getJavaType(type);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* @param clazz the class to return the java type for
|
||||
* @param type the type to return the java type for
|
||||
* @return the java type
|
||||
*/
|
||||
protected JavaType getJavaType(Class<?> clazz) {
|
||||
return TypeFactory.type(clazz);
|
||||
protected JavaType getJavaType(Type type) {
|
||||
return TypeFactory.type(type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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.http.converter.xml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.UnmarshalException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.XMLStreamReader;
|
||||
import javax.xml.transform.Result;
|
||||
import javax.xml.transform.Source;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConversionException;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
|
||||
/**
|
||||
* An {@code HttpMessageConverter} that can read XML collections using JAXB2.
|
||||
*
|
||||
* <p>This converter can read {@linkplain Collection collections} that contain classes
|
||||
* annotated with {@link XmlRootElement} and {@link XmlType}. Note that this converter
|
||||
* does not support writing.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 3.2
|
||||
*/
|
||||
public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
|
||||
extends AbstractJaxb2HttpMessageConverter<T> implements GenericHttpMessageConverter<T> {
|
||||
|
||||
private final XMLInputFactory inputFactory = createXmlInputFactory();
|
||||
|
||||
/**
|
||||
* Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
|
||||
* required generic type information in order to read a Collection.
|
||||
*/
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>Jaxb2CollectionHttpMessageConverter can read a generic
|
||||
* {@link Collection} where the generic type is a JAXB type annotated with
|
||||
* {@link XmlRootElement} or {@link XmlType}.
|
||||
*/
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
if (!(type instanceof ParameterizedType)) {
|
||||
return false;
|
||||
}
|
||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
||||
if (!(parameterizedType.getRawType() instanceof Class)) {
|
||||
return false;
|
||||
}
|
||||
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
|
||||
if (!(Collection.class.isAssignableFrom(rawType))) {
|
||||
return false;
|
||||
}
|
||||
if (parameterizedType.getActualTypeArguments().length != 1) {
|
||||
return false;
|
||||
}
|
||||
Type typeArgument = parameterizedType.getActualTypeArguments()[0];
|
||||
if (!(typeArgument instanceof Class)) {
|
||||
return false;
|
||||
}
|
||||
Class<?> typeArgumentClass = (Class<?>) typeArgument;
|
||||
return (typeArgumentClass.isAnnotationPresent(XmlRootElement.class) ||
|
||||
typeArgumentClass.isAnnotationPresent(XmlType.class)) && canRead(mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
|
||||
* does not convert collections to XML.
|
||||
*/
|
||||
@Override
|
||||
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
// should not be called, since we override canRead/Write
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T readFromSource(Class<? extends T> clazz, HttpHeaders headers, Source source) throws IOException {
|
||||
// should not be called, since we return false for canRead(Class)
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public T read(Type type, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
||||
T result = createCollection((Class<?>) parameterizedType.getRawType());
|
||||
Class<?> elementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
|
||||
|
||||
try {
|
||||
Unmarshaller unmarshaller = createUnmarshaller(elementClass);
|
||||
XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody());
|
||||
int event = moveToFirstChildOfRootElement(streamReader);
|
||||
|
||||
while (event != XMLStreamReader.END_DOCUMENT) {
|
||||
if (elementClass.isAnnotationPresent(XmlRootElement.class)) {
|
||||
result.add(unmarshaller.unmarshal(streamReader));
|
||||
}
|
||||
else if (elementClass.isAnnotationPresent(XmlType.class)) {
|
||||
result.add(unmarshaller.unmarshal(streamReader, elementClass).getValue());
|
||||
}
|
||||
else {
|
||||
// should not happen, since we check in canRead(Type)
|
||||
throw new HttpMessageConversionException("Could not unmarshal to [" + elementClass + "]");
|
||||
}
|
||||
event = moveToNextElement(streamReader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (UnmarshalException ex) {
|
||||
throw new HttpMessageNotReadableException("Could not unmarshal to [" + elementClass + "]: " + ex.getMessage(), ex);
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex);
|
||||
}
|
||||
catch (XMLStreamException ex) {
|
||||
throw new HttpMessageConversionException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Collection of the given type, with the given initial capacity
|
||||
* (if supported by the Collection type).
|
||||
*
|
||||
* @param collectionClass the type of Collection to instantiate
|
||||
* @return the created Collection instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected T createCollection(Class<?> collectionClass) {
|
||||
if (!collectionClass.isInterface()) {
|
||||
try {
|
||||
return (T) collectionClass.newInstance();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not instantiate collection class [" +
|
||||
collectionClass.getName() + "]: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
else if (List.class.equals(collectionClass)) {
|
||||
return (T) new ArrayList();
|
||||
}
|
||||
else if (SortedSet.class.equals(collectionClass)) {
|
||||
return (T) new TreeSet();
|
||||
}
|
||||
else {
|
||||
return (T) new LinkedHashSet();
|
||||
}
|
||||
}
|
||||
|
||||
private int moveToFirstChildOfRootElement(XMLStreamReader streamReader) throws XMLStreamException {
|
||||
// root
|
||||
int event = streamReader.next();
|
||||
while (event != XMLStreamReader.START_ELEMENT) {
|
||||
event = streamReader.next();
|
||||
}
|
||||
|
||||
// first child
|
||||
event = streamReader.next();
|
||||
while ((event != XMLStreamReader.START_ELEMENT) && (event != XMLStreamReader.END_DOCUMENT)) {
|
||||
event = streamReader.next();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private int moveToNextElement(XMLStreamReader streamReader) throws XMLStreamException {
|
||||
int event = streamReader.getEventType();
|
||||
while (event != XMLStreamReader.START_ELEMENT && event != XMLStreamReader.END_DOCUMENT) {
|
||||
event = streamReader.next();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code XMLInputFactory} that this converter will use to create {@link
|
||||
* javax.xml.stream.XMLStreamReader} and {@link javax.xml.stream.XMLEventReader} objects.
|
||||
* <p/> Can be overridden in subclasses, adding further initialization of the factory.
|
||||
* The resulting factory is cached, so this method will only be called once.
|
||||
*
|
||||
* @return the created factory
|
||||
*/
|
||||
protected XMLInputFactory createXmlInputFactory() {
|
||||
return XMLInputFactory.newInstance();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.web.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -25,12 +26,13 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Response extractor that uses the given {@linkplain HttpMessageConverter entity converters} to convert the response
|
||||
* into a type <code>T</code>.
|
||||
* Response extractor that uses the given {@linkplain HttpMessageConverter entity
|
||||
* converters} to convert the response into a type <code>T</code>.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @see RestTemplate
|
||||
@@ -38,21 +40,31 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
|
||||
|
||||
private final Class<T> responseType;
|
||||
private final Type responseType;
|
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
private final Log logger;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message
|
||||
* converters. The given converters must support the response type.
|
||||
* Creates a new instance of the {@code HttpMessageConverterExtractor} with the given
|
||||
* response type and message converters. The given converters must support the response
|
||||
* type.
|
||||
*/
|
||||
public HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<?>> messageConverters) {
|
||||
this((Type) responseType, messageConverters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@code HttpMessageConverterExtractor} with the given
|
||||
* response type and message converters. The given converters must support the response
|
||||
* type.
|
||||
*/
|
||||
public HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters) {
|
||||
this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class));
|
||||
}
|
||||
|
||||
HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
|
||||
HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
|
||||
Assert.notNull(responseType, "'responseType' must not be null");
|
||||
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
|
||||
this.responseType = responseType;
|
||||
@@ -65,6 +77,39 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
|
||||
if (!hasMessageBody(response)) {
|
||||
return null;
|
||||
}
|
||||
MediaType contentType = getContentType(response);
|
||||
|
||||
Class<T> responseClass = null;
|
||||
if (this.responseType instanceof Class) {
|
||||
responseClass = (Class) this.responseType;
|
||||
}
|
||||
for (HttpMessageConverter messageConverter : this.messageConverters) {
|
||||
if (responseClass != null) {
|
||||
if (messageConverter.canRead(responseClass, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading [" + responseClass.getName() + "] as \"" +
|
||||
contentType + "\" using [" + messageConverter + "]");
|
||||
}
|
||||
return (T) messageConverter.read(responseClass, response);
|
||||
}
|
||||
}
|
||||
else if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
|
||||
if (genericMessageConverter.canRead(this.responseType, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading [" + this.responseType + "] as \"" +
|
||||
contentType + "\" using [" + messageConverter + "]");
|
||||
}
|
||||
return (T) genericMessageConverter.read(this.responseType, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RestClientException(
|
||||
"Could not extract response: no suitable HttpMessageConverter found for response type [" +
|
||||
this.responseType + "] and content type [" + contentType + "]");
|
||||
}
|
||||
|
||||
private MediaType getContentType(ClientHttpResponse response) {
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
if (contentType == null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -72,24 +117,13 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
|
||||
}
|
||||
contentType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
for (HttpMessageConverter messageConverter : messageConverters) {
|
||||
if (messageConverter.canRead(responseType, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading [" + responseType.getName() + "] as \"" + contentType
|
||||
+"\" using [" + messageConverter + "]");
|
||||
}
|
||||
return (T) messageConverter.read(this.responseType, response);
|
||||
}
|
||||
}
|
||||
throw new RestClientException(
|
||||
"Could not extract response: no suitable HttpMessageConverter found for response type [" +
|
||||
this.responseType.getName() + "] and content type [" + contentType + "]");
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given response has a message body.
|
||||
* <p>Default implementation returns {@code false} for a response status of {@code 204} or {@code 304}, or a
|
||||
* {@code Content-Length} of {@code 0}.
|
||||
* Indicates whether the given response has a message body. <p>Default implementation
|
||||
* returns {@code false} for a response status of {@code 204} or {@code 304}, or a {@code
|
||||
* Content-Length} of {@code 0}.
|
||||
*
|
||||
* @param response the response to check for a message body
|
||||
* @return {@code true} if the response has a body, {@code false} otherwise
|
||||
@@ -97,7 +131,8 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
|
||||
*/
|
||||
protected boolean hasMessageBody(ClientHttpResponse response) throws IOException {
|
||||
HttpStatus responseStatus = response.getStatusCode();
|
||||
if (responseStatus == HttpStatus.NO_CONTENT || responseStatus == HttpStatus.NOT_MODIFIED) {
|
||||
if (responseStatus == HttpStatus.NO_CONTENT ||
|
||||
responseStatus == HttpStatus.NOT_MODIFIED) {
|
||||
return false;
|
||||
}
|
||||
long contentLength = response.getHeaders().getContentLength();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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
|
||||
* 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,
|
||||
@@ -20,6 +20,7 @@ import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -396,6 +397,69 @@ public interface RestOperations {
|
||||
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
Class<T> responseType) throws RestClientException;
|
||||
|
||||
/**
|
||||
* Execute the HTTP method to the given URI template, writing the given
|
||||
* request entity to the request, and returns the response as {@link ResponseEntity}.
|
||||
* The given {@link ParameterizedTypeReference} is used to pass generic type information:
|
||||
*
|
||||
* <pre class="code">
|
||||
* ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
|
||||
* ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
|
||||
* </pre>
|
||||
*
|
||||
* @param url the URL
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param requestEntity the entity (headers and/or body) to write to the
|
||||
* request, may be {@code null}
|
||||
* @param responseType the type of the return value
|
||||
* @param uriVariables the variables to expand in the template
|
||||
* @return the response as entity
|
||||
* @since 3.2.0
|
||||
*/
|
||||
<T> ResponseEntity<T> exchange(String url,HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException;
|
||||
|
||||
/**
|
||||
* Execute the HTTP method to the given URI template, writing the given
|
||||
* request entity to the request, and returns the response as {@link ResponseEntity}.
|
||||
* The given {@link ParameterizedTypeReference} is used to pass generic type information:
|
||||
*
|
||||
* <pre class="code">
|
||||
* ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
|
||||
* ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
|
||||
* </pre>
|
||||
*
|
||||
* @param url the URL
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}
|
||||
* @param responseType the type of the return value
|
||||
* @param uriVariables the variables to expand in the template
|
||||
* @return the response as entity
|
||||
* @since 3.2.0
|
||||
*/
|
||||
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
|
||||
|
||||
/**
|
||||
* Execute the HTTP method to the given URI template, writing the given
|
||||
* request entity to the request, and returns the response as {@link ResponseEntity}.
|
||||
* The given {@link ParameterizedTypeReference} is used to pass generic type information:
|
||||
*
|
||||
* <pre class="code">
|
||||
* ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
|
||||
* ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
|
||||
* </pre>
|
||||
*
|
||||
* @param url the URL
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}
|
||||
* @param responseType the type of the return value
|
||||
* @return the response as entity
|
||||
* @since 3.2.0
|
||||
*/
|
||||
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType) throws RestClientException;
|
||||
|
||||
// general execution
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 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
|
||||
* 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,
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
@@ -25,6 +26,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -35,6 +37,7 @@ import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.support.InterceptingHttpAccessor;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
@@ -384,6 +387,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
|
||||
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
|
||||
HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException {
|
||||
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
|
||||
return execute(url, method, requestCallback, responseExtractor, uriVariables);
|
||||
@@ -391,6 +395,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
|
||||
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
|
||||
HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
|
||||
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
|
||||
return execute(url, method, requestCallback, responseExtractor, uriVariables);
|
||||
@@ -398,11 +403,39 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
|
||||
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
Class<T> responseType) throws RestClientException {
|
||||
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
|
||||
return execute(url, method, requestCallback, responseExtractor);
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {
|
||||
|
||||
Type type = responseType.getType();
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
|
||||
return execute(url, method, requestCallback, responseExtractor, uriVariables);
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
|
||||
|
||||
Type type = responseType.getType();
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
|
||||
return execute(url, method, requestCallback, responseExtractor, uriVariables);
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
|
||||
ParameterizedTypeReference<T> responseType) throws RestClientException {
|
||||
|
||||
Type type = responseType.getType();
|
||||
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
|
||||
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
|
||||
return execute(url, method, requestCallback, responseExtractor);
|
||||
}
|
||||
|
||||
// general execution
|
||||
|
||||
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
|
||||
@@ -504,37 +537,62 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
*/
|
||||
private class AcceptHeaderRequestCallback implements RequestCallback {
|
||||
|
||||
private final Class<?> responseType;
|
||||
private final Type responseType;
|
||||
|
||||
private AcceptHeaderRequestCallback(Class<?> responseType) {
|
||||
private AcceptHeaderRequestCallback(Type responseType) {
|
||||
this.responseType = responseType;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void doWithRequest(ClientHttpRequest request) throws IOException {
|
||||
if (responseType != null) {
|
||||
Class<?> responseClass = null;
|
||||
if (responseType instanceof Class) {
|
||||
responseClass = (Class) responseType;
|
||||
}
|
||||
|
||||
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
|
||||
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
|
||||
if (messageConverter.canRead(responseType, null)) {
|
||||
List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes();
|
||||
for (MediaType supportedMediaType : supportedMediaTypes) {
|
||||
if (supportedMediaType.getCharSet() != null) {
|
||||
supportedMediaType =
|
||||
new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
|
||||
}
|
||||
allSupportedMediaTypes.add(supportedMediaType);
|
||||
if (responseClass != null) {
|
||||
if (messageConverter.canRead(responseClass, null)) {
|
||||
allSupportedMediaTypes
|
||||
.addAll(getSupportedMediaTypes(messageConverter));
|
||||
}
|
||||
}
|
||||
else if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||
|
||||
GenericHttpMessageConverter genericMessageConverter =
|
||||
(GenericHttpMessageConverter) messageConverter;
|
||||
if (genericMessageConverter.canRead(responseType, null)) {
|
||||
allSupportedMediaTypes
|
||||
.addAll(getSupportedMediaTypes(messageConverter));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (!allSupportedMediaTypes.isEmpty()) {
|
||||
MediaType.sortBySpecificity(allSupportedMediaTypes);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Setting request Accept header to " + allSupportedMediaTypes);
|
||||
logger.debug("Setting request Accept header to " +
|
||||
allSupportedMediaTypes);
|
||||
}
|
||||
request.getHeaders().setAccept(allSupportedMediaTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<MediaType> getSupportedMediaTypes(HttpMessageConverter<?> messageConverter) {
|
||||
List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes();
|
||||
List<MediaType> result = new ArrayList<MediaType>(supportedMediaTypes.size());
|
||||
for (MediaType supportedMediaType : supportedMediaTypes) {
|
||||
if (supportedMediaType.getCharSet() != null) {
|
||||
supportedMediaType =
|
||||
new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
|
||||
}
|
||||
result.add(supportedMediaType);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -550,7 +608,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private HttpEntityRequestCallback(Object requestBody, Class<?> responseType) {
|
||||
private HttpEntityRequestCallback(Object requestBody, Type responseType) {
|
||||
super(responseType);
|
||||
if (requestBody instanceof HttpEntity) {
|
||||
this.requestEntity = (HttpEntity) requestBody;
|
||||
@@ -618,7 +676,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
|
||||
private final HttpMessageConverterExtractor<T> delegate;
|
||||
|
||||
public ResponseEntityResponseExtractor(Class<T> responseType) {
|
||||
public ResponseEntityResponseExtractor(Type responseType) {
|
||||
if (responseType != null && !Void.class.equals(responseType)) {
|
||||
this.delegate = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user