org.wrml.runtime.rest.Resource.java Source code

Java tutorial

Introduction

Here is the source code for org.wrml.runtime.rest.Resource.java

Source

/**
 * WRML - Web Resource Modeling Language
 *  __     __   ______   __    __   __
 * /\ \  _ \ \ /\  == \ /\ "-./  \ /\ \
 * \ \ \/ ".\ \\ \  __< \ \ \-./\ \\ \ \____
 *  \ \__/".~\_\\ \_\ \_\\ \_\ \ \_\\ \_____\
 *   \/_/   \/_/ \/_/ /_/ \/_/  \/_/ \/_____/
 *
 * http://www.wrml.org
 *
 * Copyright (C) 2011 - 2013 Mark Masse <mark@wrml.org> (OSS project WRML.org)
 *
 * 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.
 */
package org.wrml.runtime.rest;

import com.google.common.collect.ComparisonChain;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wrml.model.Model;
import org.wrml.model.rest.*;
import org.wrml.runtime.Context;
import org.wrml.runtime.Dimensions;
import org.wrml.runtime.Keys;
import org.wrml.runtime.schema.*;
import org.wrml.runtime.syntax.SyntaxLoader;

import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A runtime manifestation of a specific {@link Api}'s specific {@link ResourceTemplate} (REST API URI tree node).
 */
public class Resource implements Comparable<Resource> {

