All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.hal4j.resources.ResourceSupport Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
package com.github.hal4j.resources;

import java.io.Serializable;
import java.net.URI;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.github.hal4j.resources.HALLink.*;
import static java.util.Collections.*;
import static java.util.Optional.*;
import static java.util.stream.Collectors.toList;

/**
 * Core implementation of all resources which defines all HAL contracts and data model.
 *
 */
public abstract class ResourceSupport implements Serializable {

    private final BindingContext context;

    private final URI self;

    private final Map> _links;

    private final Map> _embedded;

    ResourceSupport(ResourceSupport resource) {
        this(resource._links, resource._embedded, resource.context);
    }

    ResourceSupport(Map> _links,
                    Map> _embedded,
                    BindingContext context) {
        this._links = _links != null && !_links.isEmpty() ? clone(_links) : null;
        HALLink self = null;
        if (this._links != null) {
            List all = this._links.get(REL_SELF);
            if (all != null) {
                for (HALLink link : all) {
                    if (link.name == null) {
                        self = link;
                        break;
                    } else if (self == null) {
                        self = link;
                    }
                }
            }
        }
        this.self = self != null ? self.uri() : null;
        this._embedded = _embedded != null && ! _embedded.isEmpty() ? clone(_embedded) : null;
        this.context = context;
    }

    private static  Map> clone(Map> map) {
        Map> result = new HashMap<>();
        map.forEach((rel, list) -> result.put(rel, unmodifiableList(new ArrayList<>(list))));
        return Collections.unmodifiableMap(result);
    }

    /**
     * Returns binding context used to construct this resource object
     * @return the binding context or null if not used/set.
     */
    public BindingContext context() {
        return context;
    }

    /**
     * Checks if this resource equals given object. Two resources are considered equal if their self links are equal.
     * @param that object to check for equality
     * @return true if that object is a resource and it has the same self link.
     * @see HALLink#REL_SELF
     */
    @Override
    public boolean equals(Object that) {
        if (this == that) return true;
        if (!(that instanceof ResourceSupport)) return false;
        ResourceSupport thatResource = (ResourceSupport) that;
        URI uri;
        try {
            uri = self();
        } catch (MissingLinkException e) {
            return false;
        }
        return Objects.equals(uri, thatResource.self());
    }

    /**
     * Returns hash code of this resource defined as hash code of the self link
     * @return hash code of this object
     */
    @Override
    public int hashCode() {
        try {
            return Objects.hash(self());
        } catch (MissingLinkException e) {
            return 0;
        }
    }

    /**
     * Returns URI of the link with the rel self
     * @return the self link
     * @throws MissingLinkException if resource does not contain self link
     */
    public URI self() {
        if (this.self == null) {
            throw new MissingLinkException(REL_SELF);
        }
        return this.self;
    }

    /**
     * Returns collection of the links associated with this resource
     * @return non-null collection of the links associated with this resource
     */
    public Links links() {
        return new Links();
    }

    /**
     * Returns collection of the embedded objects included with this resource
     * @return non-null collection  of the embedded objects included with this resource
     */
    public EmbeddedObjects embedded() {
        return new EmbeddedObjects();
    }

    /**
     * Common query operations for links and embedded objects
     * @param  type of object (link or embedded)
     */
    public abstract class MetadataElements {

        private final Map> map;

        private MetadataElements(Map> map) {
            this.map = map;
        }

        /**
         * Returns underlying objects "as is", i.e. as a Map with relation keys and lists of objects.
         * May return null.
         * @return the underlying map or null
         */
        public Map> asIs() {
            return this.map;
        }

        /**
         * Return all items with given relation
         * @param rel name of relation
         * @return list of items or empty list
         */
        public List findAll(String rel) {
            if (rel == null) {
                throw new NullPointerException("Relation name cannot be null");
            }
            return ofNullable(map)
                    .map(m -> m.get(rel))
                    .orElse(emptyList());
        }

        /**
         * Return all items with given relation
         * @param rel name of relation as URI
         * @return list of items or empty list
         */
        public List findAll(URI rel) {
            return findAll(rel.toString());
        }

        /**
         * Return any of the items with given relation
         * @param rel name of relation as URI
         * @return any found item or empty Optional
         */
        public Optional find(URI rel) {
            return findAll(rel).stream().findAny();
        }

        /**
         * Return any of the items with given relation
         * @param rel name of relation
         * @return any found item or empty Optional
         */
        public Optional find(String rel) {
            return findAll(rel).stream().findAny();
        }

        /**
         * Checks if any item with given relation is present
         * @param rel name of relation as URI
         * @return true if such relation exists, false otherwise.
         */
        public boolean include(URI rel) {
            return ofNullable(map)
                    .map(m -> m.containsKey(rel.toString()))
                    .orElse(false);
        }

        /**
         * Checks if any item with given relation is present
         * @param rel name of relation
         * @return true if such relation exists, false otherwise.
         */
        public boolean include(String rel) {
            return this.include(URI.create(rel));
        }

