HalResource.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* 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.
* #L%
*/
package io.wcm.caravan.hal.resource;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.osgi.annotation.versioning.ProviderType;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableListMultimap.Builder;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
/**
* Bean representation of a HAL resource.
*/
@ProviderType
public final class HalResource implements HalObject {
/**
* The mime content type
*/
public static final String CONTENT_TYPE = "application/hal+json";
/**
* JSON object mapper
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
private final ObjectNode model;
/**
* Create an empty HAL resource, with no object state or links
*/
public HalResource() {
this(JsonNodeFactory.instance.objectNode());
}
/**
* Create a HAL resource with empty state that only contains a self link with the given URI
* @param uri the URI under which this resource can be retrieved
*/
public HalResource(String uri) {
this();
setLink(new Link(uri));
}
/**
* Create a new HalResource with the state from the given JSON object
* @param model JSON model - must be an ObjectNode
* @throws IllegalArgumentException if model is not an object node
*/
public HalResource(JsonNode model) {
Preconditions.checkArgument(model instanceof ObjectNode, "Model is not an ObjectNode");
this.model = (ObjectNode)model;
}
/**
* Create a new HalResource with the state from the given JSON object
* @param model JSON model - must be an ObjectNode
* @throws IllegalArgumentException if model is not an object node
*/
public HalResource(ObjectNode model) {
this.model = model;
}
/**
* Create a new HalResource with the state from the given POJO
* @param pojo a simple java object that will be mapped by a standard jackson {@link ObjectMapper}
* @throws IllegalArgumentException if the object can not be converted to a Jackson JSON object
*/
public HalResource(Object pojo) {
this.model = OBJECT_MAPPER.convertValue(pojo, ObjectNode.class);
}
/**
* Create a new HalResource with the state from the given JSON object
* @param model JSON model - must be an ObjectNode
* @throws IllegalArgumentException if model is not an object node
*/
public HalResource(JsonNode model, String uri) {
this(model);
if (uri != null) {
setLink(new Link(uri));
}
}
/**
* Create a new HalResource with the state from the given POJO
* @param pojo a simple java object that will be mapped by a standard jackson {@link ObjectMapper}
* @param uri the URI under which this resource can be retrieved
* @throws IllegalArgumentException if the object can not be converted to a Jackson JSON object
*/
public HalResource(Object pojo, String uri) {
this.model = OBJECT_MAPPER.convertValue(pojo, ObjectNode.class);
if (uri != null) {
setLink(new Link(uri));
}
}
@Override
public ObjectNode getModel() {
return model;
}
/**
* @param <T> return type
* @param type a class that matches the structure of this resource's model
* @return a new instance of the given class, populated with the properties of this resource's model
*/
public <T> T adaptTo(Class<T> type) {
return OBJECT_MAPPER.convertValue(model, type);
}
/**
* @param relation Link relation
* @return True if has link for the given relation
*/
public boolean hasLink(String relation) {
return hasResource(HalResourceType.LINKS, relation);
}
/**
* @param relation Embedded resource relation
* @return True if has embedded resource for the given relation
*/
public boolean hasEmbedded(String relation) {
return hasResource(HalResourceType.EMBEDDED, relation);
}
private boolean hasResource(HalResourceType type, String relation) {
return !model.at("/" + type + "/" + relation).isMissingNode();
}
/**
* @return Self link for the resource. Can be null
*/
public Link getLink() {
return getLink("self");
}
/**
* @return All links
*/
public ListMultimap<String, Link> getLinks() {
return getResources(Link.class, HalResourceType.LINKS);
}
/**
* @return All embedded resources
*/
public ListMultimap<String, HalResource> getEmbedded() {
return getResources(HalResource.class, HalResourceType.EMBEDDED);
}
private <X extends HalObject> ListMultimap<String, X> getResources(Class<X> clazz, HalResourceType type) {
if (!model.has(type.toString())) {
return ImmutableListMultimap.of();
}
Builder<String, X> resources = ImmutableListMultimap.builder();
model.get(type.toString())
.fieldNames()
.forEachRemaining(field -> resources.putAll(field, getResources(clazz, type, field)));
return resources.build();
}
/**
* @param relation Link relation
* @return Link for the given relation
*/
public Link getLink(String relation) {
return hasLink(relation) ? getLinks(relation).get(0) : null;
}
/**
* @param relation Link relation
* @return All links for the given relation
*/
public List<Link> getLinks(String relation) {
return getResources(Link.class, HalResourceType.LINKS, relation);
}
/**
* recursively collects links within this resource and all embedded resources
* @param rel the relation your interested in
* @return a list of all links
*/
public List<Link> collectLinks(String rel) {
return collectResources(Link.class, HalResourceType.LINKS, rel);
}
private <X extends HalObject> List<X> collectResources(Class<X> clazz, HalResourceType type, String relation) {
ImmutableList.Builder<X> builder = ImmutableList.<X>builder().addAll(getResources(clazz, type, relation));
getEmbedded().values().stream()
.map(embedded -> embedded.collectResources(clazz, type, relation))
.forEach(embeddedResources -> builder.addAll(embeddedResources));
return builder.build();
}
/**
* @param relation Embedded resource relation
* @return Embedded resources for the given relation
*/
public HalResource getEmbeddedResource(String relation) {
return hasEmbedded(relation) ? getEmbedded(relation).get(0) : null;
}
/**
* @param relation Embedded resource relation
* @return All embedded resources for the given relation
*/
public List<HalResource> getEmbedded(String relation) {
return getResources(HalResource.class, HalResourceType.EMBEDDED, relation);
}
/**
* recursively collects embedded resources of a specific rel
* @param rel the relation your interested in
* @return a list of all embedded resources
*/
public List<HalResource> collectEmbedded(String rel) {
return collectResources(HalResource.class, HalResourceType.EMBEDDED, rel);
}
private <X extends HalObject> List<X> getResources(Class<X> clazz, HalResourceType type, String relation) {
if (!hasResource(type, relation)) {
return ImmutableList.of();
}
JsonNode resources = model.at("/" + type + "/" + relation);
List<X> halObjects;
try {
Constructor<X> constructor = clazz.getConstructor(JsonNode.class);
if (resources instanceof ObjectNode) {
halObjects = ImmutableList.of(constructor.newInstance(resources));
}
else {
ImmutableList.Builder<X> result = ImmutableList.builder();
for (JsonNode resource : resources) {
if (resource instanceof ObjectNode) {
result.add(constructor.newInstance(resource));
}
}
halObjects = result.build();
}
updateContextResource(halObjects);
return halObjects;
}
catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
/**
* Sets link for the {@code self} relation. Overwrites existing one.
* @param link Link to set
* @return HAL resource
*/
public HalResource setLink(Link link) {
return setLink("self", link);
}
/**
* Sets link for the given relation. Overwrites existing one. If {@code link} is {@code null} it gets ignored.
* @param relation Link relation
* @param link Link to add
* @return HAL resource
*/
public HalResource setLink(String relation, Link link) {
if (link == null) {
return this;
}
return addResources(HalResourceType.LINKS, relation, false, new Link[] {
link
});
}
/**
* Adds links for the given relation
* @param relation Link relation
* @param links Links to add
* @return HAL resource
*/
public HalResource addLinks(String relation, Link... links) {
return addResources(HalResourceType.LINKS, relation, true, links);
}
/**
* Adds links for the given relation
* @param relation Link relation
* @param links Links to add
* @return HAL resource
*/
public HalResource addLinks(String relation, Iterable<Link> links) {
return addLinks(relation, Iterables.toArray(links, Link.class));
}
/**
* Embed resource for the given relation. Overwrites existing one.
* @param relation Embedded resource relation
* @param resource Resource to embed
* @return HAL resource
*/
public HalResource setEmbedded(String relation, HalResource resource) {
if (resource == null) {
return this;
}
return addResources(HalResourceType.EMBEDDED, relation, false, new HalResource[] {
resource
});
}
/**
* Embed resources for the given relation
* @param relation Embedded resource relation
* @param resources Resources to embed
* @return HAL resource
*/
public HalResource addEmbedded(String relation, HalResource... resources) {
return addResources(HalResourceType.EMBEDDED, relation, true, resources);
}
/**
* Embed resources for the given relation
* @param relation Embedded resource relation
* @param resources Resources to embed
* @return HAL resource
*/
public HalResource addEmbedded(String relation, Iterable<HalResource> resources) {
return addEmbedded(relation, Iterables.toArray(resources, HalResource.class));
}
private <X extends HalObject> HalResource addResources(HalResourceType type, String relation, boolean asArray, X[] newResources) {
if (newResources.length == 0) {
return this;
}
ObjectNode resources = model.has(type.toString()) ? (ObjectNode)model.get(type.toString()) : model.putObject(type.toString());
if (asArray) {
ArrayNode container = getArrayNodeContainer(type, relation, resources);
Arrays.stream(newResources).forEach(link -> container.add(link.getModel()));
}
else {
resources.set(relation, newResources[0].getModel());
}
updateContextResource(Arrays.asList(newResources));
return this;
}
private <X extends HalObject> void updateContextResource(Iterable<X> halObjects) {
for (X halObject : halObjects) {
if (halObject instanceof Link) {
((Link)halObject).setContext(this);
}
}
}
private ArrayNode getArrayNodeContainer(HalResourceType type, String relation, ObjectNode resources) {
if (hasResource(type, relation)) {
if (resources.get(relation).isArray()) {
return (ArrayNode)resources.get(relation);
}
else {
JsonNode temp = resources.get(relation);
return resources.putArray(relation).add(temp);
}
}
else {
return resources.putArray(relation);
}
}
/**
* Removes all links for the given relation.
* @param relation Link relation
* @return HAL resource
*/
public HalResource removeLinks(String relation) {
return removeResource(HalResourceType.LINKS, relation);
}
/**
* Removes all embedded resources for the given relation.
* @param relation Embedded resource relation
* @return HAL resource
*/
public HalResource removeEmbedded(String relation) {
return removeResource(HalResourceType.EMBEDDED, relation);
}
private HalResource removeResource(HalResourceType type, String relation) {
if (hasResource(type, relation)) {
((ObjectNode)model.get(type.toString())).remove(relation);
}
return this;
}
/**
* Removes one link for the given relation and index.
* @param relation Link relation
* @param index Array index
* @return HAL resource
*/
public HalResource removeLink(String relation, int index) {
return removeResource(HalResourceType.LINKS, relation, index);
}
/**
* Remove the link with the given relation and href
* @param relation Link relation
* @param href to identify the link to remove
* @return this HAL resource
*/
public HalResource removeLinkWithHref(String relation, String href) {
List<Link> links = getLinks(relation);
for (int i = 0; i < links.size(); i++) {
if (href.equals(links.get(i).getHref())) {
return removeLink(relation, i);
}
}
return this;
}
/**
* Removes one embedded resource for the given relation and index.
* @param relation Embedded resource relation
* @param index Array index
* @return HAL resource
*/
public HalResource removeEmbedded(String relation, int index) {
return removeResource(HalResourceType.EMBEDDED, relation, index);
}
private HalResource removeResource(HalResourceType type, String relation, int index) {
if (hasResource(type, relation)) {
JsonNode resources = model.at("/" + type + "/" + relation);
if (resources instanceof ObjectNode || resources.size() <= 1) {
((ObjectNode)model.get(type.toString())).remove(relation);
}
else {
((ArrayNode)resources).remove(index);
}
}
return this;
}
/**
* Removes all links.
* @return HAL resource
*/
public HalResource removeLinks() {
return removeResources(HalResourceType.LINKS);
}
/**
* Removes all embedded resources.
* @return HAL resource
*/
public HalResource removeEmbedded() {
return removeResources(HalResourceType.EMBEDDED);
}
/**
* Changes the rel of embedded resources
* @param relToRename the rel that you want to change
* @param newRel the new rel for all embedded items
* @return HAL resource
*/
public HalResource renameEmbedded(String relToRename, String newRel) {
List<HalResource> resources = getEmbedded(relToRename);
return removeEmbedded(relToRename).addEmbedded(newRel, resources);
}
private HalResource removeResources(HalResourceType type) {
model.remove(type.toString());
return this;
}
/**
* Adds state to the resource.
* @param state Resource state
* @return HAL resource
*/
public HalResource addState(ObjectNode state) {
state.fields().forEachRemaining(entry -> model.set(entry.getKey(), entry.getValue()));
return this;
}
/**
* @return JSON field names for the state object
*/
public List<String> getStateFieldNames() {
Iterable<String> iterable = () -> model.fieldNames();
return StreamSupport.stream(iterable.spliterator(), false)
.filter(field -> !"_links".equals(field) && !"_embedded".equals(field))
.collect(Collectors.toList());
}
/**
* Removes all state attributes
* @return HAL resource
*/
public HalResource removeState() {
getStateFieldNames().forEach(field -> model.remove(field));
return this;
}
}