Introduce API evolution

Show, through an old/new client/server how to implement backwards compatibility using Spring HATEOAS, thereby eliminating the need to version between domain object changes.
This commit is contained in:
Greg Turnquist
2017-05-30 12:04:13 -05:00
parent a423412ed5
commit fa3940690b
36 changed files with 1261 additions and 10 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.hateoas;
/**
* Analogous to {@link ResourceAssembler} but for resource collections.
*
* @author Greg Turnquist
*/
public interface ResourcesAssembler<T, D extends ResourceSupport> {
/**
* Converts all given entities into resources and wraps the collection as a resource as well.
*
* @see ResourceAssembler#toResource(Object)
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@link Resource} of {@code T}.
*/
Resources<D> toResources(Iterable<? extends T> entities);
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.hateoas;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import org.springframework.core.GenericTypeResolver;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
/**
* @author Greg Turnquist
*/
public class SimpleIdentifiableResourceAssembler<T extends Identifiable<?>> extends SimpleResourceAssembler<T> {
/**
* The Spring MVC class for the {@link Identifiable} from which links will be built.
*/
private final Class<?> controllerClass;
/**
* A {@link RelProvider} to look up names of links as options for resource paths.
*/
private final RelProvider relProvider;
/**
* A {@link Class} depicting the {@link Identifiable}'s type.
*/
private final Class<?> resourceType;
/**
* Default base path as empty.
*/
private String basePath = "";
/**
* Default a assembler based on Spring MVC controller, resource type, and {@link RelProvider}. With this combination
* of information, resources can be defined.
*
* @see #setBasePath(String) to adjust base path to something like "/api"/
*
* @param controllerClass - Spring MVC controller to base links off of
* @param relProvider
*/
public SimpleIdentifiableResourceAssembler(Class<?> controllerClass, RelProvider relProvider) {
this.controllerClass = controllerClass;
this.relProvider = relProvider;
// Find the "T" type contained in "T extends Identifiable<?>", e.g. SimpleIdentifiableResourceAssembler<User> -> User
this.resourceType = GenericTypeResolver.resolveTypeArgument(this.getClass(), SimpleIdentifiableResourceAssembler.class);
}
/**
* Alternate constructor that falls back to {@link EvoInflectorRelProvider}.
*
* @param controllerClass
*/
public SimpleIdentifiableResourceAssembler(Class<?> controllerClass) {
this(controllerClass, new EvoInflectorRelProvider());
}
/**
* Define links to add to every {@link Resource}.
*
* @param resource
*/
@Override
protected void addLinks(Resource<T> resource) {
resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel());
resource.add(getCollectionLinkBuilder().withRel(this.relProvider.getCollectionResourceRelFor(this.resourceType)));
}
/**
* Define links to add to {@link Resources} collection.
*
* @param resources
*/
@Override
protected void addLinks(Resources<Resource<T>> resources) {
resources.add(getCollectionLinkBuilder().withSelfRel());
}
/**
* Build up a URI for the collection using the Spring MVC controller followed by the resource type transformed
* by the {@link RelProvider}.
*
* Assumption is that an {@link org.springframework.hateoas.examples.EmployeeController} serving up {@link org.springframework.hateoas.examples.Employee}
* objects will be serving resources at {@code /employees} and {@code /employees/1}.
*
* If this is not the case, simply override this method in your concrete instance, or simply resort to
* overriding {@link #addLinks(Resource)} and {@link #addLinks(Resources)} where you have full control over exactly
* what links are put in the individual and collection resources.
*
* @return
*/
protected LinkBuilder getCollectionLinkBuilder() {
ControllerLinkBuilder linkBuilder = linkTo(this.controllerClass);
for (String pathComponent : (getPrefix() + this.relProvider.getCollectionResourceRelFor(this.resourceType)).split("/")) {
if (!pathComponent.isEmpty()) {
linkBuilder = linkBuilder.slash(pathComponent);
}
}
return linkBuilder;
}
private String getPrefix() {
return getBasePath().isEmpty() ? "" : getBasePath() + "/";
}
public String getBasePath() {
return this.basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017 the original author or authors.
*
* 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.springframework.hateoas;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.Assert;
/**
* A {@link ResourceAssembler}/{@link ResourcesAssembler} that focuses purely on the domain type,
* returning back {@link Resource} and {@link Resources} for that type instead of
* {@link org.springframework.hateoas.ResourceSupport}.
*
* @author Greg Turnquist
*/
public class SimpleResourceAssembler<T> implements ResourceAssembler<T, Resource<T>>, ResourcesAssembler<T, Resource<T>> {
/**
* Converts the given entity into a {@link Resource}.
*
* @param entity
* @return
*/
@Override
public Resource<T> toResource(T entity) {
Resource<T> resource = new Resource<T>(entity);
addLinks(resource);
return resource;
}
/**
* Converts all given entities into resources and wraps the collection as a resource as well.
*
* @see #toResource(Object)
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@link Resource} of {@code T}.
*/
public Resources<Resource<T>> toResources(Iterable<? extends T> entities) {
Assert.notNull(entities, "Entities must not be null!");
List<Resource<T>> result = new ArrayList<Resource<T>>();
for (T entity : entities) {
result.add(toResource(entity));
}
Resources<Resource<T>> resources = new Resources<>(result);
addLinks(resources);
return resources;
}
/**
* Define links to add to every individual {@link Resource}.
*
* @param resource
*/
protected void addLinks(Resource<T> resource) {
// Default adds no links
}
/**
* Define links to add to the {@link Resources} collection.
*
* @param resources
*/
protected void addLinks(Resources<Resource<T>> resources) {
// Default adds no links.
}
}