/*******************************************************************************
* 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.util.Calendar;
import java.util.Collection;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.commons.FormatUtils;
import org.eclipse.skalli.commons.XMLUtils;
import org.eclipse.skalli.model.EntityBase;
import org.eclipse.skalli.services.rest.RestReader;
import org.eclipse.skalli.services.rest.RestWriter;
import org.restlet.data.MediaType;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
/**
* Base class for the implementation of {@link RestConverter REST converters}
* for entities, extensions or similar beans.
*
* The main purpose of this class is to provide some convenience methods
* for common recurring rendering tasks.
*/
public abstract class RestConverterBase<T> implements RestConverter<T> {
/** Relation type for a link of a resource to itself (<tt>rel={@value}</tt>) */
protected static final String SELF_RELATION = "self"; //$NON-NLS-1$
/** Relation type for a link to another project's resource (<tt>rel={@value}</tt>) */
protected static final String PROJECT_RELATION = "project"; //$NON-NLS-1$
/** Relation type for a link to another project's detail page (<tt>rel={@value}</tt>) */
protected static final String BROWSE_RELATION = "browse"; //$NON-NLS-1$
/** Relation type for a permant link to another project's detail page (<tt>rel={@value}</tt>) */
protected static final String PROJECT_PERMALINK = "permalink"; //$NON-NLS-1$
/** Relation type for a link to the project's issues resource (<tt>rel={@value}</tt>) */
protected static final String ISSUES_RELATION = "issues"; //$NON-NLS-1$
/** Relation type for a link to the project's parent project (<tt>rel={@value}</tt>) */
protected static final String PARENT_RELATION = "parent"; //$NON-NLS-1$
/** Relation type for a link to a subproject of a project (<tt>rel={@value}</tt>) */
protected static final String SUBPROJECT_RELATION = "subproject"; //$NON-NLS-1$
/** Relation type for a link to the collection of subprojects of a project (<tt>rel={@value}</tt>) */
protected static final String SUBPROJECTS_RELATION = "subprojects"; //$NON-NLS-1$
/** Relation type for a link to a user resource (<tt>rel={@value}</tt>) */
protected static final String USER_RELATION = "user"; //$NON-NLS-1$
private static final String MODIFIED_BY = "modifiedBy"; //$NON-NLS-1$
private static final String LAST_MODIFIED = "lastModified"; //$NON-NLS-1$
private static final String LAST_MODIFIED_MILLIS = "lastModifiedMillis"; //$NON-NLS-1$
private static final String API_VERSION = "apiVersion"; //$NON-NLS-1$
private static final String NAMESPACE = "namespace"; //$NON-NLS-1$
private static final String HREF = "href"; //$NON-NLS-1$
private static final String REL = "rel"; //$NON-NLS-1$
private static final String LINK = "link"; //$NON-NLS-1$
private final String host;
private final String alias;
private final Class<T> clazz;
/** The writer to use for marshalling. */
protected RestWriter writer;
/** The reader to use for unmarshalling. */
protected RestReader reader;
/**
* Constructs a REST converter for the given class.
*
* @param clazz the class with which this converter is associated.
*/
public RestConverterBase(Class<T> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("argument 'clazz' must not be null");
}
this.clazz = clazz;
this.host = null;
this.alias = null;
}
/**
* Constructs a REST converter for the given class.
*
* @param clazz the class with which this converter is associated.
* @param alias the alias used in the XML representation.
*/
@Deprecated
public RestConverterBase(Class<T> clazz, String alias) {
this(clazz, alias, null);
}
/**
* Constructs a REST converter for the given class.
*
* @param clazz the class with which this converter is associated.
* @param alias the alias to use in the XML representation.
* @param host the host part of the web locator of the server on which this converter is running,
* e.g. <tt>http://localhost:8080</tt>.
*/
@Deprecated
public RestConverterBase(Class<T> clazz, String alias, String host) {
this.clazz = clazz;
this.alias = alias;
this.host = host;
}
@Override
@Deprecated
public String getAlias() {
return alias;
}
@Override
public Class<T> getConversionClass() {
return clazz;
}
@Override
@Deprecated
public boolean canConvert(Class type) {
return type.equals(clazz);
}
/**
* Returns the host, or <code>null</code> if the host is not known.
*/
@Deprecated
protected String getHost() {
return host;
}
@Override
public void marshal(Object obj, RestWriter writer) throws IOException {
if (obj == null) {
return;
}
if (clazz.isAssignableFrom(obj.getClass())) {
this.writer = writer;
marshal(clazz.cast(obj));
} else {
throw new IllegalArgumentException("Cannot convert object of type " + obj.getClass());
}
}
@Override
public T unmarshal(RestReader reader) throws RestException, IOException {
this.reader = reader;
return unmarshal();
}
/**
* Marshals the given object to the underlying {@link RestWriter}.
*
* @param obj the object instance to marshal.
*
* @throws IOException if an i/o error occured.
*/
protected void marshal(T obj) throws IOException {
}
/**
* Unmarshals an object from the underlying {@link RestReader}.
*
* @return an object instance, or <code>null</code> if this
* converter does not support unmarshalling. This default
* implementation always returns <code>null</code>.
*
* @throws RestException if the input could not be parsed successfully.
* @throws IOException if an i/o error occured.
*/
protected T unmarshal() throws RestException, IOException {
return null;
}
/**
* Marshals the namespace attributes provided by this converter to the
* underlying stream writer.
*
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalNSAttributes(HierarchicalStreamWriter writer) {
marshalNSAttributes(writer, this);
}
/**
* Marshals the namespace attributes provided by the given converter to the
* underlying stream writer.
*
* @param converter the converter from which to retrieve namespace information.
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalNSAttributes(HierarchicalStreamWriter writer, RestConverter<?> converter) {
writer.addAttribute(XMLUtils.XMLNS, converter.getNamespace());
writer.addAttribute(XMLUtils.XMLNS_XSI, XMLUtils.XSI_INSTANCE_NS);
if (StringUtils.isNotBlank(host) && StringUtils.isNotBlank(converter.getXsdFileName())) {
writer.addAttribute(XMLUtils.XSI_SCHEMA_LOCATION, getSchemaLocationAttribute(converter));
}
}
@Deprecated
private String getSchemaLocationAttribute(RestConverter<?> converter) {
StringBuilder sb = new StringBuilder();
sb.append(converter.getNamespace());
sb.append(" "); //$NON-NLS-1$
sb.append(host);
sb.append(RestUtils.URL_SCHEMAS);
sb.append(converter.getXsdFileName());
return sb.toString();
}
/**
* Marshals the API version attribute provided by this converter to the
* underlying stream writer.
*
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalApiVersion(HierarchicalStreamWriter writer) {
marshalApiVersion(writer, this);
}
/**
* Marshals the API version attribute provided by the given converter to the
* underlying stream writer.
*
* @param converter the converter from which to retrieve API version information.
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalApiVersion(HierarchicalStreamWriter writer, RestConverter<?> converter) {
writer.addAttribute(API_VERSION, converter.getApiVersion());
}
/**
* Marshal common entity attributes like API version, last modified and last modifier attributes
* provided by this converter to the underlying stream writer.
*
* @param entity the entity from which to retrieve the attributes to marshal.
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalCommonAttributes(HierarchicalStreamWriter writer, EntityBase entity) {
marshalCommonAttributes(writer, entity, this);
}
/**
* Marshal common entity attributes like API version, last modified and last modifier attributes
* provided by the given converter to the underlying stream writer.
*
* @param entity the entity from which to retrieve the attributes to marshal.
* @param converter the converter from which to retrieve API version information.
* @param writer the writer to use for marshaling.
*/
@Deprecated
protected void marshalCommonAttributes(HierarchicalStreamWriter writer,
EntityBase entity, RestConverter<?> converter) {
marshalApiVersion(writer, converter);
String lastModified = entity.getLastModified();
if (StringUtils.isNotBlank(lastModified)) {
writer.addAttribute(LAST_MODIFIED, lastModified);
}
String modifiedBy = entity.getLastModifiedBy();
if (StringUtils.isNotBlank(modifiedBy)) {
writer.addAttribute(MODIFIED_BY, modifiedBy);
}
}
/**
* Marshals a <link> tag with the given relation (<tt>rel</tt>) attribute an URL.
*
* @param writer the writer to use for marshaling.
* @param relation the optional relation attribute, or <code>null</code>.
* @param url the URL of the link.
*/
@Deprecated
protected void writeLink(HierarchicalStreamWriter writer, String relation, String url) {
writer.startNode(LINK);
if (StringUtils.isNotBlank(relation)) {
writer.addAttribute(REL, relation);
}
writer.addAttribute(HREF, url);
writer.endNode();
}
/**
* Marshals a <link> tag with the given relation (<tt>rel</tt>) attribute and an URL
* pointing to the project with the specified unique identifier.
*
* @param writer the writer to use for marshaling.
* @param relation the optional relation attribute, or <code>null</code>. Typical relations
* for projects are {@link #SELF_RELATION}, {@link #PROJECT_RELATION}, {@link #PARENT_RELATION}
* and {@link #SUBPROJECT_RELATION}.
* @param uuid the unique identifier of a project.
*/
@Deprecated
protected void writeProjectLink(HierarchicalStreamWriter writer, String relation, UUID uuid) {
writeLink(writer, relation, host + RestUtils.URL_PROJECTS + uuid.toString());
}
/**
* Marshals a <link> tag with the given relation (<tt>rel</tt>) attribute and an URL
* pointing to the resource associated with the specified user.
*
* @param writer the writer to use for marshaling.
* @param relation the optional relation attribute, or <code>null</code>. In most cases
* the relation should be set to {@link #USER_RELATION}, but under certain circumstances
* it might be useful to define relations with alternative semantics.
* @param userId the user's unique identifier.
*/
@Deprecated
protected void writeUserLink(HierarchicalStreamWriter writer, String relation, String userId) {
writeLink(writer, relation, host + RestUtils.URL_USER + userId);
}
/**
* Marshals an empty node with the given name, e.g. <tt><deleted/></tt>.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the node.
*/
@Deprecated
protected void writeNode(HierarchicalStreamWriter writer, String nodeName) {
writeNode(writer, nodeName, (String) null);
}
/**
* Marshals a node with a <tt>xsd:long</tt> content and a given name.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the node.
* @param value the value of the node.
*/
@Deprecated
protected void writeNode(HierarchicalStreamWriter writer, String nodeName, long value) {
writeNode(writer, nodeName, Long.toString(value));
}
/**
* Marshals a node with string content and a given name.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the node.
* @param value the value of the node.
*/
@Deprecated
protected void writeNode(HierarchicalStreamWriter writer, String nodeName, String value) {
if (StringUtils.isNotBlank(value)) {
writer.startNode(nodeName);
writer.setValue(value);
writer.endNode();
}
}
/**
* Marshals a node with string content and a given name.
* The given value is converted with <code>value.toString()</code>.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the node.
* @param value the value of the node.
*/
@Deprecated
protected void writeNode(HierarchicalStreamWriter writer, String nodeName, Object value) {
String s = value != null? value.toString() : null;
writeNode(writer, nodeName, s);
}
/**
* Marshals a list of nodes with given item name under a common root node.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the root node.
* @param itemName the name of the nodes in the list.
* @param values the collection of values to marshal.
*/
@Deprecated
protected void writeNode(HierarchicalStreamWriter writer, String nodeName, String itemName,
Collection<String> values) {
if (values != null && values.size() > 0) {
writer.startNode(nodeName);
for (String value : values) {
writeNode(writer, itemName, value);
}
writer.endNode();
}
}
/**
* Marshals a node with a <tt>xsd:dateTime</tt> content and a given name.
* The given timestamp is first converted to a {@link Calendar date} with timezone
* UTC and locale EN; afterwards the date is converted to <tt>xsd:dateTime</tt>
* format. The given timestamp in milliseconds argument is rendered also
* as attribute <tt>millis</tt>.
*
* @param writer the writer to use for marshaling.
* @param nodeName the name of the node.
* @param millis the timestamp to marshal. If the value is zero ior negative,
* nothing will be marshalled.
*/
@Deprecated
protected void writeDateTime(HierarchicalStreamWriter writer, String nodeName, long millis) {
if (millis > 0) {
writer.startNode(nodeName);
writer.addAttribute("millis", Long.toString(millis)); //$NON-NLS-1$
writer.setValue(FormatUtils.formatUTC(millis));
writer.endNode();
}
}
/**
* Renders the API version provided by this converter as <tt>apiVersion</tt> attribute.
*
* @param writer the writer to use for rendering.
*/
protected void apiVersion() throws IOException {
apiVersion(this);
}
/**
* Renders the API version provided by the given converter as <tt>apiVersion</tt> attribute.
*
* @param writer the writer to use for rendering.
* @param converter the converter from which to retrieve API version information.
*/
protected void apiVersion(RestConverter<?> converter) throws IOException {
writer.attribute(API_VERSION, converter.getApiVersion());
}
/**
* Renders common attributes like API version, last modified and last modifier.
*
* @param writer the writer to use for rendering.
* @param entity the entity from which to retrieve the attributes to marshal.
*/
protected void commonAttributes(EntityBase entity) throws IOException {
commonAttributes(entity, this);
}
/**
* Renders common attributes like API version, last modified and last modifier.
*
* @param writer the writer to use for rendering.
* @param entity the entity from which to retrieve the attributes to marshal.
* @param converter the converter from which to retrieve API version information.
*/
protected void commonAttributes(EntityBase entity, RestConverter<?> converter) throws IOException {
apiVersion(converter);
long lastModifiedMillis = entity.getLastModifiedMillis();
if (lastModifiedMillis > 0) {
writer.attribute(LAST_MODIFIED_MILLIS, lastModifiedMillis);
}
String lastModified = entity.getLastModified();
if (StringUtils.isNotBlank(lastModified)) {
writer.attribute(LAST_MODIFIED, lastModified);
}
String modifiedBy = entity.getLastModifiedBy();
if (StringUtils.isNotBlank(modifiedBy)) {
writer.attribute(MODIFIED_BY, modifiedBy);
}
}
/**
* Marshals the namespace attributes provided by this converter to the
* underlying stream writer.
*
* @param writer the writer to use for marshaling.
*/
protected void namespaces() throws IOException {
namespaces(this);
}
/**
* Marshals the namespace attributes provided by the given converter to the
* underlying stream writer.
*
* @param converter the converter from which to retrieve namespace information.
* @param writer the writer to use for marshaling.
*/
protected void namespaces(RestConverter<?> converter) throws IOException {
if (writer.isMediaType(MediaType.TEXT_XML)) {
writer.namespace(XMLUtils.XMLNS, converter.getNamespace());
} else {
writer.attribute(NAMESPACE, converter.getNamespace());
}
writer.namespace(XMLUtils.XMLNS_XSI, XMLUtils.XSI_INSTANCE_NS);
if (StringUtils.isNotBlank(writer.getHost()) && StringUtils.isNotBlank(converter.getXsdFileName())) {
writer.namespace(XMLUtils.XSI_SCHEMA_LOCATION, getSchemaLocation(converter));
}
}
private String getSchemaLocation(RestConverter<?> converter) {
StringBuilder sb = new StringBuilder();
sb.append(converter.getNamespace());
sb.append(" "); //$NON-NLS-1$
sb.append(writer.getHost());
sb.append("/schemas/"); //$NON-NLS-1$
sb.append(converter.getXsdFileName());
return sb.toString();
}
}