        /**
         * Count number of items with given relation
         * @param rel name of relation
         * @return number of items or 0 if relation does not exist in this resource.
         */
        public int count(String rel) {
            return this.findAll(rel).size();
        }

        /**
         * Count number of items with given relation
         * @param rel name of relation as URI
         * @return number of items or 0 if relation does not exist in this resource.
         */
        public int count(URI rel) {
            return this.findAll(rel).size();
        }

        public Stream selectAll(String uri) {
            return this.findAll(uri).stream();
        }

        public Stream selectAll(URI uri) {
            return this.findAll(uri).stream();
        }

        /**
         * Returns underlying objects as a Map with relation keys and lists of objects.
         * If underlying map is null, returns empty map.
         * @return the underlying map or empty map
         */
        public Map> all() {
            return ofNullable(map).orElse(emptyMap());
        }
    }

    /**
     * Wrapper for the collection of links providing convenience methods for querying them
     */
    public class Links extends MetadataElements {

        Links() {
            super(_links);
        }

        /**
         * Checks if there's at least one link with given relation and name
         * @param rel name of relation
         * @param name name of the link (see {@link HALLink#name})
         * @return true if such link exists, false otherwise.
         */
        public boolean include(String rel, String name) {
            return findAll(rel).stream().anyMatch(link -> name.equals(link.name));
        }

        /**
         * Finds a link with the given name of relation and resolves it to the permanent URI of resource
         * @param rel name of relation
         * @return Optional with the link if such link exists, Optional.empty otherwise.
         */
        public Optional resolve(String rel) {
            return resolve(rel, link -> Objects.equals(null, link.name));
        }

        /**
         * Finds any matching link with given relation and name
         * @param rel name of relation
         * @param name name of the link (see {@link HALLink#name})
         * @return Optional with the link if such link exists, Optional.empty otherwise.
         */
        public Optional resolve(String rel, String name) {
            return resolve(rel, link -> Objects.equals(name, link.name));
        }

        /**
         * Finds any link with given relation that matches given condition
         * @param rel name of relation
         * @param condition the condition to match
         * @return Optional with the link if such link exists, Optional.empty otherwise.
         */
        public Optional resolve(String rel, Predicate condition) {
            List links = findAll(rel);
            if (links.isEmpty()) {
                return empty();
            }
            HALLink link = null;
            HALLink self = null;
            for (int i = 0; i < links.size(); i++) {
                HALLink l = links.get(i);
                if (condition.test(l)) {
                    if (!HREF_SAME_RESOURCE.equals(l.href)) {
                        return of(l);
                    }
                    link = l;
                }
                if (l.name == null && !HREF_SAME_RESOURCE.equals(l.href)) {
                    self = l;
                }
                if (link != null && self != null) {
                    break;
                }
            }
            if (link == null) return empty();
            if (self == null) {
                self = findAll(REL_SELF).stream().filter(value -> !SAME_RESOURCE.test(value)).findFirst()
                        .orElseThrow(() -> new IllegalStateException("Self link not found"));
            }
            return Optional.of(link.resolve(self));
        }

    }

    /**
     * Wrapper for the collection of embedded objects providing convenience methods for querying them
     */
    public class EmbeddedObjects extends MetadataElements {

        EmbeddedObjects() {
            super(_embedded);
        }

        /**
         * Find an embedded object with given relation
         * and return the search result as an object of given type, if any found.
         * @param rel relation of searched object
         * @param type the class object used as a metamodel for mapping
         * @param  the type of the expected result
         * @return search result as an Optional
         */
        public  Optional find(String rel, Class type) {
            return find(rel).map(item -> context().bind(item, type));
        }

        /**
         * Find an embedded object with given relation and return the search result
         * as a resource of given type if any found.
         * @param rel relation of searched object
         * @param type the class object used as a metamodel for mapping
         * @param  the type of the expected result
         * @return search result as an Optional
         */
        public  Optional> findResource(String rel, Class type) {
            return find(rel).map(item -> context().bind(item, GenericResource.class))
                    .map(resource -> resource.as(type));
        }

        /**
         * Find a collection of embedded objects with given relation and return
         * the search results as objects of given type, if any found.
         * @param rel relation of searched objects
         * @param type the class object used as a metamodel for mapping
         * @param  the type of the expected results
         * @return search result as a List of objects
         */
        public   List findAll(String rel, Class type) {
            return findAll(rel).stream()
                    .map(item -> context().bind(item, type))
                    .collect(toList());
        }

        /**
         * Find a collection of embedded objects with given relation and return
         * the search results as resources of given type
         * @param rel relation of searched objects
         * @param type the class object used as a metamodel for mapping
         * @param  the type of the expected results
         * @return search result as a List of objects
         */
        public   List> findResources(String rel, Class type) {
            return findAll(rel).stream()
                    .map(item -> context().bind(item, GenericResource.class))
                    .map(resource -> resource.as(type))
                    .collect(toList());
        }

    }

}