package com.temenos.interaction.media.hal;
/*
* #%L
* interaction-media-hal
* %%
* 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 java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.apache.commons.lang.StringUtils;
import org.odata4j.core.OCollection;
import org.odata4j.core.OComplexObject;
import org.odata4j.core.OEntity;
import org.odata4j.core.OObject;
import org.odata4j.core.OProperty;
import org.odata4j.core.OSimpleObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.temenos.interaction.core.UriInfoImpl;
import com.temenos.interaction.core.command.InteractionContext;
import com.temenos.interaction.core.entity.Entity;
import com.temenos.interaction.core.entity.EntityMetadata;
import com.temenos.interaction.core.entity.EntityProperties;
import com.temenos.interaction.core.entity.EntityProperty;
import com.temenos.interaction.core.entity.Metadata;
import com.temenos.interaction.core.entity.vocabulary.terms.TermMandatory;
import com.temenos.interaction.core.hypermedia.DefaultResourceStateProvider;
import com.temenos.interaction.core.hypermedia.Link;
import com.temenos.interaction.core.hypermedia.MethodNotAllowedException;
import com.temenos.interaction.core.hypermedia.ResourceState;
import com.temenos.interaction.core.hypermedia.ResourceStateMachine;
import com.temenos.interaction.core.hypermedia.ResourceStateProvider;
import com.temenos.interaction.core.hypermedia.Transition;
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 com.theoryinpractise.halbuilder.api.ReadableRepresentation;
import com.theoryinpractise.halbuilder.api.Representation;
import com.theoryinpractise.halbuilder.api.RepresentationException;
import com.theoryinpractise.halbuilder.api.RepresentationFactory;
import com.theoryinpractise.halbuilder.json.JsonRepresentationReader;
import com.theoryinpractise.halbuilder.json.JsonRepresentationWriter;
import com.theoryinpractise.halbuilder.standard.StandardRepresentationFactory;
@Provider
@Consumes({HALMediaType.APPLICATION_HAL_XML, HALMediaType.APPLICATION_HAL_JSON, MediaType.APPLICATION_JSON})
@Produces({HALMediaType.APPLICATION_HAL_XML, HALMediaType.APPLICATION_HAL_JSON, MediaType.APPLICATION_JSON})
public class HALProvider implements MessageBodyReader<RESTResource>, MessageBodyWriter<RESTResource> {
private final Logger logger = LoggerFactory.getLogger(HALProvider.class);
@Context
private UriInfo uriInfo;
@Context
private Request requestContext;
private Metadata metadata = null;
private ResourceStateProvider resourceStateProvider;
private RepresentationFactory representationFactory;
public HALProvider(Metadata metadata, ResourceStateProvider resourceStateProvider) {
this(metadata);
this.resourceStateProvider = resourceStateProvider;
}
public HALProvider(Metadata metadata, ResourceStateProvider resourceStateProvider, RepresentationFactory representationFactory) {
this(metadata, representationFactory);
this.resourceStateProvider = resourceStateProvider;
}
@Deprecated
public HALProvider(Metadata metadata, ResourceStateMachine rsm) {
this(metadata);
this.resourceStateProvider = new DefaultResourceStateProvider(rsm);
}
public HALProvider(Metadata metadata) {
this(metadata, irisRepresentationFactory());
this.metadata = metadata;
assert(metadata != null);
}
public HALProvider(Metadata metadata, RepresentationFactory representationFactory) {
this.metadata = metadata;
this.representationFactory = representationFactory;
}
private static RepresentationFactory irisRepresentationFactory() {
return new StandardRepresentationFactory().
withReader(MediaType.APPLICATION_JSON, JsonRepresentationReader.class).
withRenderer(MediaType.APPLICATION_JSON, JsonRepresentationWriter.class);
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
if (mediaType.equals(HALMediaType.APPLICATION_HAL_XML_TYPE)
|| mediaType.equals(HALMediaType.APPLICATION_HAL_JSON_TYPE)
|| mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) {
return ResourceTypeHelper.isType(type, genericType, EntityResource.class)
|| ResourceTypeHelper.isType(type, genericType, CollectionResource.class);
}
return false;
}
@Override
public long getSize(RESTResource t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
private Representation buildHalResource(URI id, RESTResource resource, Class<?> type, Type genericType) throws URISyntaxException {
logger.debug("buildHalResource({})", id);
if (!ResourceTypeHelper.isType(type, genericType, EntityResource.class)
&& !ResourceTypeHelper.isType(type, genericType, CollectionResource.class))
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
// create the hal resource
Representation halResource = representationFactory.newRepresentation(id);
if (resource.getGenericEntity() != null) {
// get the links
Collection<Link> links = resource.getLinks();
Link selfLink = findSelfLink(links);
// build the HAL representation with self link
if (selfLink != null)
halResource = representationFactory.newRepresentation(selfLink.getHref());
// add our links
if (links != null) {
for (Link l : links) {
if (l.equals(selfLink))
continue;
logger.debug("Link: id=[" + l.getId() + "] rel=[" + l.getRel() +
"] method=[" + l.getMethod() + "] href=[" + l.getHref() + "]");
String[] rels = new String[0];
if (l.getRel() != null) {
rels = l.getRel().split(" ");
}
if (rels != null) {
for (int i = 0 ; i < rels.length; i++) {
halResource.withLink(rels[i], l.getHref(), l.getId(), l.getTitle(), null, null);
}
}
}
}
// add the embedded resources
Map<Transition, RESTResource> embedded = resource.getEmbedded();
if (embedded != null) {
for (Transition t : embedded.keySet()) {
RESTResource embeddedResource = embedded.get(t);
// TODO work our rel for embedded resource, just as we need to work out the rel for the other links
Link link = findLinkByTransition(links, t);
// Check link for null before using it
if(link!=null) {
String rel = (link.getRel() != null ? link.getRel() : "embedded/" + embeddedResource.getEntityName());
logger.debug("Embedded resource: rel=[" + rel + "] href=[" + link.getHref() + "]");
Representation embeddedRepresentation = buildHalResource(new URI(link.getHref()),
embeddedResource,
embeddedResource.getGenericEntity().getRawType(),
embeddedResource.getGenericEntity().getType());
halResource.withRepresentation(rel, embeddedRepresentation);
}
}
}
// add contents of supplied entity to the representation
buildRepresentation(halResource, resource, type, genericType);
} else {
logger.warn("Resource with URI {} has null genericEntity--no output produced", id);
}
return halResource;
}
/**
* Writes a Hypertext Application Language (HAL) 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 HAL XML 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 {
logger.debug("Writing " + mediaType);
Representation halResource;
try {
halResource = buildHalResource(uriInfo.getBaseUri(), resource, type, genericType);
}
catch(URISyntaxException e) {
logger.error("Invalid link syntax", e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String baseMediaType = HALMediaType.baseMediaType( mediaType );
String representation = halResource.toString(baseMediaType);
String charset = HALMediaType.charset( mediaType, "UTF-8" );
logger.debug("Produced [" + representation + "]");
entityStream.write(representation.getBytes(charset));
}
private Link findLinkByTransition(Collection<Link> links, Transition transition) {
Link link = null;
if (links != null) {
for (Link l : links) {
if (l.getTransition() != null && l.getTransition().equals(transition)) {
link = l;
break;
}
}
}
return link;
}
protected Link findSelfLink(Collection<Link> links) {
Link selfLink = null;
if (links != null) {
for (Link l : links) {
Transition t = l.getTransition();
// TODO this bit is a bit hacky.
// The latest version of the HAL spec should not require us to find a 'self' link for the subresource
if (l.getRel().contains("self") ||
(l.getTransition() != null
&& (t.getCommand().getMethod() == null || t.getCommand().getMethod().equals("GET"))
&& t.getTarget().getEntityName().equals(t.getSource().getEntityName()))) {
selfLink = l;
break;
}
}
}
return selfLink;
}
/** Build the metadata fully-qualified property name by joining the simple property
* name to the containing property name, if any.
* @param prefix the containing property name or empty string if this is a top-level property
* @param the simple name of the current property
* @return the fully-qualified property name as used in EntityMetadata
*/
private String lengthenPrefix(String prefix, String extra) {
if (prefix.isEmpty()) return extra;
else return prefix + "." + extra;
}
/** Turn an OPropertyName into the property name used in the metadata
* For complex properties, the property name is prefixed with the
* entity name, possibly so it can also be used as a unique type name.
* Note this is totally separate from the fully-qualified property names
* that are the keys to the entityMetadata.
* i.e. if simple property prop2 is inside complex property prop1, in
* entity ent, the OProperty name of prop1 is ent_prop1, the OProperty name
* of prop2 is prop2, that is accessed in the entity metadata as prop1.prop2
* (not ent_prop1.prop2)
* So, this takes an OProperty name and removes the entity prefix if appropriate.
*/
private String simpleOPropertyName(EntityMetadata entityMetadata, OProperty property) {
String rawName = property.getName();
if (!property.getType().isSimple()) {
String expectedPrefix = entityMetadata.getEntityName() + "_";
if (rawName.startsWith(expectedPrefix)) {
String simpleName = rawName.substring(expectedPrefix.length());
logger.debug(String.format("property lookup: %s -> %s", rawName, simpleName));
return simpleName;
} else {
// This is probably not expected. Logging as info, it might be better to throw if we
// are confident it shouldn't happen
logger.info(String.format("property %s does not start with %s", rawName, expectedPrefix));
}
}
return rawName;
}
/** transform OData4j object into String, Map or List
* Only properties defined in the entityMetadata vocabulary are included in transform output
*/
private Object buildFromOObject(EntityMetadata entityMetadata, String prefix, Object any)
{
if (any instanceof OObject) {
OObject object = (OObject)any;
if (object.getType().isSimple())
return ((OSimpleObject<Object>)object).getValue().toString();
else if (object instanceof OCollection) {
ArrayList builtList = new ArrayList<Object>();
OCollection<OObject> collection = (OCollection<OObject>)object;
for ( OObject each : collection ) {
builtList.add(buildFromOObject(entityMetadata, prefix, each));
}
return builtList;
} else {
OComplexObject complex = (OComplexObject)object;
HashMap<String,Object> map = new HashMap<String,Object>();
for (OProperty property : complex.getProperties()) {
String simpleName = simpleOPropertyName(entityMetadata, property);
String qualifiedName = lengthenPrefix(prefix, simpleName);
if (entityMetadata.getPropertyVocabulary(qualifiedName) != null) {
String mandatory = entityMetadata.getTermValue(qualifiedName, TermMandatory.TERM_NAME);
if(property.getValue() != null || (null != mandatory && "true".equals(mandatory))) {
map.put(simpleName, buildFromOObject(entityMetadata, qualifiedName, property.getValue()));
}
} else {
logger.debug(String.format("not adding property %s [%s], value %s",
property.getName(), qualifiedName, property.getValue()));
}
}
return map;
}
} else
return any;
}
/** populate a Map with the properties of an OEntity
*/
protected void buildFromOEntity(Map<String, Object> map, OEntity entity, String entityName) {
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new IllegalStateException("Entity metadata could not be found [" + entityName + "]");
for (OProperty<?> property : entity.getProperties()) {
// add properties if they are present on the resolved entity
String simpleName = simpleOPropertyName(entityMetadata, property);
if (entityMetadata.getPropertyVocabulary(simpleName) != null) {
String mandatory = entityMetadata.getTermValue(simpleName, TermMandatory.TERM_NAME);
if(property.getValue() != null || (null != mandatory && "true".equals(mandatory))) {
map.put(simpleName, buildFromOObject(entityMetadata, simpleName, property.getValue()));
}
}
else {
logger.debug(String.format("not adding property %s, value %s",
property.getName(), property.getValue()));
}
}
}
/** populate a Map from an Entity
*/
protected void buildFromEntity(Map<String, Object> map, Entity entity, String entityName) {
logger.debug("Serialising entity " + entityName);
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new IllegalStateException("Entity metadata could not be found [" + entityName + "]");
buildFromEntityProperties(entityMetadata, "", map, entity.getProperties());
}
protected void buildFromEntityProperties(EntityMetadata entityMetadata, String prefix, Map<String, Object> map, EntityProperties entityProperties) {
Map<String, EntityProperty> properties = entityProperties.getProperties();
for (Map.Entry<String, EntityProperty> property : properties.entrySet()) {
String propertyName = property.getKey();
logger.debug("property key " + propertyName + " name " + property.getValue().getName());
String qualifiedName = lengthenPrefix(prefix, propertyName);
if (entityMetadata.getPropertyVocabulary(qualifiedName) != null )
map.put( propertyName, entityPropertyValueToPOJO(entityMetadata, qualifiedName, property.getValue().getValue()));
}
}
protected Object entityPropertyValueToPOJO(EntityMetadata entityMetadata, String prefix, Object propertyValue) {
logger.debug("property value: " + propertyValue);
if ( propertyValue == null ) return "";
logger.debug("property value has type " + propertyValue.getClass());
if ( propertyValue instanceof EntityProperties ) {
Map<String,Object> newMap = new HashMap<String,Object>();
buildFromEntityProperties(entityMetadata, prefix, newMap, (EntityProperties)propertyValue);
return newMap;
} else if ( propertyValue instanceof Collection ) {
List newList = new ArrayList<EntityProperties>();
for (Object element : (Collection<?>) propertyValue ) {
newList.add(entityPropertyValueToPOJO(entityMetadata, prefix, element));
}
return newList;
} else if ( propertyValue instanceof EntityProperty ) {
return ((EntityProperty)propertyValue).getValue();
} else {
return propertyValue;
}
}
/** populate a Map from a java bean
* TODO implement nested structures and collections
*/
protected void buildFromBean(Map<String, Object> map, Object bean, String entityName) {
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new IllegalStateException("Entity metadata could not be found [" + entityName + "]");
try {
BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
for (PropertyDescriptor propertyDesc : beanInfo.getPropertyDescriptors()) {
String propertyName = propertyDesc.getName();
if (entityMetadata.getPropertyVocabulary(propertyName) != null) {
Object value = propertyDesc.getReadMethod().invoke(bean);
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);
}
}
// Populate a Representation with the links and properties
void collectLinksAndProperties(Representation resource, Iterable<Link> links,
Map<String, Object> propertyMap) {
if (links != null) {
for (Link l : links) {
if(l != null) {
logger.debug("Link: id=[" + l.getId() + "] rel=[" + l.getRel() +
"] method=[" + l.getMethod() + "] href=[" + l.getHref() + "]");
String[] rels = new String[0];
if (l.getRel() != null) {
rels = l.getRel().split(" ");
}
if (rels != null) {
for (int i = 0 ; i < rels.length; i++) {
resource.withLink(rels[i], l.getHref(), l.getId(), l.getTitle(), null, null);
}
}
}
}
}
// add properties to HAL sub resource
for (String key : propertyMap.keySet()) {
resource.withProperty(key, propertyMap.get(key));
}
}
private Representation buildRepresentation(Representation halResource,
RESTResource resource,
Class<?> type,
Type genericType) {
if (genericType == null)
genericType = resource.getGenericEntity().getType();
if (type == null)
type = resource.getGenericEntity().getRawType();
if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, OEntity.class)) {
@SuppressWarnings("unchecked")
EntityResource<OEntity> oentityResource = (EntityResource<OEntity>) resource;
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromOEntity(propertyMap, oentityResource.getEntity(), oentityResource.getEntityName());
// add properties to HAL resource
for (String key : propertyMap.keySet()) {
logger.debug(String.format("add property to representation: %s %s = %s",
propertyMap.get(key).getClass(), key,
propertyMap.get(key)));
halResource.withProperty(key, propertyMap.get(key));
}
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class, Entity.class)) {
logger.debug("transforming EntityResource<Entity>");
EntityResource<Entity> entityResource = (EntityResource<Entity>) resource;
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromEntity(propertyMap, entityResource.getEntity(), entityResource.getEntityName());
// add properties to HAL resource
for (String key : propertyMap.keySet()) {
halResource.withProperty(key, propertyMap.get(key));
}
} else if (ResourceTypeHelper.isType(type, genericType, EntityResource.class)) {
EntityResource<?> entityResource = (EntityResource<?>) resource;
Object entity = entityResource.getEntity();
if (entity != null) {
/*
* // regular java bean
* halResource.withBean(entity);
*/
// java bean, now limited to just the properties specified in the metadata entity model
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromBean(propertyMap, entity, entityResource.getEntityName());
for (String key : propertyMap.keySet()) {
halResource.withProperty(key, propertyMap.get(key));
}
}
} else if(ResourceTypeHelper.isType(type, genericType, CollectionResource.class, OEntity.class)) {
@SuppressWarnings("unchecked")
CollectionResource<OEntity> cr = (CollectionResource<OEntity>) resource;
List<EntityResource<OEntity>> entities = (List<EntityResource<OEntity>>) cr.getEntities();
Integer inlineCount = cr.getInlineCount();
if (inlineCount != null) {
halResource.withProperty("count", inlineCount.toString());
}
for (EntityResource<OEntity> er : entities) {
OEntity entity = er.getEntity();
// the subresource is an item of the collection (http://tools.ietf.org/html/rfc6573)
String rel = "item";
// the properties
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromOEntity(propertyMap, entity, cr.getEntityName());
// create hal resource and add link for self - if there is one
Representation subResource = representationFactory.newRepresentation();
collectLinksAndProperties(subResource, er.getLinks(), propertyMap);
halResource.withRepresentation(rel, subResource);
}
} else if(ResourceTypeHelper.isType(type, genericType, CollectionResource.class, Entity.class)) {
logger.debug("Transforming CollectionResource<Entity>");
@SuppressWarnings("unchecked")
CollectionResource<Entity> cr = (CollectionResource<Entity>) resource;
List<EntityResource<Entity>> entities = (List<EntityResource<Entity>>) cr.getEntities();
for (EntityResource<Entity> er : entities) {
// Make property Map
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromEntity(propertyMap, er.getEntity(), cr.getEntityName());
// Make Representation
Representation subResource = representationFactory.newRepresentation();
collectLinksAndProperties(subResource, er.getLinks(), propertyMap);
halResource.withRepresentation("item", subResource);
}
} else if (ResourceTypeHelper.isType(type, genericType, CollectionResource.class)) {
@SuppressWarnings("unchecked")
CollectionResource<Object> cr = (CollectionResource<Object>) resource;
List<EntityResource<Object>> entities = (List<EntityResource<Object>>) cr.getEntities();
for (EntityResource<Object> er : entities) {
Object entity = er.getEntity();
// the properties
Map<String, Object> propertyMap = new HashMap<String, Object>();
buildFromBean(propertyMap, entity, cr.getEntityName());
// Make Representation
Representation subResource = representationFactory.newRepresentation();
collectLinksAndProperties(subResource, er.getLinks(), propertyMap);
halResource.withRepresentation("item", subResource);
}
} else {
logger.error("Accepted object for writing in isWriteable, but type not supported in writeTo method");
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
return halResource;
}
@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 {
/* To detect if the stream is empty (a valid case since an input entity is
* sometimes optional), wrap in a PushbackInputStream before passing on
*/
PushbackInputStream wrappedStream = new PushbackInputStream(entityStream);
int firstByte = wrappedStream.read();
uriInfo = new UriInfoImpl(uriInfo);
if ( firstByte == -1 ) {
// No data provided
return null;
} else {
// There is something in the body, so we will parse it. It is required
// to be a valid JSON object. First replace the byte we borrowed.
wrappedStream.unread(firstByte);
//Parse hal+json into an Entity object
Entity entity;
try {
entity = buildEntityFromHal(wrappedStream, mediaType);
} catch (MethodNotAllowedException e) {
if (logger.isDebugEnabled()) {
logger.debug("Error building the entity.", e);
}
StringBuilder allowHeader = new StringBuilder();
Set<String> allowedMethods = new HashSet<String>(e.getAllowedMethods());
allowedMethods.add("HEAD");
allowedMethods.add("OPTIONS");
for(String method: allowedMethods) {
allowHeader.append(method);
allowHeader.append(", ");
}
Response response = Response.status(405).header("Allow", allowHeader.toString().substring(0, allowHeader.length() - 2)).build();
throw new WebApplicationException(response);
}
return new EntityResource<Entity>(entity);
}
}
private Entity buildEntityFromHal(InputStream entityStream, MediaType mediaType) throws MethodNotAllowedException {
try {
// create the hal resource
String baseUri = uriInfo.getBaseUri().toString();
ReadableRepresentation halResource = representationFactory.readRepresentation(mediaType.toString(), new InputStreamReader(entityStream));
String resourcePath = uriInfo.getPath();
logger.info("Reading HAL content for [" + resourcePath + "]");
if (resourcePath == null)
throw new IllegalStateException("No resource found");
// trim the baseuri
if (resourcePath.length() > baseUri.length() && resourcePath.startsWith(baseUri))
resourcePath = resourcePath.substring(baseUri.length() - 1);
/*
* add a leading '/' if it needs it (when defining resources we must use a
* full path, but requests can be relative, i.e. without a '/'
*/
if (!resourcePath.startsWith("/")) {
resourcePath = "/" + resourcePath;
}
// get the entity name
String entityName = getEntityName(resourcePath);
if(entityName == null) {
throw new IllegalStateException("Entity name could not be found [" + resourcePath + "]");
}
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new IllegalStateException("Entity metadata could not be found [" + entityName + "]");
// add properties if they are present on the resolved entity
EntityProperties entityFields = new EntityProperties();
Map<String, Object> halProperties = halResource.getProperties();
iterateProperties(entityMetadata, entityFields, halProperties, "");
return new Entity(entityName, entityFields);
} catch (RepresentationException e) {
logger.warn("Malformed request from client", e);
throw new WebApplicationException(Status.BAD_REQUEST);
} catch (IllegalStateException e) {
logger.warn("Malformed request from client", e);
throw new WebApplicationException(Status.BAD_REQUEST);
}
}
/*
* Iterate through property keys and extract values if the vocabulary is correct.
*/
private void iterateProperties(EntityMetadata entityMetadata, EntityProperties entityFields,
Map<String, Object> halProperties, String prefix) {
for (String propName : halProperties.keySet()) {
if (entityMetadata.getPropertyVocabulary(concatenatePrefixes(prefix,propName)) != null) {
Object propertyValue = halProperties.get(propName);
if (propertyValue != null) {
Object halValue = getHalPropertyValue(entityMetadata, propName, halProperties.get(propName),prefix);
entityFields.setProperty(new EntityProperty(propName, halValue));
}
}
}
}
protected ResourceState getCurrentState(String baseUri, String resourcePath) throws MethodNotAllowedException {
ResourceState state = null;
if (resourcePath != null) {
String tmpResourcePath = resourcePath;
if(resourcePath.charAt(0) != '/') {
tmpResourcePath = '/' + tmpResourcePath;
}
state = resourceStateProvider.getResourceState(requestContext.getMethod(), tmpResourcePath);
}
return state;
}
private String getEntityName(String resourcePath) throws MethodNotAllowedException {
String entityName = null;
if (resourcePath != null) {
String absoluteUri = uriInfo.getBaseUri() + uriInfo.getPath();
String tmpResourcePath = absoluteUri.substring(uriInfo.getBaseUri().toString().length());
ResourceState state = getCurrentState(uriInfo.getBaseUri().toString(), tmpResourcePath);
if (state != null) {
entityName = state.getEntityName();
} else {
logger.warn("No state found, dropping back to path matching");
Map<String, Set<String>> pathToResourceStates = resourceStateProvider.getResourceStatesByPath();
for (String path : pathToResourceStates.keySet()) {
for (String stateName : pathToResourceStates.get(path)) {
ResourceState s = resourceStateProvider.getResourceState(stateName);
String pathIdParameter = InteractionContext.DEFAULT_ID_PATH_ELEMENT;
if (s.getPathIdParameter() != null) {
pathIdParameter = s.getPathIdParameter();
}
Matcher matcher = Pattern.compile("(.*)\\{" + pathIdParameter + "\\}(.*)").matcher(path);
if (matcher.find()) {
int groupCount = matcher.groupCount();
if ((groupCount == 1 && resourcePath.startsWith(matcher.group(1))) ||
(groupCount == 2 && resourcePath.startsWith(matcher.group(1)) && resourcePath.endsWith(matcher.group(2)))) {
entityName = s.getEntityName();
}
}
if (entityName == null && path.startsWith(resourcePath)) {
entityName = s.getEntityName();
}
}
}
}
}
return entityName;
}
/* Ugly testing support :-( */
protected void setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
protected void setRequestContext(Request request) {
this.requestContext = request;
}
/*
* If a property is given with a null value, return it in a usable form for JSON
*/
private Object nullHalPropertyValue( EntityMetadata entityMetadata, String propertyName ) {
if ( entityMetadata.isPropertyText( propertyName ) )
return "";
else if ( entityMetadata.isPropertyNumber( propertyName ) )
return 0L;
return "";
}
/*
* Parse property values from input data.
*/
private Object getHalPropertyValue( EntityMetadata entityMetadata, String propertyName, Object halPropertyValue, String currentPrefix )
{
if ( halPropertyValue == null )
return nullHalPropertyValue( entityMetadata, propertyName );
if(halPropertyValue instanceof Collection){
return getValuesFromJsonArray(entityMetadata, propertyName, halPropertyValue, currentPrefix);
} else if(halPropertyValue instanceof Map){
return getValuesFromJsonObject(entityMetadata, propertyName, (Map<String, Object>)halPropertyValue, currentPrefix);
}
String stringValue = halPropertyValue.toString();
Object typedValue;
if ( entityMetadata.isPropertyText( propertyName ) )
{
typedValue = stringValue;
}
else if ( entityMetadata.isPropertyNumber( propertyName ) )
{
typedValue = Long.parseLong( stringValue );
}
else
{
typedValue = stringValue;
}
return typedValue;
}
/*
* Retrieve values from a JSON array.
*/
private List<EntityProperties> getValuesFromJsonArray(EntityMetadata entityMetadata, String propertyName,
Object halPropertyValue, String currentPrefix) {
Collection halPropertyValueCollection = (Collection)halPropertyValue;
ArrayList<EntityProperties> embeddedArray = new ArrayList<EntityProperties>();
for(Object o : halPropertyValueCollection){
if(o instanceof Map){
EntityProperties properties = new EntityProperties();
Map<String, Object> halPropertiesMap = (Map<String,Object>) o;
this.iterateProperties(entityMetadata, properties, halPropertiesMap, this.concatenatePrefixes(currentPrefix, propertyName));
embeddedArray.add(properties);
}
}
return embeddedArray;
}
private EntityProperties getValuesFromJsonObject(EntityMetadata entityMetadata, String propertyName,
Map<String, Object> halPropertyValue, String currentPrefix){
EntityProperties properties = new EntityProperties();
this.iterateProperties(entityMetadata, properties, halPropertyValue, this.concatenatePrefixes(currentPrefix, propertyName));
return properties;
}
/*
* Concatenate an object prefix with a nested object name using dot notation
* if a prefix already exists, else return the object name.
*/
private String concatenatePrefixes(String current, String newPrefixAddition){
if(StringUtils.isNotBlank(current)){
StringBuilder sb = new StringBuilder();
sb.append(current).append(".").append(newPrefixAddition);
return sb.toString();
}
else {
return newPrefixAddition;
}
}
}