    private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);

    private static final String TO_STRING_FORMAT = "{\"Resource\" : { \"ResourceTemplateId\" : \"%s\",\"UriTemplate\" : %s,\"FullPath\" : \"%s\"}}";

    /**
     * The {@link ApiNavigator} that owns us.
     */
    private final ApiNavigator _ApiNavigator;

    private final ResourceTemplate _ResourceTemplate;

    private final UriTemplate _UriTemplate;

    private final Resource _ParentResource;

    private final String _FullPath;

    private final String _ParentPath;

    private final ConcurrentHashMap<String, Resource> _LiteralPathSubresources;

    private final ConcurrentHashMap<String, Resource> _VariablePathSubresources;

    /**
     * The ways in which resources may reference us.
     */
    private final ConcurrentHashMap<URI, LinkTemplate> _ReferenceTemplates;

    private final ConcurrentHashMap<Method, Set<URI>> _ReferenceTemplateMethodToLinkRelationUrisMap;

    private final ConcurrentHashMap<Method, Set<URI>> _ReferenceTemplateMethodToRequestSchemaUrisMap;

    private final ConcurrentHashMap<Method, Set<URI>> _ReferenceTemplateMethodToResponseSchemaUriMap;

    /**
     * The ways in which we may link to (or reference) resources.
     */
    private final ConcurrentHashMap<URI, LinkTemplate> _LinkTemplates;

    /**
     * <p>
     * The {@link Resource} constructor compiles a "chunk" of the {@link Api} metadata; an individual
     * {@link ResourceTemplate}. It is part of a runtime-constructed tree structure that represents each URI path '/' as
     * a hierarchical tree of {@link Resource} nodes.
     * </p>
     * <p/>
     * <p>
     * If an {@link Api} were a regex input string, and an {@link ApiNavigator} was its corresponding Regex compilation;
     * then a {@link Resource} would be a subexpression, a nested component within the compiled (optimized) regex. The
     * {@link Resource} (along with the {@link ApiNavigator}) compile {@link Api} metadata so that it is ready to be
     * used by the runtime for "pattern matching" (request routing by the framework).
     * </p>
     */
    Resource(final ApiNavigator apiNavigator, final ResourceTemplate resourceTemplate,
            final Resource parentResource) {

        if (apiNavigator == null) {
            throw new ResourceException("The apiNavigator may not be null", null, this);
        }

        if (resourceTemplate == null) {
            throw new ResourceException("The resource template may not be null", null, this);
        }

        _ApiNavigator = apiNavigator;
        _ResourceTemplate = resourceTemplate;
        _ParentResource = parentResource;
        _FullPath = getFullPath(parentResource);
        if (_ParentResource != null) {
            _ParentPath = _ParentResource.getPathText();
        } else {
            _ParentPath = null;
        }

        final Api api = apiNavigator.getApi();
        final Context context = api.getContext();
        final ApiLoader apiLoader = context.getApiLoader();
        final SyntaxLoader syntaxLoader = context.getSyntaxLoader();

        final URI apiUri = api.getUri();

        final String uriTemplateString = StringUtils.join(apiUri.toString(), _FullPath);
        LOGGER.debug("creating resource with uriTemplateString={} and _FullPath={}", uriTemplateString, _FullPath);

        _UriTemplate = new UriTemplate(syntaxLoader, uriTemplateString);
        _LiteralPathSubresources = new ConcurrentHashMap<String, Resource>();
        _VariablePathSubresources = new ConcurrentHashMap<String, Resource>();
        _LinkTemplates = new ConcurrentHashMap<URI, LinkTemplate>();

        // The reference templates are API metadata that describe possible "request/link" types that may target this
        // resource as an endpoint.
        _ReferenceTemplates = new ConcurrentHashMap<URI, LinkTemplate>();

        _ReferenceTemplateMethodToLinkRelationUrisMap = new ConcurrentHashMap<Method, Set<URI>>();
        _ReferenceTemplateMethodToRequestSchemaUrisMap = new ConcurrentHashMap<Method, Set<URI>>();
        _ReferenceTemplateMethodToResponseSchemaUriMap = new ConcurrentHashMap<Method, Set<URI>>();

        final UUID resourceTemplateId = _ResourceTemplate.getUniqueId();

        final List<LinkTemplate> linkTemplates = api.getLinkTemplates();
        for (final LinkTemplate linkTemplate : linkTemplates) {

            final URI linkRelationUri = linkTemplate.getLinkRelationUri();
            if (linkRelationUri == null) {
                continue;
            }

            final UUID endPointId = linkTemplate.getEndPointId();
            if (endPointId != null && endPointId.equals(resourceTemplateId)) {
                _ReferenceTemplates.put(linkRelationUri, linkTemplate);

                final LinkTemplate reference = linkTemplate;

                // Each reference has an associate link relation which is it's "metafunction".

                final SchemaLoader schemaLoader = context.getSchemaLoader();
                final URI documentSchemaUriConstant = schemaLoader.getDocumentSchemaUri();

                final Keys relKeys = apiLoader.buildDocumentKeys(linkRelationUri,
                        schemaLoader.getLinkRelationSchemaUri());

                final Dimensions relDimensions = apiNavigator.getLinkRelationDimensions();
                final LinkRelation rel = context.getModel(relKeys, relDimensions);

                if (rel == null) {
                    throw new ResourceException("The link relation: " + linkRelationUri + " was not found", null,
                            this);
                }

                // The interaction method associated with the link relation matches the parameter.

                final Method requestMethod = rel.getMethod();

                if (!_ReferenceTemplateMethodToLinkRelationUrisMap.containsKey(requestMethod)) {
                    _ReferenceTemplateMethodToLinkRelationUrisMap.put(requestMethod, new LinkedHashSet<URI>());
                }

                final Set<URI> linkRelationUris = _ReferenceTemplateMethodToLinkRelationUrisMap.get(requestMethod);
                linkRelationUris.add(linkRelationUri);

                if (!_ReferenceTemplateMethodToRequestSchemaUrisMap.containsKey(requestMethod)) {
                    _ReferenceTemplateMethodToRequestSchemaUrisMap.put(requestMethod, new LinkedHashSet<URI>());
                }

                final Set<URI> requestSchemaUris = _ReferenceTemplateMethodToRequestSchemaUrisMap
                        .get(requestMethod);

                // The API's reference template may have defined its own API-specific argument type
                final URI referenceRequestSchemaUri = reference.getRequestSchemaUri();
                if (referenceRequestSchemaUri != null) {
                    requestSchemaUris.add(referenceRequestSchemaUri);
                }

                // The reference's link relation may have defined a generic, reusable argument type
                final URI relRequestSchemaUri = rel.getRequestSchemaUri();
                if (relRequestSchemaUri != null && !documentSchemaUriConstant.equals(relRequestSchemaUri)) {
                    requestSchemaUris.add(relRequestSchemaUri);
                }

                if (!_ReferenceTemplateMethodToResponseSchemaUriMap.containsKey(requestMethod)) {
                    _ReferenceTemplateMethodToResponseSchemaUriMap.put(requestMethod, new LinkedHashSet<URI>());
                }

                final Set<URI> responseSchemaUris = _ReferenceTemplateMethodToResponseSchemaUriMap
                        .get(requestMethod);

                // The API's reference template may have defined its own API-specific response type
                final URI referenceResponseSchemaUri = reference.getResponseSchemaUri();
                if (referenceResponseSchemaUri != null) {
                    responseSchemaUris.add(referenceResponseSchemaUri);
                }

                // The reference's link relation may have defined a generic, reusable response type
                final URI relResponseSchemaUri = rel.getResponseSchemaUri();
                if (relResponseSchemaUri != null && !documentSchemaUriConstant.equals(relResponseSchemaUri)) {
                    responseSchemaUris.add(relResponseSchemaUri);
                }

            }

            final UUID referrerId = linkTemplate.getReferrerId();
            if (referrerId != null && referrerId.equals(resourceTemplateId)) {
                _LinkTemplates.put(linkRelationUri, linkTemplate);
            }

        }

    }

    private final String getFullPath(final Resource parentResource) {

        final StringBuffer sb = new StringBuffer();
        boolean appendPathSeparator = true;

        if (parentResource != null && parentResource.getPathText() != null) {
            final String text = parentResource.getPathText();
            sb.append(text);
            if (text.endsWith(ApiNavigator.PATH_SEPARATOR)) {
                appendPathSeparator = false;
            }
        }

        if (appendPathSeparator) {
            sb.append(ApiNavigator.PATH_SEPARATOR);
        }

        final String pathSegment = getPathSegment();

        if (StringUtils.isNotEmpty(pathSegment)) {
            sb.append(pathSegment);
        }

        return sb.toString();
    }

    /**
     * @return a flattened {@link List} of all child and sub-child {@link Resource}s (recursive).
     */
    public List<Resource> getAllChildResources() {

        final List<Resource> allChildResources = new LinkedList<>();
        final List<ResourceTemplate> childResourceTemplates = this._ResourceTemplate.getChildren();
        for (final ResourceTemplate childResourceTemplate : childResourceTemplates) {
            final Resource childResource = this._ApiNavigator.getResource(childResourceTemplate.getUniqueId());
            if (childResource != null) {
                allChildResources.add(childResource);
                allChildResources.addAll(childResource.getAllChildResources());
            }
        }

        return allChildResources;
    }

    /**
     * @return the {@link ApiNavigator} that owns this {@link Resource}.
     */
    public ApiNavigator getApiNavigator() {

        return _ApiNavigator;
    }

    public URI getDefaultDocumentUri() {

        final UriTemplate uriTemplate = getUriTemplate();

        final String[] parameterNames = uriTemplate.getParameterNames();
        final Map<String, Object> parameterMap = new LinkedHashMap<>();

        if (parameterNames != null && parameterNames.length > 0) {

            final Api api = getApiNavigator().getApi();
            final Context context = api.getContext();
            final SchemaLoader schemaLoader = context.getSchemaLoader();

            final URI defaultSchemaUri = getDefaultSchemaUri();
            final Prototype defaultPrototype = (defaultSchemaUri != null)
                    ? schemaLoader.getPrototype(defaultSchemaUri)
                    : null;

            for (int i = 0; i < parameterNames.length; i++) {
                final String parameterName = parameterNames[i];

                URI keyedSchemaUri = null;

                if (defaultPrototype != null) {
                    final Set<String> allKeySlotNames = defaultPrototype.getAllKeySlotNames();
                    if (allKeySlotNames != null && allKeySlotNames.contains(parameterName)) {
                        keyedSchemaUri = defaultSchemaUri;
                    }
                }

                if (keyedSchemaUri == null) {

                    final ConcurrentHashMap<URI, LinkTemplate> referenceTemplates = getReferenceTemplates();

                    if (referenceTemplates != null && !referenceTemplates.isEmpty()) {

                        final Set<URI> referenceLinkRelationUris = getReferenceLinkRelationUris(Method.Get);
                        if (referenceLinkRelationUris != null && !referenceLinkRelationUris.isEmpty()) {
                            for (URI linkRelationUri : referenceLinkRelationUris) {
                                final LinkTemplate referenceTemplate = referenceTemplates.get(linkRelationUri);
                                final URI responseSchemaUri = referenceTemplate.getResponseSchemaUri();
                                final Prototype responseSchemaPrototype = schemaLoader
                                        .getPrototype(responseSchemaUri);
                                if (responseSchemaPrototype != null) {
                                    final Set<String> allKeySlotNames = responseSchemaPrototype
                                            .getAllKeySlotNames();
                                    if (allKeySlotNames != null && allKeySlotNames.contains(parameterName)) {
                                        keyedSchemaUri = responseSchemaUri;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }

                Object defaultValue = null;

                if (keyedSchemaUri != null) {

                    final Prototype keyedPrototype = schemaLoader.getPrototype(keyedSchemaUri);
                    final ProtoSlot keyProtoSlot = keyedPrototype.getProtoSlot(parameterName);
                    if (keyProtoSlot instanceof PropertyProtoSlot) {
                        final PropertyProtoSlot keyPropertyProtoSlot = (PropertyProtoSlot) keyProtoSlot;

                        // TODO: Allow more fine grain control of the default parameter value

                        defaultValue = keyPropertyProtoSlot.getDefaultValue();

                        if (defaultValue == null) {
                            defaultValue = keyPropertyProtoSlot.getValueType().getDefaultValue();
                        }

                    }
                }

                parameterMap.put(parameterName, defaultValue);
            }
        }

        return uriTemplate.evaluate(parameterMap, true);
    }

    public URI getDefaultSchemaUri() {

        return getResourceTemplate().getDefaultSchemaUri();
    }

    public URI getDocumentUri(final Document document) {

        final UriTemplate uriTemplate = getUriTemplate();
        final String[] parameterNames = uriTemplate.getParameterNames();
        if (parameterNames == null) {
            return uriTemplate.evaluate(null);
        } else {
            final Map<String, Object> parameterMap = new LinkedHashMap<>();
            for (final String parameterName : parameterNames) {
                if (!document.containsSlotValue(parameterName)) {
                    return null;
                }

                Object parameterValue = document.getSlotValue(parameterName);
                if (parameterValue == null) {
                    return null;
                }

                parameterMap.put(parameterName, parameterValue);
            }
            return uriTemplate.evaluate(parameterMap);
        }

    }

    /**
     * A mapping of {@link LinkRelation} id ({@link URI}) to {@link LinkTemplate} model instance. This (conceptual) set
     * of {@link LinkTemplate}s represent the ways in which we may link to (reference) resources.
     *
     * @return A map of link relation id to link template, which communicates the ways that this {@link Resource} may
     * reference other {@link Resource}s.
     */
    public ConcurrentHashMap<URI, LinkTemplate> getLinkTemplates() {

        return _LinkTemplates;
    }

    public ConcurrentHashMap<String, Resource> getLiteralPathSubresources() {

        return _LiteralPathSubresources;
    }

    public Resource getParentResource() {

        return _ParentResource;
    }

    public List<Resource> getPath(final boolean includeDocroot) {

        final List<Resource> path = new ArrayList<>();
        Resource parent = getParentResource();

        if (!includeDocroot && parent == null) {
            return path;
        }

        path.add(this);

        if (parent == null) {
            return path;
        }

        while (parent != null) {
            path.add(parent);
            parent = parent.getParentResource();
        }

        if (!includeDocroot) {
            path.remove(path.size() - 1);
        }

        Collections.reverse(path);

        return path;
    }

    public String getPathSegment() {

        return this._ResourceTemplate.getPathSegment();
    }

    public String getPathText() {

        return _FullPath;
    }

    public String getParentPathText() {

        return _ParentPath;
    }

    public Set<URI> getReferenceLinkRelationUris(final Method requestMethod) {

        return _ReferenceTemplateMethodToLinkRelationUrisMap.get(requestMethod);
    }

    /**
     * A mapping of {@link LinkRelation} id ({@link URI}) to {@link LinkTemplate} model instance. This (conceptual) set
     * of {@link LinkTemplate}s represent the ways in which other resources may link to (reference) us.
     *
     * @return The ways that we may be referenced by other resources.
     */
    public ConcurrentHashMap<URI, LinkTemplate> getReferenceTemplates() {

        return _ReferenceTemplates;
    }

    public Set<URI> getRequestSchemaUris(final Method requestMethod) {

        return _ReferenceTemplateMethodToRequestSchemaUrisMap.get(requestMethod);
    }

    public ResourceTemplate getResourceTemplate() {

        return _ResourceTemplate;
    }

    public UUID getResourceTemplateId() {

        if (_ResourceTemplate == null) {
            return null;
        }
        return _ResourceTemplate.getUniqueId();
    }

    public Set<URI> getResponseSchemaUris(final Method requestMethod) {

        return _ReferenceTemplateMethodToResponseSchemaUriMap.get(requestMethod);
    }

    /**
     * Generates the "href" URI used to refer to this resource from the specified referrer {@link Model} instance using
     * the specified {@link LinkRelation} {@link URI} value.
     */
    public URI getHrefUri(final Model referrer, final URI referenceRelationUri) {

        if (referrer == null) {
            return null;
        }

        /*
         * Given the nature of the Api's ResourceTemplate metadata tree, the runtime resource can determine its own
         * UriTemplate (and it only needs to determine/compute this once).
         */

        final UriTemplate uriTemplate = getUriTemplate();
        if (uriTemplate == null) {
            return null;
        }

        /*
         * Get the end point id's templated parameter names, for example: a UriTemplate might have slots that look like
         * {teamId} or {highScoreId} or {name} appearing where legal (according to UriTemplate syntax, see:
         * http://tools.ietf.org/html/rfc6570). A fixed URI, meaning a UriTemplate with no variables, will return null
         * here.
         */
        final String[] uriTemplateParameterNames = this._UriTemplate.getParameterNames();

        Map<String, Object> parameterMap = null;

        if (uriTemplateParameterNames != null && uriTemplateParameterNames.length > 0) {

            // Get the Link slot's bindings, which may be used to provide an alternative source for one or more URI
            // template parameter values.
            final Prototype referrerPrototype = referrer.getPrototype();

            final SortedMap<URI, LinkProtoSlot> linkProtoSlots = referrerPrototype.getLinkProtoSlots();

            Map<String, ProtoValueSource> linkSlotBindings = null;

            if (linkProtoSlots != null && !linkProtoSlots.isEmpty()) {
                final LinkProtoSlot linkProtoSlot = linkProtoSlots.get(referenceRelationUri);
                if (linkProtoSlot != null) {
                    linkSlotBindings = linkProtoSlot.getLinkSlotBindings();
                }
            }

            parameterMap = new LinkedHashMap<>(uriTemplateParameterNames.length);

            for (final String paramName : uriTemplateParameterNames) {

                final Object paramValue;

                if (linkSlotBindings != null && linkSlotBindings.containsKey(paramName)) {
                    // The link slot has declared a binding to an alternate source for this URI template parameter's
                    // value.
                    final ProtoValueSource valueSource = linkSlotBindings.get(paramName);
                    paramValue = valueSource.getValue(referrer);
                } else {
                    // By default, if the end point's UriTemplate has parameters (blanks) to fill in, then by convention
                    // we
                    // assume that the referrer model has the corresponding slot values to match the UriTemplate's
                    // inputs/slots.
                    //
                    // Put simply, (by default) referrer model slot names "match" UriTemplate param names.
                    //
                    // This enforces that the model's own represented resource state is the only thing used to
                    // (automatically) drive the link-based graph traversal (aka HATEOAS). Its also a simple convention
                    // that
                    // is (reasonably) easy to comprehend, hopefully even intuitive.

                    paramValue = referrer.getSlotValue(paramName);
                }

                parameterMap.put(paramName, paramValue);
            }
        }

        final URI uri = this._UriTemplate.evaluate(parameterMap);
        return uri;

    }

    public UriTemplate getUriTemplate() {

        return _UriTemplate;
    }

    /**
     * @return a sorted map of variable path child resources (i.e. {keySlotName})
     */
    public ConcurrentHashMap<String, Resource> getVariablePathSubresources() {

        return _VariablePathSubresources;
    }

    public boolean isDocroot() {

        return (getParentResource() == null);
    }

    /**
     * Adds a resource to this resource's list of subresources, differentiating based on its inclusion of the {
     * character whether it's a literal or variable subresource
     *
     * @param subresource
     */
    void addSubresource(final Resource subresource) {

        final String pathSegment = subresource.getPathSegment();
        if (StringUtils.containsAny(pathSegment, '{')) {
            addVariablePathSubresource(pathSegment, subresource);
        } else {
            addLiteralPathSubresource(pathSegment, subresource);
        }
    }

    private void addLiteralPathSubresource(final String pathSegment, final Resource subresource) {

        _LiteralPathSubresources.put(pathSegment, subresource);
    }

    private void addVariablePathSubresource(final String pathSegment, final Resource subresource) {

        _VariablePathSubresources.put(pathSegment, subresource);
    }

    @Override
    public String toString() {

        return String.format(TO_STRING_FORMAT, getResourceTemplateId(), _UriTemplate, _FullPath);
    }

    @Override
    public int compareTo(final Resource otherResource) {

        return ComparisonChain.start().compare(this._FullPath, otherResource._FullPath).result();
    }

    public SortedSet<Parameter> getSurrogateKeyComponents(final URI uri, final Prototype prototype) {

        final Set<URI> responseSchemaUris = getResponseSchemaUris(Method.Get);
        if (responseSchemaUris == null) {
            return null;
        }

        boolean isCompatibleResource = false;
        for (final URI responseSchemaUri : responseSchemaUris) {
            if (prototype.isAssignableFrom(responseSchemaUri)) {
                isCompatibleResource = true;
                break;
            }
        }

        if (!isCompatibleResource) {
            return null;
        }

        final UriTemplate uriTemplate = getUriTemplate();
        return uriTemplate.getParameters(uri);

    }
}