diff --git a/pom.xml b/pom.xml index 20378e0..c3a47ce 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ black.door hate - v1r0t3 + v1r1t0 diff --git a/src/main/java/black/door/hate/CannotEmbedLinkException.java b/src/main/java/black/door/hate/CannotEmbedLinkException.java new file mode 100644 index 0000000..8b77737 --- /dev/null +++ b/src/main/java/black/door/hate/CannotEmbedLinkException.java @@ -0,0 +1,10 @@ +package black.door.hate; + +/** + * Created by nfischer on 1/31/2016. + */ +public class CannotEmbedLinkException extends RuntimeException { + public CannotEmbedLinkException(String fieldName){ + super(fieldName + " is a link and cannot be embedded as a resource."); + } +} diff --git a/src/main/java/black/door/hate/HalLink.java b/src/main/java/black/door/hate/HalLink.java index 8c66db0..8f89c78 100644 --- a/src/main/java/black/door/hate/HalLink.java +++ b/src/main/java/black/door/hate/HalLink.java @@ -14,7 +14,7 @@ @Builder @Getter @JsonInclude(JsonInclude.Include.NON_NULL) -public class HalLink { +public class HalLink implements LinkOrResource{ @NonNull private URI href; @@ -25,4 +25,9 @@ public class HalLink { private URI profile; private String title; private String hreflang; + + @Override + public HalLink asLink() { + return this; + } } diff --git a/src/main/java/black/door/hate/HalRepresentation.java b/src/main/java/black/door/hate/HalRepresentation.java index fb74bfd..5e46abb 100644 --- a/src/main/java/black/door/hate/HalRepresentation.java +++ b/src/main/java/black/door/hate/HalRepresentation.java @@ -11,7 +11,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -19,6 +18,9 @@ import static black.door.hate.Constants._links; import static black.door.util.Misc.require; +import static java.util.stream.Collectors.*; +import static java.util.Map.Entry; + /** * Created by nfischer on 12/8/2015. */ @@ -32,18 +34,18 @@ public class HalRepresentation implements java.io.Serializable { WRITER = mapper.writer(); } - private final Map links; - private final Map> multiLinks; - private final Map embedded; - private final Map> multiEmbedded; + private final Map links; + private final Map> multiLinks; + private final Map embedded; + private final Map> multiEmbedded; private final Map properties; HalRepresentation( - Map links, - Map> multiLinks, - Map embedded, - Map> multiEmbedded, - Map properties) { + Map links, + Map> multiLinks, + Map embedded, + Map> multiEmbedded, + Map properties) { require(null != links); require(null != multiLinks); require(null != embedded); @@ -105,19 +107,46 @@ public void serialize(HalRepresentation halRepresentation, throws IOException{ jsonGenerator.writeStartObject(); + //write all properties to json for(Map.Entry e :halRepresentation.properties.entrySet()){ jsonGenerator.writeObjectField(e.getKey(), e.getValue()); } + //map links from LinkOrResources to HalLinks + Map linkz = halRepresentation.getLinks().entrySet() + .stream() + .collect(toMap(Entry::getKey, e -> e.getValue().asLink())); + Map> multiLinkz = halRepresentation.getMultiLinks().entrySet() + .stream() + .collect(toMap(Entry::getKey, e -> e.getValue() + .stream() + .map(LinkOrResource::asLink) + .collect(toList()) + )); + + //put all links and collections of links together in one object Map links = new HashMap<>(); - links.putAll(halRepresentation.links); - links.putAll(halRepresentation.multiLinks); + links.putAll(linkz); + links.putAll(multiLinkz); if(!links.isEmpty()) jsonGenerator.writeObjectField(_links, links); + //map all HalResources to HalRepresentations + Map embeddz = halRepresentation.getEmbedded().entrySet() + .stream() + .collect(toMap(Entry::getKey, e -> e.getValue().asEmbedded())); + Map> multiEmbeddz = halRepresentation.getMultiEmbedded().entrySet() + .stream() + .collect(toMap(Entry::getKey, e -> e.getValue() + .stream() + .map(HalResource::asEmbedded) + .collect(toList()) + )); + + //put all embedded resources and collections of embedded resources into one object Map embedded = new HashMap<>(); - embedded.putAll(halRepresentation.embedded); - embedded.putAll(halRepresentation.multiEmbedded); + embedded.putAll(embeddz); + embedded.putAll(multiEmbeddz); if(!embedded.isEmpty()) jsonGenerator.writeObjectField(_embedded, embedded); @@ -126,10 +155,10 @@ public void serialize(HalRepresentation halRepresentation, } public static class HalRepresentationBuilder{ - private Map links; - private Map> multiLinks; - private Map embedded; - private Map> multiEmbedded; + private Map links; + private Map> multiLinks; + private Map embedded; + private Map> multiEmbedded; private Map properties; private boolean ignoreNullProperties = false; @@ -141,6 +170,26 @@ public HalRepresentationBuilder() { properties = new HashMap<>(); } + public void expand(String fieldName){ + if(links.containsKey(fieldName)){ + addEmbedded(fieldName, links.remove(fieldName).asResource().orElseThrow( + () -> new CannotEmbedLinkException(fieldName) + )); + } else if(multiLinks.containsKey(fieldName)){ + try { + addEmbedded(fieldName, multiLinks.remove(fieldName) + .stream() + .map(e -> e.asResource().get()) + .collect(toList()) + ); + }catch (NoSuchElementException e){ + throw new CannotEmbedLinkException(fieldName); + } + } else if (!(embedded.containsKey(fieldName) || multiEmbedded.containsKey(fieldName))) { + throw new NoSuchElementException("There is no linked or embedded resource with the field name " +fieldName); + } + } + /** * Causes any properties with null values added to this builder after this call to be ignored. * Properties with null values added before this call will still be included. @@ -165,64 +214,54 @@ public HalRepresentationBuilder addProperties(JsonNode jax){ return this; } - private void add(String name, HalResource res, Map rs, - Map> multiRs, - Function trans){ + /** + * + * @param either a HalResource or a HalLink + * @param name the field name + * @param res the resource to add to the representation + * @param rs + * @param multiRs + */ + private void add(String name, T res, Map rs, + Map> multiRs){ if(res == null) return; if(multiRs.containsKey(name)){ - multiRs.get(name).add(trans.apply(res)); + multiRs.get(name).add(res); }else if(rs.containsKey(name)){ List ls = new LinkedList<>(); ls.add(rs.remove(name)); - ls.add(trans.apply(res)); + ls.add(res); multiRs.put(name, ls); }else{ - rs.put(name, trans.apply(res)); + rs.put(name, res); } } - private void addMulti(String name, Collection res, - Map> multiRs, - Function trans){ - Collection resource = res == null ? new LinkedList<>() : res; + private void addMulti(String name, + List res, + Map> multiRs){ + List resource = res == null ? new LinkedList<>() : res; Collection links = multiRs.get(name); - List ls = resource.stream() - .map(trans) - .collect(Collectors.toList()); if(links == null) { - multiRs.put(name, ls); + multiRs.put(name, resource); }else{ - links.addAll(ls); + links.addAll(resource); } } public HalRepresentationBuilder addEmbedded(String name, HalResource link){ - add(name, link, embedded, multiEmbedded, HalResource::asEmbedded); - return this; - } - - public HalRepresentationBuilder addEmbedded(String name, Collection link){ - addMulti(name, link, multiEmbedded, HalResource::asEmbedded); + add(name, link, embedded, multiEmbedded); return this; } - public HalRepresentationBuilder addLink(String name, HalResource link){ - add(name, link, links, multiLinks, HalResource::asLink); + public HalRepresentationBuilder addEmbedded(String name, List link){ + addMulti(name, link, multiEmbedded); return this; } - public HalRepresentationBuilder addLink(String name, HalLink link){ - if(multiLinks.containsKey(name)){ - multiLinks.get(name).add(link); - }else if(links.containsKey(name)){ - List ls = new LinkedList<>(); - ls.add(links.remove(name)); - ls.add(link); - multiLinks.put(name, ls); - }else{ - links.put(name, link); - } + public HalRepresentationBuilder addLink(String name, LinkOrResource link){ + add(name, link, links, multiLinks); return this; } @@ -231,8 +270,8 @@ public HalRepresentationBuilder addLink(String name, URI link){ return addLink(name, l); } - public HalRepresentationBuilder addLink(String name, Collection link){ - addMulti(name, link, multiLinks, HalResource::asLink); + public HalRepresentationBuilder addLink(String name, List link){ + addMulti(name, link, multiLinks); return this; } diff --git a/src/main/java/black/door/hate/HalResource.java b/src/main/java/black/door/hate/HalResource.java index 724bb9b..e0193ac 100644 --- a/src/main/java/black/door/hate/HalResource.java +++ b/src/main/java/black/door/hate/HalResource.java @@ -3,12 +3,16 @@ /** * Created by nfischer on 12/8/2015. */ -public interface HalResource extends LocatableResource { - default HalLink asLink(){ - return HalLink.builder() - .href(this.location()) - .build(); +public interface HalResource extends LocatableResource, LinkOrResource { + + default HalRepresentation asEmbedded(String... expand){ + HalRepresentation.HalRepresentationBuilder builder = representationBuilder(); + for(String e : expand){ + builder.expand(e); + } + return builder.build(); } - HalRepresentation asEmbedded(); + HalRepresentation.HalRepresentationBuilder representationBuilder(); + } diff --git a/src/main/java/black/door/hate/LinkOrResource.java b/src/main/java/black/door/hate/LinkOrResource.java new file mode 100644 index 0000000..f2860e3 --- /dev/null +++ b/src/main/java/black/door/hate/LinkOrResource.java @@ -0,0 +1,18 @@ +package black.door.hate; + +import java.util.Optional; + +/** + * Created by nfischer on 1/31/2016. + */ +public interface LinkOrResource { + + HalLink asLink(); + + default Optional asResource(){ + if(this instanceof HalResource){ + return Optional.of((HalResource) this); + }else + return Optional.empty(); + } +} diff --git a/src/main/java/black/door/hate/LocatableResource.java b/src/main/java/black/door/hate/LocatableResource.java index 1b767b0..c10f130 100644 --- a/src/main/java/black/door/hate/LocatableResource.java +++ b/src/main/java/black/door/hate/LocatableResource.java @@ -5,6 +5,12 @@ /** * Created by nfischer on 12/8/2015. */ -public interface LocatableResource { +public interface LocatableResource extends LinkOrResource{ URI location(); + + default HalLink asLink(){ + return HalLink.builder() + .href(this.location()) + .build(); + } } diff --git a/src/test/java/black/door/hate/HalRepresentationTest.java b/src/test/java/black/door/hate/HalRepresentationTest.java index 0461423..c5915fe 100644 --- a/src/test/java/black/door/hate/HalRepresentationTest.java +++ b/src/test/java/black/door/hate/HalRepresentationTest.java @@ -12,14 +12,13 @@ import java.io.IOException; import java.net.URI; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.net.URISyntaxException; +import java.util.*; import java.util.stream.IntStream; import static black.door.util.Misc.list; import static java.util.Arrays.asList; +import static java.util.Arrays.fill; import static org.junit.Assert.*; /** @@ -73,10 +72,36 @@ public void testPagination() throws Exception{ assertEquals(20, rep.getMultiEmbedded().get("orders").size()); assertEquals("/orders/20", - rep.getMultiEmbedded().get("orders").get(0).getLinks().get("self") + rep.getMultiEmbedded().get("orders").get(0).asEmbedded().getLinks().get("self").asLink() .getHref().toASCIIString()); } + @Test + public void testExpand() throws JsonProcessingException, URISyntaxException { + Order o = new Order(1, 1, "USD", "status", new Basket(2), new Customer(3)); + + val builder = o.representationBuilder(); + assertTrue(builder.build().getLinks().containsKey("basket")); + builder.expand("basket"); + assertFalse(builder.build().getLinks().containsKey("basket")); + assertTrue(o.asEmbedded("basket").getEmbedded().containsKey("basket")); + System.out.println(o.asEmbedded("basket").serialize()); + + builder.expand("basket"); + + try{ + builder.expand("shoe"); + fail(); + }catch (NoSuchElementException e){} + + builder.addLink("cars", new URI("/cars")); + + try{ + builder.expand("cars"); + fail(); + }catch (CannotEmbedLinkException e){} + } + @Test public void testNulls() throws Exception{ val basket1 = new Basket(98712); diff --git a/src/test/java/black/door/hate/example/Basket.java b/src/test/java/black/door/hate/example/Basket.java index a1b56e5..a71f60b 100644 --- a/src/test/java/black/door/hate/example/Basket.java +++ b/src/test/java/black/door/hate/example/Basket.java @@ -18,7 +18,9 @@ protected String resName() { } @Override - public HalRepresentation asEmbedded() { - throw new NotImplementedException(); + public HalRepresentation.HalRepresentationBuilder representationBuilder() { + return HalRepresentation.builder() + .addProperty("id", id) + .addLink("self", this); } } diff --git a/src/test/java/black/door/hate/example/Customer.java b/src/test/java/black/door/hate/example/Customer.java index 5ea0224..5c46306 100644 --- a/src/test/java/black/door/hate/example/Customer.java +++ b/src/test/java/black/door/hate/example/Customer.java @@ -18,7 +18,7 @@ protected String resName() { } @Override - public HalRepresentation asEmbedded() { + public HalRepresentation.HalRepresentationBuilder representationBuilder() { throw new NotImplementedException(); } } diff --git a/src/test/java/black/door/hate/example/Order.java b/src/test/java/black/door/hate/example/Order.java index 3694cc3..6a01cbf 100644 --- a/src/test/java/black/door/hate/example/Order.java +++ b/src/test/java/black/door/hate/example/Order.java @@ -27,15 +27,14 @@ public Order(long id, double total, String currency, String status, @Override - public HalRepresentation asEmbedded() { + public HalRepresentation.HalRepresentationBuilder representationBuilder() { return HalRepresentation.builder() .addProperty("total", total) .addProperty("currency", currency) .addProperty("status", status) .addLink("basket", basket) .addLink("customer", customer) - .addLink("self", this) - .build(); + .addLink("self", this); }