/* * Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten * * 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 de.escalon.hypermedia.affordance; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonUnwrapped; import de.escalon.hypermedia.action.Cardinality; import org.springframework.aop.DynamicIntroductionAdvice; import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.UriTemplate; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import java.util.*; import static de.escalon.hypermedia.affordance.Affordance.LinkParam.*; /** * Represents an http affordance for purposes of a ReST service as described by <a * href="http://tools.ietf.org/html/rfc5988">Web Linking rfc-5988</a>. Additionally includes {@link ActionDescriptor}s * for http methods and expected request bodies. <p>Also supports templated affordances, in which case it is represented * as a <a href="http://tools.ietf.org/html/draft-nottingham-link-template-01">Link-Template Header</a></p> <p>This * class can be created manually or via one of the {@link de.escalon.hypermedia.spring.AffordanceBuilder#linkTo} * methods. In the latter case the affordance should be created with pre-expanded variables (using {@link * PartialUriTemplate#expand} on the given uri template). In the former case one may use {@link #expandPartially} to * expand the Affordance variables as far as possible, while keeping unsatisified variables.</p> <p>Created by dschulten * on 07.09.2014.</p> */ public class Affordance extends Link { enum LinkParam { REL("rel"), ANCHOR("anchor"), REV("rev"), HREFLANG("hreflang"), MEDIA("media"), TITLE("title"), TITLE_STAR("title*"), TYPE("type"); String paramName; LinkParam(String paramName) { this.paramName = paramName; } static LinkParam valueOfParamName(String paramName) { LinkParam.values(); for (LinkParam linkParam : LinkParam.values()) { if (linkParam.paramName.equals(paramName)) { return linkParam; } } return null; } } private boolean selfRel = false; private List<ActionDescriptor> actionDescriptors = new ArrayList<ActionDescriptor>(); private MultiValueMap<String, String> linkParams = new LinkedMultiValueMap<String, String>(); private PartialUriTemplate partialUriTemplate; private Cardinality cardinality = Cardinality.SINGLE; private TypedResource collectionHolder; /** * Creates affordance. Action descriptors and link header params may be added later. * * @param uriTemplate * uri or uritemplate of the affordance * @param rels * describing the link relation type */ public Affordance(String uriTemplate, String... rels) { this(new PartialUriTemplate(uriTemplate), new ArrayList<ActionDescriptor>(), rels); } /** * Creates affordance. Rels, action descriptors and link header params may be added later. * * @param uriTemplate * uri or uritemplate of the affordance */ public Affordance(String uriTemplate) { this(uriTemplate, new String[]{}); } /** * Creates affordance, usually for a pre-expanded uriTemplate. Link header params may be added later. Optional * variables will be stripped before passing it to the underlying link. Use {@link #getUriTemplateComponents()} to * access the base uri, query head, query tail with optional variables etc. * * @param uriTemplate * pre-expanded uri or uritemplate of the affordance * @param actionDescriptors * describing the possible http methods on the affordance * @param rels * describing the link relation type * @see PartialUriTemplate#expand */ public Affordance(PartialUriTemplate uriTemplate, List<ActionDescriptor> actionDescriptors, String... rels) { // Since AffordanceBuilder creates variables for undefined arguments, // we would get a link-template where ControllerLinkBuilder only sees a link. // For compatibility we strip variables deemed to be not required by the actionDescriptors before passing on // the template to the underlying Link. That way the href of an Affordance stays compatible with a Link that // has been created with ControllerLinkBuilder. Only serializers that make use of Affordance will see the // optional variables, too. // They can access the base uri, query etc. via getUriTemplateComponents. super(uriTemplate.stripOptionalVariables(actionDescriptors) .toString()); this.partialUriTemplate = uriTemplate; Assert.noNullElements(rels, "null rels are not allowed"); for (String rel : rels) { addRel(rel); if ("self".equals(rel)) { selfRel = true; } } // if any action refers to a collection resource, make the affordance a collection affordance for (ActionDescriptor actionDescriptor : actionDescriptors) { if (Cardinality.COLLECTION == actionDescriptor.getCardinality()) { this.cardinality = Cardinality.COLLECTION; break; } } this.actionDescriptors.addAll(actionDescriptors); } private Affordance(String uriTemplate, MultiValueMap<String, String> linkParams, List<ActionDescriptor> actionDescriptors) { this(new PartialUriTemplate(uriTemplate), actionDescriptors); // no rels to pass this.linkParams = linkParams; // takes care of rels } /** * The relation type of the link. * * @param rel * IANA-registered type or extension relation type. */ public void addRel(String rel) { Assert.hasLength(rel); linkParams.add(REL.paramName, rel); } /** * The "type" parameter, when present, is a hint indicating what the media type of the result of dereferencing the * link should be. Note that this is only a hint; for example, it does not override the Content-Type header of a * HTTP response obtained by actually following the link. There MUST NOT be more than one type parameter in a link- * value. * * @param mediaType * to set */ public void setType(String mediaType) { if (mediaType != null) linkParams.set(TYPE.paramName, mediaType); else linkParams.remove(TYPE.paramName); } /** * The "hreflang" parameter, when present, is a hint indicating what the language of the result of dereferencing the * link should be. Note that this is only a hint; for example, it does not override the Content-Language header of * a HTTP response obtained by actually following the link. Multiple "hreflang" parameters on a single link- value * indicate that multiple languages are available from the indicated resource. * * @param hreflang * to add */ public void addHreflang(String hreflang) { Assert.hasLength(hreflang); linkParams.add(HREFLANG.paramName, hreflang); } /** * The "title" parameter, when present, is used to label the destination of a link such that it can be used as a * human-readable identifier (e.g., a menu entry) in the language indicated by the Content- Language header (if * present). The "title" parameter MUST NOT appear more than once in a given link-value; occurrences after the * first MUST be ignored by parsers. * * @param title * to set */ public void setTitle(String title) { if (title != null) linkParams.set(TITLE.paramName, title); else { linkParams.remove(TITLE.paramName); } } @JsonIgnore public boolean isBaseUriTemplated() { return partialUriTemplate.asComponents() .isBaseUriTemplated(); } /** * Gets the 'title' link parameter, which gives a human-readable identifier in the language indicated by * Content-Language header. * * @return title of link * @see #setTitle(String) */ public String getTitle() { return linkParams.getFirst(TITLE.paramName); } /** * Bean which allows to json-unwrap map-valued properties. * @see <a href="https://github.com/FasterXML/jackson-databind/issues/171">JsonUnwrapped not supported for Map-valued properties</a> * @see <a href="http://www.cowtowncoder.com/blog/archives/2011/07/entry_458.html">Jackson tips: using @JsonAnyGetter/@JsonAnySetter to create "dyna beans"</a> */ public class DynaBean { protected Map<String,Object> dynaProperties = new HashMap<String,Object>(); public Object get(String name) { return dynaProperties.get(name); } public void putAll(Map<String, String> map) { dynaProperties.putAll(map); } // "any getter" needed for serialization @JsonAnyGetter public Map<String,Object> any() { return dynaProperties; } @JsonAnySetter public void set(String name, Object value) { dynaProperties.put(name, value); } @Override public String toString() { return dynaProperties.toString(); } } @JsonUnwrapped public DynaBean getLinkExtensions() { DynaBean dynaBean = new DynaBean(); LinkedHashMap<String, String> linkExtensions = new LinkedHashMap<String, String>(); linkExtensions.putAll(linkParams.toSingleValueMap()); for (LinkParam linkParam : LinkParam.values()) { linkExtensions.remove(linkParam.paramName); } dynaBean.putAll(linkExtensions); return dynaBean; } /** * Gets the 'type' link parameter, which gives a hint which mime type can be expected when following the link. * * @return type of link * @see #setType(String) */ public String getType() { return linkParams.getFirst(TYPE.paramName); } /** * The "title*" parameter can be used to encode the title label in a different character set, and/or contain * language information as per <a href="https://tools.ietf.org/html/rfc5987">RFC-5987</a>. The "title*" parameter MUST NOT appear more than once in a given * link-value; occurrences after the first MUST be ignored by parsers. If the parameter does not contain language * information, its language is indicated by the Content-Language header (when present). * * If both the "title" and "title*" parameters appear in a link-value, processors SHOULD use the "title*" * parameter's value. <p>The example below shows an instance of the Link header encoding a link title using <a href="https://tools.ietf.org/html/rfc2231">RFC-2231</a> * encoding to encode both non-ASCII characters and language information.</p> * <pre> * Link: </TheBook/chapter2> * rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel * </pre> * * @param titleStar * to set */ public void setTitleStar(String titleStar) { if (titleStar != null) linkParams.set(TITLE_STAR.paramName, titleStar); else linkParams.remove(TITLE_STAR.paramName); } /** * The "media" parameter, when present, is used to indicate intended destination medium or media for style * information (see [W3C.REC-html401-19991224], Section 6.13). Note that this may be updated by * [W3C.CR-css3-mediaqueries-20090915]). Its value MUST be quoted if it contains a semicolon (";") or comma (","), * and there MUST NOT be more than one "media" parameter in a link-value. * * @param mediaDesc * to set */ public void setMedia(String mediaDesc) { if (mediaDesc != null) linkParams.set(MEDIA.paramName, mediaDesc); else linkParams.remove(MEDIA.paramName); } /** * The "rev" parameter has been used in the past to indicate that the semantics of the relationship are in the * reverse direction. That is, a link from A to B with REL="X" expresses the same relationship as a link from B to * A with REV="X". "rev" is deprecated by this specification because it often confuses authors and readers; in most * cases, using a separate relation type is preferable. * * @param rev * to add */ public void addRev(String rev) { Assert.hasLength(rev); linkParams.add(REV.paramName, rev); } /** * By default, the context of a link conveyed in the Link header field is the IRI of the requested resource. <p>When * present, the anchor parameter overrides this with another URI, such as a fragment of this resource, or a third * resource (i.e., when the anchor value is an absolute URI). If the anchor parameter's value is a relative URI, * parsers MUST resolve it as per [RFC3986], Section 5. Note that any base URI from the body's content is not * applied. * * @param anchor * base uri to define */ public void setAnchor(String anchor) { if (anchor != null) linkParams.set(ANCHOR.paramName, anchor); else linkParams.remove(ANCHOR.paramName); } /** * Adds link-extension params, i.e. custom params which are not described in the web linking rfc. * * @param paramName * of link-extension * @param values * one or more values to add */ public void addLinkParam(String paramName, String... values) { Assert.notEmpty(values); for (String value : values) { Assert.hasLength(value); linkParams.add(paramName, value); } } /** * Gets header name of the affordance, either Link or Link-Template depending on the presence of template * variables. * * @return header name * @see <a href="http://tools.ietf.org/html/rfc5988">Web Linking rfc-5988</a> * @see <a href="http://tools.ietf.org/html/draft-nottingham-link-template-01">Link-Template Header</a> */ @JsonIgnore public String getHeaderName() { String headerName; if (super.isTemplated()) { headerName = "Link-Template"; } else { headerName = "Link"; } return headerName; } /** * Affordance represented as http link header value. Note that the href may be templated, for convenience you can * use {@link #getHeaderName()} to ensure a Link or Link-Header is produced appropriately. * * @return Link or Link-template header value */ public String asHeader() { StringBuilder result = new StringBuilder(); for (Map.Entry<String, List<String>> linkParamEntry : linkParams.entrySet()) { if (result.length() != 0) { result.append("; "); } String linkParamEntryKey = linkParamEntry.getKey(); if (REL.paramName.equals(linkParamEntryKey) || REV.paramName.equals(linkParamEntryKey)) { result.append(linkParamEntryKey) .append("="); result.append("\"") .append(StringUtils.collectionToDelimitedString(linkParamEntry.getValue(), " ")) .append("\""); } else { StringBuilder linkParams = new StringBuilder(); for (String value : linkParamEntry.getValue()) { if (linkParams.length() != 0) { linkParams.append("; "); } linkParams.append(linkParamEntryKey) .append("="); linkParams.append("\"") .append(value) .append("\""); } result.append(linkParams); } } String linkHeader = "<" + partialUriTemplate.asComponents() .toString() + ">; "; return result.insert(0, linkHeader) .toString(); } /** * Returns template variables contained in the underlying Link with basic distinction of required and optional * variables based on the variable type. If actionDescriptors are present, they should be preferred over variables * because they consider the handler methods to determine if a variable is required or optional. * * @return variables */ @Override public List<TemplateVariable> getVariables() { return super.getVariables(); } @Override public String toString() { return getHeaderName() + ": " + asHeader(); } @Override public Affordance withRel(String rel) { linkParams.set(REL.paramName, rel); return new Affordance(this.getHref(), linkParams, actionDescriptors); } @Override public Affordance withSelfRel() { if (!linkParams.get(REL.paramName) .contains(Link.REL_SELF)) { linkParams.add(REL.paramName, Link.REL_SELF); } return new Affordance(this.getHref(), linkParams, actionDescriptors); } /** * Expands template variables, arguments must satisfy all required template variables, optional variables will be * removed. * * @param arguments * to expansion in the order they appear in the template * @return expanded affordance */ @Override public Affordance expand(Object... arguments) { UriTemplate template = new UriTemplate(partialUriTemplate.asComponents() .toString()); String expanded = template.expand(arguments) .toASCIIString(); return new Affordance(expanded, linkParams, actionDescriptors); } /** * Gets parts of the uri template such as base uri, expanded query part, unexpanded query part etc. * * @return template component parts */ @JsonIgnore public PartialUriTemplateComponents getUriTemplateComponents() { return partialUriTemplate.asComponents(); } /** * Expands template variables, arguments must satisfy all required template variables, unsatisfied optional * arguments will be removed. * * @param arguments * to expansion * @return expanded affordance */ @Override public Affordance expand(Map<String, ? extends Object> arguments) { UriTemplate template = new UriTemplate(partialUriTemplate.asComponents() .toString()); String expanded = template.expand(arguments) .toASCIIString(); return new Affordance(expanded, linkParams, actionDescriptors); } /** * Expands template variables as far as possible, unsatisfied variables will remain variables. This is primarily for * manually created affordances. If the Affordance has been created with linkTo-methodOn, it should not be necessary * to expand the affordance again. * * @param arguments * for expansion, in the order they appear in the template * @return partially expanded affordance */ public Affordance expandPartially(Object... arguments) { return new Affordance(partialUriTemplate.expand(arguments) .toString(), linkParams, actionDescriptors); } /** * Expands template variables as far as possible, unsatisfied variables will remain variables. This is primarily for * manually created affordances. If the Affordance has been created with linkTo-methodOn, it should not be necessary * to expand the affordance again. * * @param arguments * for expansion * @return partially expanded affordance */ public Affordance expandPartially(Map<String, ? extends Object> arguments) { return new Affordance(partialUriTemplate.expand((Map<String, Object>) arguments) .toString(), linkParams, actionDescriptors); } /** * Allows to retrieve all rels defined for this affordance. * * @return rels */ @JsonIgnore public List<String> getRels() { final List<String> rels = linkParams.get(REL.paramName); return rels == null ? Collections.<String>emptyList() : Collections.unmodifiableList(rels); } /** * Gets the rel. * * @return first defined rel or null */ @Override public String getRel() { return linkParams.getFirst(REL.paramName); } /** * Retrieves all revs for this affordance. * * @return revs */ @JsonIgnore public List<String> getRevs() { final List<String> revs = linkParams.get(REV.paramName); return revs == null ? Collections.<String>emptyList() : Collections.unmodifiableList(revs); } /** * Gets the rev. * * @return first defined rev or null */ @JsonIgnore public String getRev() { return linkParams.getFirst(REV.paramName); } /** * Sets action descriptors. * * @param actionDescriptors * to set */ public void setActionDescriptors(List<ActionDescriptor> actionDescriptors) { if (this.actionDescriptors.isEmpty()) { this.actionDescriptors = actionDescriptors; } else { throw new IllegalStateException("cannot redefine existing action descriptors"); } } /** * Gets action descriptors. * * @return descriptors, never null */ @JsonIgnore public List<ActionDescriptor> getActionDescriptors() { return Collections.unmodifiableList(actionDescriptors); } /** * Determines if the affordance points to a single or a collection resource. * * @return single or collection cardinality, never null */ @JsonIgnore public Cardinality getCardinality() { return cardinality; } /** * Determines if the affordance is a self rel. * * @return true if the affordance is a self rel */ @JsonIgnore public boolean isSelfRel() { return selfRel; } /** * Determines if the affordance has unsatisfied required variables. This allows to decide if the affordance can also * be treated as a plain Link without template variables if the caller omits all optional variables. Serializers can * use this to render it as a resource with optional search features. * * @return true if the affordance has unsatisfied required variables */ @JsonIgnore public boolean hasUnsatisfiedRequiredVariables() { for (ActionDescriptor actionDescriptor : actionDescriptors) { Map<String, ActionInputParameter> requiredParameters = actionDescriptor.getRequiredParameters(); for (ActionInputParameter annotatedParameter : requiredParameters.values()) { if (!annotatedParameter.hasValue()) { return true; } } } return false; } /** * Gets collection holder. If an affordance points to a collection, there are cases where the resource that has the * affordance is not semantically <em>holding</em> the collection items, but just has a loose relationship to the * collection. E.g. a product "has" no orderedItems, but it may have a loose relationship to a collection of ordered * items where the product can be POSTed to. The thing that semantically <em>holds</em> ordered items is an order, * not a product. Hence the order would be the collection holder. * * @return collection holder */ @JsonIgnore public TypedResource getCollectionHolder() { return collectionHolder; } public void setCollectionHolder(TypedResource collectionHolder) { this.collectionHolder = collectionHolder; } }