blob: 8c919d5bfcc688fa18f58aa2d54ea256dce73d5e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.services.extension.rest;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.services.Services;
import org.eclipse.skalli.services.rest.RequestContext;
import org.eclipse.skalli.services.rest.RestReader;
import org.eclipse.skalli.services.rest.RestService;
import org.eclipse.skalli.services.rest.RestWriter;
import org.restlet.data.MediaType;
import org.restlet.representation.Representation;
import org.restlet.representation.WriterRepresentation;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.CompactWriter;
import com.thoughtworks.xstream.mapper.MapperWrapper;
/**
* Base class for REST resources based on XStream as converter technology between beans
* and their XML representation.
*
* @param <T> the type of the bean with which this REST representation is associated.
*/
public class ResourceRepresentation<T> extends WriterRepresentation {
private static final String RELATIVE_LINKS = "relative"; //$NON-NLS-1$
private static final String LINKS_QUERY_ATTRIBUTE = "links"; //$NON-NLS-1$
private static final String MEMBERS_QUERY_ATTRIBUTE = "members"; //$NON-NLS-1$
private static final String ALL_MEMBERS = "all"; //$NON-NLS-1$
private T object;
private RequestContext context;
private RestConverter<T> converter;
private XStream xstream;
private Set<Class<?>> annotatedClasses = new HashSet<Class<?>>();
private Set<RestConverter<?>> converters = new HashSet<RestConverter<?>>();
private Map<String, Class<?>> aliases = new HashMap<String, Class<?>>();
private ClassLoader classLoader;
private boolean compact;
/**
* Creates an uninitialized representation for converting an XML representation
* into an instance of the bean.
*/
public ResourceRepresentation() {
super(MediaType.APPLICATION_XML);
}
/**
* Creates a representation initialized with the given bean instance for
* converting it to its XML representation.
*
* @param object a bean instance.
*/
@Deprecated
public ResourceRepresentation(T object) {
this();
this.object = object;
if (object != null) {
addAnnotatedClass(object.getClass());
}
}
/**
* Creates a representation initialized with the given bean instance for
* converting it to its XML representation, and adds additional converters
* for non-standard property types.
*
* @param object a bean instance.
* @param converters additional converters that may be necessary for
* conversion of certain properties or inner beans of the bean.
*/
@Deprecated
public ResourceRepresentation(T object, RestConverter<?>... converters) {
this(object);
if (converters != null) {
for (RestConverter<?> converter: converters) {
addConverter(converter);
}
}
}
/**
* Creates a resource representation for unmarshalling an input stream to
* an object using the given REST converter. The REST converter must implement
* {@link RestConverter#unmarshal(RestReader)}.
*
* @param context
* @param converter
*/
public ResourceRepresentation(RequestContext context, RestConverter<T> converter) {
super(context.getMediaType());
this.context = context;
this.converter = converter;
}
/**
* Creates a resource representation for marshalling a given object to
* an output stream using the given REST converter. The REST converter must implement
* {@link RestConverter#marshal(Object, RestWriter)}.
*
* @param context the request context.
* @param object the object to convert.
* @param converter the converter to use.
*/
public ResourceRepresentation(RequestContext context, T object, RestConverter<T> converter) {
this(context, converter);
this.object = object;
}
@Override
public void write(Writer writer) throws IOException {
if (object != null) {
if (context == null) {
writer.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"); //$NON-NLS-1$
XStream xstream = getXStream();
if (compact) {
xstream.marshal(object, new CompactWriter(writer));
} else {
xstream.toXML(object, writer);
}
} else if (converter.getConversionClass().isAssignableFrom(object.getClass())) {
RestService restService = Services.getRequiredService(RestService.class);
MediaType mediaType = context.getMediaType();
if (!restService.isSupported(context)) {
throw new IOException(MessageFormat.format("Unsupported media type ''{0}''", mediaType));
}
RestWriter restWriter = restService.getRestWriter(writer, context);
String hrefQueryAttr = context.getQueryAttribute(LINKS_QUERY_ATTRIBUTE);
if (RELATIVE_LINKS.equalsIgnoreCase(hrefQueryAttr)) {
restWriter.set(RestWriter.RELATIVE_LINKS);
}
String membersQueryAttr = context.getQueryAttribute(MEMBERS_QUERY_ATTRIBUTE);
if (ALL_MEMBERS.equalsIgnoreCase(membersQueryAttr)) {
restWriter.set(RestWriter.ALL_MEMBERS);
}
try {
converter.marshal(object, restWriter);
} catch (RuntimeException e) {
// don't trust the integrity of plugins!
throw new IOException(MessageFormat.format("Failed to write response for {0}",
context.getPath()), e);
} finally {
restWriter.flush();
}
} else {
throw new IOException("Failed to create resource representation");
}
}
}
public T read(Reader reader) throws IOException, RestException {
RestService restService = Services.getRequiredService(RestService.class);
if (!restService.isSupported(context)) {
throw new IOException(MessageFormat.format("Unsupported media type ''{0}''", context.getMediaType()));
}
RestReader restReader = restService.getRestReader(reader, context);
try {
return converter.unmarshal(restReader);
} catch (RuntimeException e) {
// don't trust the integrity of plugins!
throw new RestException(MessageFormat.format("Failed to read request {0} {1}",
context.getAction(), context.getPath()), e);
}
}
/**
* Reads the XML representation of a bean from a given representation (e.g. a string
* or a socket) and initializes a bean instance from it.
*
* @param representation the representation from which to read the XML representation of the bean.
* @param c the class of the bean to instantiate.
*
* @return an instance of the requested bean class initialized from it XML representation.
*
* @throws IOException if reading the bean caused an i/o error.
* @throws ClassCastExeption if the XML representation of the bean cannot be casted to the
* requested class.
*/
@Deprecated
public T read(Representation representation, Class<T> c) throws IOException {
return read(representation.getStream(), c);
}
/**
* Reads the XML representation of a bean from a given input stream and initializes
* a bean instance from it.
*
* @param in the stream to read.
* @param c the class of the bean to instantiate.
*
* @return an instance of the requested bean class initialized from it XML representation.
*
* @throws IOException if reading the bean caused an i/o error.
* @throws ClassCastExeption if the XML representation of the bean cannot be casted to the
* requested class.
*/
@Deprecated
public T read(InputStream in, Class<T> c) throws IOException {
return c.cast(getXStream().fromXML(in));
}
/**
* Adds an additional class with XStream annotations that represents an inner structure
* of the class to convert, e.g. the entries of a collection-like property. If the class
* to convert and the classes representing its inner structure live in different bundles,
* calling this method is mandatory.
* <p>
* The class to convert is automatically added.
*
* @param annotatedClass the class to add to the conversion.
*/
@Deprecated
public void addAnnotatedClass(Class<?> annotatedClass) {
if (annotatedClass != null) {
annotatedClasses.add(annotatedClass);
}
}
/**
* Adds an alias name for a given type.
*
* @param name the alias.
* @param type the class for which to use the alias.
*/
@Deprecated
public void addAlias(String name, Class<?> type) {
if (StringUtils.isNotBlank(name) && type != null) {
aliases.put(name, type);
}
}
/**
* Adds an additional converter for the conversion of certain properties
* or inner classes, which are not supported by XStream out-of-the-box.
*
* @param converter the REST converter to add.
*/
@Deprecated
public void addConverter(RestConverter<?> converter) {
if (converter != null) {
converters.add(converter);
}
}
/**
* Sets an alternative classloader for the conversion.
* @param classLoader the classloader to use, or <code>null</code>
* if the default classloader should be used.
*/
@Deprecated
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Sets an alternative {@link XStream} instance. By default, a suitable
* <code>XStream</code> instance is constructed automatically.
*
* @param xstream a <code>XStream</code> instance, or <code>null</code>
* if the default <code>XStream</code> instance should be used.
*/
@Deprecated
public void setXStream(XStream xstream) {
this.xstream = xstream;
}
/**
* Switches compact output on or off (default is off). Switching on
* compact output will remove line breaks and indentations from the output
* performing slightly faster and reducing the amount of data written
* to the underlying socket.
*
* @param compact if <code>true</code> the output is compacted.
*/
@Deprecated
public void setCompact(boolean compact) {
this.compact = compact;
}
/**
* Creates an <code>XStream</code> instance for rendering/parsing the XML
* representation of the bean.
*
* @return an <code>XStream</code> instance pre-initialized with the
* specified {@link #setClassLoader(ClassLoader) class loader},
* {@link #setConverters(RestConverter...) converters} and
* {@link #setAnnotatedClasses(Class...) (inner) bean classes}. A special
* wrapper is used to skip unknown tags in the XML representation.
* {@link XStream#NO_REFERENCES} is set so that references betweem tags
* are always resolved.
*/
@Deprecated
protected XStream getXStream() {
if (xstream != null) {
return xstream;
}
XStream result = new XStream() {
@Override
protected MapperWrapper wrapMapper(MapperWrapper next) {
return new MapperWrapperIgnoreUnknownElements(next);
}
};
for (RestConverter<?> converter : converters) {
result.registerConverter(converter);
result.alias(converter.getAlias(), converter.getConversionClass());
}
for (Class<?> annotatedClass : annotatedClasses) {
result.processAnnotations(annotatedClass);
}
for (Entry<String, Class<?>> alias: aliases.entrySet()) {
result.alias(alias.getKey(), alias.getValue());
}
if (classLoader != null) {
result.setClassLoader(classLoader);
}
result.setMode(XStream.NO_REFERENCES);
return result;
}
@Deprecated
private static class MapperWrapperIgnoreUnknownElements extends MapperWrapper {
public MapperWrapperIgnoreUnknownElements(MapperWrapper next) {
super(next);
}
@SuppressWarnings("rawtypes")
@Override
public boolean shouldSerializeMember(Class definedIn, String fieldName) {
if (definedIn == Object.class) {
return false;
}
return super.shouldSerializeMember(definedIn, fieldName);
}
}
}