de.escalon.hypermedia.spring.AffordanceBuilder Maven / Gradle / Ivy
/*
* 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.spring;
import de.escalon.hypermedia.affordance.*;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.springframework.util.StringUtils.hasText;
/**
* Builder for hypermedia affordances, usable as rfc-5988 web links and optionally holding information about request
* body requirements. Created by dschulten on 07.09.2014.
*/
public class AffordanceBuilder implements LinkBuilder {
private static final AffordanceBuilderFactory FACTORY = new AffordanceBuilderFactory();
private PartialUriTemplateComponents partialUriTemplateComponents;
private List actionDescriptors = new ArrayList();
private MultiValueMap linkParams = new LinkedMultiValueMap();
private List rels = new ArrayList();
private List reverseRels = new ArrayList();
private TypedResource collectionHolder;
/**
* Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class.
*
* @param controller
* the class to discover the annotation on, must not be {@literal null}.
* @return builder
*/
public static AffordanceBuilder linkTo(Class> controller) {
return linkTo(controller, new Object[0]);
}
/**
* Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class. The
* additional parameters are used to fill up potentially available path variables in the class scope request
* mapping.
*
* @param controller
* the class to discover the annotation on, must not be {@literal null}.
* @param parameters
* additional parameters to bind to the URI template declared in the annotation, must not be {@literal
* null}.
* @return builder
*/
public static AffordanceBuilder linkTo(Class> controller, Object... parameters) {
return FACTORY.linkTo(controller, parameters);
}
/**
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
*/
public static AffordanceBuilder linkTo(Method method, Object... parameters) {
return linkTo(method.getDeclaringClass(), method, parameters);
}
/**
* Creates a new {@link AffordanceBuilder} with a base of the mapping annotated to the given controller class. The
* additional parameters are used to fill up potentially available path variables in the class scop request
* mapping.
*
* @param controller
* the class to discover the annotation on, must not be {@literal null}.
* @param parameters
* additional parameters to bind to the URI template declared in the annotation, must not be {@literal
* null}.
* @return
*/
public static AffordanceBuilder linkTo(Class> controller, Map parameters) {
return FACTORY.linkTo(controller, parameters);
}
/**
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class, Method, Object...)
*/
public static AffordanceBuilder linkTo(Class> controller, Method method, Object... parameters) {
return FACTORY.linkTo(controller, method, parameters);
}
/**
* Creates a {@link AffordanceBuilder} pointing to a controller method. Hand in a dummy method invocation result
* you
* can create via {@link #methodOn(Class, Object...)} or {@link DummyInvocationUtils#methodOn(Class, Object...)}.
*
* @RequestMapping("/customers")
* class CustomerController {
* @RequestMapping("/{id}/addresses")
* HttpEntity<Addresses> showAddresses(@PathVariable Long id) { � }
* }
* Link link = linkTo(methodOn(CustomerController.class).showAddresses(2L)).withRel("addresses");
*
* The resulting {@link Link} instance will point to {@code /customers/2/addresses} and have a rel of {@code
* addresses}. For more details on the method invocation constraints, see {@link
* DummyInvocationUtils#methodOn(Class, Object...)}.
*
* @param methodInvocation
* @return
*/
public static AffordanceBuilder linkTo(Object methodInvocation) {
return FACTORY.linkTo(methodInvocation);
}
/**
* Creates a new {@link AffordanceBuilder} pointing to this server, but without ActionDescriptor.
*/
AffordanceBuilder() {
this(new PartialUriTemplate(getBuilder().build()
.toString()).expand(Collections.emptyMap()),
Collections.emptyList());
}
/**
* Creates a new {@link AffordanceBuilder} using the given {@link ActionDescriptor}.
*
* @param partialUriTemplateComponents
* must not be {@literal null}
* @param actionDescriptors
* must not be {@literal null}
*/
public AffordanceBuilder(PartialUriTemplateComponents partialUriTemplateComponents, List
actionDescriptors) {
Assert.notNull(partialUriTemplateComponents);
Assert.notNull(actionDescriptors);
this.partialUriTemplateComponents = partialUriTemplateComponents;
for (ActionDescriptor actionDescriptor : actionDescriptors) {
this.actionDescriptors.add(actionDescriptor);
}
}
public static T methodOn(Class clazz, Object... parameters) {
return DummyInvocationUtils.methodOn(clazz, parameters);
}
/**
* Builds affordance with one or multiple rels which must have been defined previously using {@link #rel(String)} or
* {@link #reverseRel(String, String)}. The motivation for multiple rels is this statement in the web linking
* rfc-5988: "Note that link-values can convey multiple links between the same target and context IRIs; for
* example:
*
* Link: <http://example.org/>
* rel="start http://example.net/relation/other"
*
* Here, the link to 'http://example.org/' has the registered relation type 'start' and the extension relation type
* 'http://example.net/relation/other'."
*
* @return affordance
* @see Web Linking Examples
*/
public Affordance build() {
Assert.state(!(rels.isEmpty() && reverseRels.isEmpty()),
"no rels or reverse rels found, call rel() or rev() before building the affordance");
final Affordance affordance;
affordance = new Affordance(new PartialUriTemplate(this.toString()), actionDescriptors,
rels.toArray(new String[rels.size()]));
for (Map.Entry> linkParamEntry : linkParams.entrySet()) {
final List values = linkParamEntry.getValue();
for (String value : values) {
affordance.addLinkParam(linkParamEntry.getKey(), value);
}
}
for (String reverseRel : reverseRels) {
affordance.addRev(reverseRel);
}
affordance.setCollectionHolder(collectionHolder);
return affordance;
}
/**
* Allows to define one or more reverse link relations (a "rev" in terms of rfc-5988), where the resource that has
* the affordance will be considered the object in a subject-predicate-object statement. E.g. if you had a rel
* ex:parent
which connects a child to its father, you could also use ex:parent on the father to point
* to the child by reverting the direction of ex:parent. This is mainly useful when you have no other way to
* express
* in your context that the direction of a relationship is inverted.
*
* @param rev
* to be used as reverse relationship
* @param revertedRel
* to be used in contexts which have no notion of reverse relationships. E.g. for a reverse rel
* ex:parent
you can use a made-up rel name ex:child
which will be used as rel
* when rendering HAL.
* @return builder
*/
public AffordanceBuilder reverseRel(String rev, String revertedRel) {
this.rels.add(revertedRel);
this.reverseRels.add(rev);
return this;
}
/**
* Allows to define one or more reverse link relations (a "rev" in terms of rfc-5988) to collections in cases where
* the resource that has the affordance is not the object in a subject-predicate-object statement about each
* collection item. See {@link #rel(TypedResource, String)} for explanation.
*
* @param rev
* to be used as reverse relationship
* @param revertedRel
* to be used in contexts which have no notion of reverse relationships, e.g. HAL
* @param object
* describing the object
* @return builder
*/
public AffordanceBuilder reverseRel(String rev, String revertedRel, TypedResource object) {
this.collectionHolder = object;
this.rels.add(0, revertedRel);
this.reverseRels.add(rev);
return this;
}
/**
* Allows to define one or more link relations for the affordance.
*
* @param rel
* to be used as link relation
* @return builder
*/
public AffordanceBuilder rel(String rel) {
this.rels.add(rel);
return this;
}
/**
* Allows to define one or more link relations for affordances that point to collections in cases where the
* resource
* that has the affordance is not the subject in a subject-predicate-object statement about each collection item.
* E.g. a product might have a loose relationship to ordered items where it can be POSTed, but the ordered items do
* not belong to the product, but to an order. You can express that by saying:
*
* TypedResource order = new TypedResource("http://schema.org/Order"); // holds the ordered items
* Resource<Product> product = new Resource<>(); // has a loose relationship to ordered items
* product.add(linkTo(methodOn(OrderController.class).postOrderedItem()
* .rel(order, "orderedItem")); // order has ordered items, not product has ordered items
*
* If the order doesn't exist yet, it cannot be identified. In that case you may pass null to express that
*
* @param rel
* to be used as link relation
* @param subject
* describing the subject
* @return builder
*/
public AffordanceBuilder rel(TypedResource subject, String rel) {
this.collectionHolder = subject;
this.rels.add(rel);
return this;
}
public AffordanceBuilder withTitle(String title) {
this.linkParams.set("title", title);
return this;
}
public AffordanceBuilder withTitleStar(String titleStar) {
this.linkParams.set("title*", titleStar);
return this;
}
/**
* Allows to define link header params (not UriTemplate variables).
*
* @param name
* of the link header param
* @param value
* of the link header param
* @return builder
*/
public AffordanceBuilder withLinkParam(String name, String value) {
this.linkParams.add(name, value);
return this;
}
public AffordanceBuilder withAnchor(String anchor) {
this.linkParams.set("anchor", anchor);
return this;
}
public AffordanceBuilder withHreflang(String hreflang) {
this.linkParams.add("hreflang", hreflang);
return this;
}
public AffordanceBuilder withMedia(String media) {
this.linkParams.set("media", media);
return this;
}
public AffordanceBuilder withType(String type) {
this.linkParams.set("type", type);
return this;
}
@Override
public AffordanceBuilder slash(Object object) {
if (object == null) {
return this;
}
if (object instanceof Identifiable) {
return slash((Identifiable>) object);
}
String urlPart = object.toString();
// make sure one cannot delete the fragment
if (urlPart.endsWith("#")) {
urlPart = urlPart.substring(0, urlPart.length() - 1);
}
if (!StringUtils.hasText(urlPart)) {
return this;
}
final PartialUriTemplateComponents urlPartComponents = new PartialUriTemplate(urlPart).expand(Collections
.emptyMap());
final PartialUriTemplateComponents affordanceComponents = partialUriTemplateComponents;
final String path = !affordanceComponents.getBaseUri()
.endsWith("/") && !urlPartComponents.getBaseUri()
.startsWith("/") ?
affordanceComponents.getBaseUri() + "/" + urlPartComponents.getBaseUri() :
affordanceComponents.getBaseUri() + urlPartComponents.getBaseUri();
final String queryHead = affordanceComponents.getQueryHead() +
(StringUtils.hasText(urlPartComponents.getQueryHead()) ?
"&" + urlPartComponents.getQueryHead()
.substring(1) :
"");
final String queryTail = affordanceComponents.getQueryTail() +
(StringUtils.hasText(urlPartComponents.getQueryTail()) ?
"," + urlPartComponents.getQueryTail() :
"");
final String fragmentIdentifier = StringUtils.hasText(urlPartComponents.getFragmentIdentifier()) ?
urlPartComponents.getFragmentIdentifier() :
affordanceComponents.getFragmentIdentifier();
List variableNames = new ArrayList();
variableNames.addAll(affordanceComponents.getVariableNames());
variableNames.addAll(urlPartComponents.getVariableNames());
final PartialUriTemplateComponents mergedUriComponents =
new PartialUriTemplateComponents(path, queryHead, queryTail, fragmentIdentifier, variableNames);
return new AffordanceBuilder(mergedUriComponents, actionDescriptors);
}
@Override
public AffordanceBuilder slash(Identifiable> identifiable) {
if (identifiable == null) {
return this;
}
return slash(identifiable.getId());
}
@Override
public URI toUri() {
PartialUriTemplate partialUriTemplate = new PartialUriTemplate(partialUriTemplateComponents.toString());
final String actionLink = partialUriTemplate.stripOptionalVariables(actionDescriptors)
.toString();
if (actionLink == null || actionLink.contains("{")) {
throw new IllegalStateException("cannot convert template to URI");
}
return UriComponentsBuilder.fromUriString(actionLink)
.build()
.toUri();
}
@Override
public Affordance withRel(String rel) {
return rel(rel).build();
}
@Override
public Affordance withSelfRel() {
return rel(Link.REL_SELF).build();
}
@Override
public String toString() {
return partialUriTemplateComponents.toString();
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with scheme tweaked in case the
* request contains an {@code X-Forwarded-Ssl} header, which is not (yet) supported by the underlying
* {@link UriComponentsBuilder}. If no {@link RequestContextHolder} exists (you're outside a Spring Web call), fall
* back to relative URIs.
*
* @return
*/
static UriComponentsBuilder getBuilder() {
if (RequestContextHolder.getRequestAttributes() == null) {
return UriComponentsBuilder.fromPath("/");
}
HttpServletRequest request = getCurrentRequest();
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
// special case handling for X-Forwarded-Ssl:
// apply it, but only if X-Forwarded-Proto is unset.
String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
ForwardedHeader forwarded = ForwardedHeader.of(request.getHeader(ForwardedHeader.NAME));
String proto = hasText(forwarded.getProto()) ? forwarded.getProto() : request.getHeader("X-Forwarded-Proto");
if (!hasText(proto) && hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) {
builder.scheme("https");
}
return builder;
}
/**
* Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110 gets fixed.
*
* @return request
*/
private static HttpServletRequest getCurrentRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Assert.state(requestAttributes != null, "Could not find current request via RequestContextHolder");
Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
return servletRequest;
}
/**
* Adds actionDescriptors of the given AffordanceBuilder to this affordanceBuilder.
*
* @param affordanceBuilder
* whose action descriptors should be added to this one
* @return builder
*/
public AffordanceBuilder and(AffordanceBuilder affordanceBuilder) {
for (ActionDescriptor actionDescriptor : affordanceBuilder.actionDescriptors) {
this.actionDescriptors.add(actionDescriptor);
}
return this;
}
}