package com.temenos.interaction.media.xhtml;
/*
* #%L
* interaction-media-xhtml
* %%
* Copyright (C) 2012 - 2013 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import com.temenos.interaction.core.entity.*;
import com.temenos.interaction.core.hypermedia.Link;
import com.temenos.interaction.core.resource.CollectionResource;
import com.temenos.interaction.core.resource.EntityResource;
import com.temenos.interaction.core.resource.RESTResource;
import com.temenos.interaction.core.resource.ResourceTypeHelper;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.VelocityException;
import org.odata4j.core.OEntity;
import org.odata4j.core.OProperty;
import org.odata4j.edm.EdmDataServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.*;
import static com.temenos.interaction.media.xhtml.VelocityTemplateEngine.VmTemplate;
@Provider
@Consumes({MediaType.APPLICATION_XHTML_XML})
@Produces({MediaType.APPLICATION_XHTML_XML, MediaType.TEXT_HTML})
public class XHTMLProvider implements MessageBodyReader<RESTResource>, MessageBodyWriter<RESTResource> {
private final Logger logger = LoggerFactory.getLogger(XHTMLProvider.class);
@Context
private UriInfo uriInfo;
private Metadata metadata = null;
private VelocityTemplateEngine templateEngine = new VelocityTemplateEngine();
public XHTMLProvider(Metadata metadata) {
this.metadata = metadata;
assert(metadata != null);
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return ResourceTypeHelper.isType(type, genericType, EntityResource.class) ||
ResourceTypeHelper.isType(type, genericType, CollectionResource.class);
}
@Override
public long getSize(RESTResource t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
/**
* Writes a XHTML representation of
* {@link EntityResource} to the output stream.
*
* @precondition supplied {@link EntityResource} is non null
* @precondition {@link EntityResource#getEntity()} returns a valid OEntity, this
* provider only supports serialising OEntities
* @postcondition non null XHTML document written to OutputStream
* @invariant valid OutputStream
*/
@Override
public void writeTo(RESTResource resource, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException,
WebApplicationException {
assert (resource != null);
logger.debug("Writing " + mediaType);
Writer writer = new BufferedWriter(new OutputStreamWriter(entityStream, "UTF-8"));
try {
renderResource(writer, resource, mediaType, type, genericType);
writer.flush();
}
catch (ParseErrorException pee) {
String msg = String.format("Failed to render XHTML response (Apache Velocity error).\n" +
"Message: %s\n" +
"TemplateName: %s\n" +
"LineNumber: %d\n" +
"InvalidSyntax: %s",
pee.getMessage(),
pee.getTemplateName(),
pee.getLineNumber(),
pee.getInvalidSyntax());
logger.error(msg, pee);
throw new WebApplicationException(createErrorResponse(mediaType, "Failed to render XHTML response"));
}
catch(VelocityException ve) {
String msg = "Failed to render XHTML response: ";
logger.error(msg, ve);
throw new WebApplicationException(createErrorResponse(mediaType, msg));
}
catch(Exception e) {
logger.error("Failed to render XHTML response: ", e);
throw new WebApplicationException(createErrorResponse(mediaType, e.getMessage()));
}
}
/*
* Render this resource to the output stram
* @param writer output stream
* @param resource resource to render
* @param mediaType media type
* @param type resource type
* @param genericType resource generic type
* @throws Exception
*/
@SuppressWarnings("unchecked")
private void renderResource(Writer writer, RESTResource resource, MediaType mediaType, Class<?> type, Type genericType) throws Exception {
Map<String, Object> properties = new HashMap<>();
// create the xhtml resource
if (resource.getGenericEntity() != null) {
RESTResource rResource = (RESTResource) resource.getGenericEntity().getEntity();
Collection<Link> links = rResource.getLinks();
if(links == null) {
links = new ArrayList<Link>();
}
//Render header
properties.put("siteName", (uriInfo != null && uriInfo.getPath() != null) ? uriInfo.getPath() : "");
properties.put("resourceLinks", links);
templateEngine.merge(writer, properties,
MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType) ? VmTemplate.HEADER_MINIMAL.get() : VmTemplate.HEADER.get());
//links are already in header - render for text/html
if (MediaType.TEXT_HTML_TYPE.equals(mediaType)) {
templateEngine.merge(writer, properties, VmTemplate.RESOURCE_LINKS.get());
}
//render data
if (ResourceTypeHelper.isType(type, genericType, EntityResource.class)) {
if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, OEntity.class)) {
//OEntity entity resource
EntityResource<OEntity> oentityResource = (EntityResource<OEntity>) resource;
EntityMetadata entityMetadata = metadata.getEntityMetadata(oentityResource.getEntityName());
properties.put("entityResource", new EntityResourceWrapperXHTML(entityMetadata, buildFromOEntity(oentityResource)));
templateEngine.merge(writer, properties,
MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType) ? VmTemplate.ENTITY_MINIMAL.get() : VmTemplate.ENTITY.get());
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, Entity.class)) {
//Entity entity resource
EntityResource<Entity> entityResource = (EntityResource<Entity>) resource;
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityResource.getEntityName() != null ? entityResource.getEntityName() : entityResource.getEntity().getName());
properties.put("entityResource", new EntityResourceWrapperXHTML(entityMetadata, buildFromEntity(entityResource)));
templateEngine.merge(writer, properties,
MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType) ? VmTemplate.ENTITY_MINIMAL.get() : VmTemplate.ENTITY.get());
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, EdmDataServices.class)) {
//The resources are shown in the resource links section
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, GenericError.class)) {
//Generic error error
EntityResource<GenericError> entityResource = (EntityResource<GenericError>) resource;
GenericError error = entityResource.getEntity();
writer.write(getErrorMessage(mediaType, "[" + error.getCode() + "] " + error.getMessage()));
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class)) {
//JAXB entity resource
EntityResource<Object> entityResource = (EntityResource<Object>) resource;
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityResource.getEntityName());
if(entityResource.getEntity() != null) {
properties.put("entityResource", new EntityResourceWrapperXHTML(entityMetadata, buildFromBean(entityResource)));
templateEngine.merge(writer, properties,
MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType) ? VmTemplate.ENTITY_MINIMAL.get() : VmTemplate.ENTITY.get());
}
} else {
logger.error("Accepted object for writing in isWriteable, but type not supported in writeTo method");
throw new Exception("Unable to render this resource as an XHTML response.");
}
}
else if (ResourceTypeHelper.isType(type, genericType, CollectionResource.class)) {
if (ResourceTypeHelper.isType(type, genericType, CollectionResource.class, Entity.class)) {
//Entity collection resource
CollectionResource<Entity> collectionResource = (CollectionResource<Entity>) resource;
EntityMetadata entityMetadata = metadata.getEntityMetadata(collectionResource.getEntityName());
Set<String> entityPropertyNames = entityMetadata.getTopLevelProperties();
List<EntityResource<Entity>> entityResources = (List<EntityResource<Entity>>) collectionResource.getEntities();
List<EntityResourceWrapperXHTML> entities = new ArrayList<EntityResourceWrapperXHTML>();
for (EntityResource<Entity> er : entityResources) {
entities.add(new EntityResourceWrapperXHTML(entityMetadata, entityPropertyNames, buildFromEntity(er)));
}
properties.put("entitySetName", collectionResource.getEntitySetName());
properties.put("entityPropertyNames", entityPropertyNames);
properties.put("entityResources", entities);
} else if(ResourceTypeHelper.isType(type, genericType, CollectionResource.class, OEntity.class)) {
//OEntity collection resource
CollectionResource<OEntity> collectionResource = ((CollectionResource<OEntity>) resource);
EntityMetadata entityMetadata = metadata.getEntityMetadata(collectionResource.getEntityName());
Set<String> entityPropertyNames = entityMetadata.getTopLevelProperties();
List<EntityResource<OEntity>> entityResources = (List<EntityResource<OEntity>>) collectionResource.getEntities();
List<EntityResourceWrapperXHTML> entities = new ArrayList<EntityResourceWrapperXHTML>();
for (EntityResource<OEntity> er : entityResources) {
entities.add(new EntityResourceWrapperXHTML(entityMetadata, entityPropertyNames, buildFromOEntity(er)));
}
properties.put("entitySetName", collectionResource.getEntitySetName());
properties.put("entityPropertyNames", entityPropertyNames);
properties.put("entityResources", entities);
} else if (ResourceTypeHelper.isType(type, genericType, CollectionResource.class)) {
//JAXB collection resource
CollectionResource<Object> collectionResource = (CollectionResource<Object>) resource;
List<EntityResource<Object>> entityResources = (List<EntityResource<Object>>) collectionResource.getEntities();
List<EntityResourceWrapperXHTML> entities = new ArrayList<EntityResourceWrapperXHTML>();
EntityMetadata entityMetadata = metadata.getEntityMetadata(collectionResource.getEntityName());
Set<String> entityPropertyNames = entityMetadata.getTopLevelProperties();
for (EntityResource<Object> er : entityResources) {
er.setEntityName(collectionResource.getEntityName());
entities.add(new EntityResourceWrapperXHTML(entityMetadata, entityPropertyNames, buildFromBean(er)));
}
properties.put("entitySetName", collectionResource.getEntitySetName());
properties.put("entityPropertyNames", entityPropertyNames);
properties.put("entityResources", entities);
} else {
logger.error("Accepted object for writing in isWriteable, but type not supported in writeTo method");
throw new Exception("Unable to render this resource as an XHTML response.");
}
templateEngine.merge(writer, properties,
MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType) ? VmTemplate.ENTITIES_MINIMAL.get() : VmTemplate.ENTITIES.get());
} else {
logger.error("Accepted object for writing in isWriteable, but type not supported in writeTo method");
throw new Exception("Unable to render this resource as an XHTML response.");
}
//Render footer
templateEngine.merge(writer, properties, VmTemplate.FOOTER.get());
} else {
logger.error("Unable to render empty resource.");
throw new Exception("Unable to render this empty resource as an XHTML response.");
}
}
protected Set<String> getEntityPropertyNames(String entityName, List<EntityResourceWrapperXHTML> entities) {
if(entities.size() > 0) {
return entities.get(0).getResource().getEntity().keySet();
}
else {
return metadata.getEntityMetadata(entityName).getTopLevelProperties();
}
}
protected EntityResource<Map<String, Object>> buildFromOEntity(EntityResource<OEntity> entityResource) {
OEntity entity = entityResource.getEntity();
Map<String, Object> map = new HashMap<String, Object>();
for (OProperty<?> property : entity.getProperties())
{
if(property != null) {
map.put(property.getName(), property.getValue());
}
}
EntityResource<Map<String, Object>> er = new EntityResource<Map<String, Object>>(map);
er.setLinks(entityResource.getLinks());
er.setEntityName(entity.getEntitySetName());
return er;
}
protected EntityResource<Map<String, Object>> buildFromEntity(EntityResource<Entity> entityResource) {
Entity entity = entityResource.getEntity();
Map<String, Object> map = new HashMap<String, Object>();
Map<String, EntityProperty> properties = entity.getProperties().getProperties();
for (String propertyName : properties.keySet())
{
EntityProperty propertyValue = (EntityProperty) properties.get(propertyName);
if(propertyValue != null) {
map.put(propertyName, propertyValue.getValue());
}
}
EntityResource<Map<String, Object>> er = new EntityResource<Map<String, Object>>(map);
er.setLinks(entityResource.getLinks());
er.setEntityName(entity.getName());
return er;
}
protected EntityResource<Map<String, Object>> buildFromBean(EntityResource<Object> entityResource) {
Map<String, Object> map = new HashMap<String, Object>();
String entityName = entityResource.getEntityName();
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new IllegalStateException("Entity metadata could not be found [" + entityName + "]");
try {
BeanInfo beanInfo = Introspector.getBeanInfo(entityResource.getEntity().getClass());
for (PropertyDescriptor propertyDesc : beanInfo.getPropertyDescriptors()) {
String propertyName = propertyDesc.getName();
if (entityMetadata.getPropertyVocabulary(propertyName) != null) {
Object value = propertyDesc.getReadMethod().invoke(entityResource.getEntity());
map.put(propertyName, value);
}
}
} catch (IllegalArgumentException e) {
logger.error("Error accessing bean property", e);
} catch (IntrospectionException e) {
logger.error("Error accessing bean property", e);
} catch (IllegalAccessException e) {
logger.error("Error accessing bean property", e);
} catch (InvocationTargetException e) {
logger.error("Error accessing bean property", e);
}
EntityResource<Map<String, Object>> er = new EntityResource<Map<String, Object>>(map);
er.setEntityName(entityName);
er.setLinks(entityResource.getLinks());
return er;
}
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
// this class can only deserialise EntityResource
return ResourceTypeHelper.isType(type, genericType, EntityResource.class);
}
/**
* Reads a Hypertext Application Language (HAL) representation of
* {@link EntityResource} from the input stream.
*
* @precondition {@link InputStream} contains a valid HAL <resource/> document
* @postcondition {@link EntityResource} will be constructed and returned.
* @invariant valid InputStream
*/
@Override
public RESTResource readFrom(Class<RESTResource> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
if (!ResourceTypeHelper.isType(type, genericType, EntityResource.class))
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
return null;
}
/* Ugly testing support :-( */
protected void setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
/*
* Render an error response
* @param mediaType media type
* @param errorMessage error message
* @return error response
*/
private Response createErrorResponse(MediaType mediaType, String errorMessage) {
ResponseBuilder responseBuilder = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
StringBuilder entity = new StringBuilder("");
if (MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType)) {
entity.append(VmTemplate.HEADER_MINIMAL.toString());
} else {
entity.append(VmTemplate.HEADER.toString());
}
entity.append(getErrorMessage(mediaType, errorMessage));
entity.append(VmTemplate.FOOTER.toString());
responseBuilder.entity(entity.toString());
responseBuilder.type(mediaType);
return responseBuilder.build();
}
/*
* Return an error message
* @param mediaType media type
* @param errorMessage error message
* @return error response
*/
private String getErrorMessage(MediaType mediaType, String errorMessage) {
if(MediaType.APPLICATION_XHTML_XML_TYPE.equals(mediaType)) {
return "\t\t<error>" + errorMessage + "</error>";
}
else {
return "\t\t<div class=\"error\">" + errorMessage + "</div>";
}
}
}