From 1f6660766fcdad053fb5597774376f9f2cbf2f95 Mon Sep 17 00:00:00 2001 From: Mark Derricutt Date: Sun, 19 Jul 2015 19:51:01 +1200 Subject: [PATCH] Added support for managed relationship types Representations now track defined Rel instances, which are used by the renderers to control rel specific functionality. The core code now looks at Rel.singleton and prevents duplicate usage. --- pom.xml | 10 ++-- .../DefaultRepresentationFactory.java | 31 +++++++++++-- .../representations/BaseRepresentation.java | 16 ++++--- .../ImmutableRepresentation.java | 4 ++ .../MutableRepresentation.java | 46 ++++++++++++++++++- .../halbuilder/SingleLinksTest.java | 46 +++++++++++++++++++ 6 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 src/test/java/com/theoryinpractise/halbuilder/SingleLinksTest.java diff --git a/pom.xml b/pom.xml index 1d3d628..98a7e2d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.theoryinpractise halbuilder-core - 4.0.4-SNAPSHOT + 4.1.1-SNAPSHOT Java based builder for the Hal specification http://stateless.co/hal_specification.html jar @@ -82,7 +82,7 @@ maven-compiler-plugin - 3.1 + 3.3 maven-release-plugin @@ -103,7 +103,7 @@ com.theoryinpractise halbuilder-api - 4.0.1 + 4.1.1-SNAPSHOT @@ -115,7 +115,7 @@ org.testng testng - 6.8.8 + 6.9.4 test @@ -133,7 +133,7 @@ com.google.guava guava - 17.0 + 18.0 com.google.code.findbugs diff --git a/src/main/java/com/theoryinpractise/halbuilder/DefaultRepresentationFactory.java b/src/main/java/com/theoryinpractise/halbuilder/DefaultRepresentationFactory.java index faa7112..5e38e77 100644 --- a/src/main/java/com/theoryinpractise/halbuilder/DefaultRepresentationFactory.java +++ b/src/main/java/com/theoryinpractise/halbuilder/DefaultRepresentationFactory.java @@ -1,16 +1,18 @@ package com.theoryinpractise.halbuilder; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.theoryinpractise.halbuilder.api.Rel; +import com.theoryinpractise.halbuilder.api.ContentRepresentation; import com.theoryinpractise.halbuilder.api.Link; import com.theoryinpractise.halbuilder.api.Representation; import com.theoryinpractise.halbuilder.api.RepresentationException; import com.theoryinpractise.halbuilder.api.RepresentationFactory; import com.theoryinpractise.halbuilder.api.RepresentationReader; import com.theoryinpractise.halbuilder.api.RepresentationWriter; -import com.theoryinpractise.halbuilder.api.ContentRepresentation; import com.theoryinpractise.halbuilder.impl.ContentType; import com.theoryinpractise.halbuilder.impl.representations.MutableRepresentation; import com.theoryinpractise.halbuilder.impl.representations.NamespaceManager; @@ -29,8 +31,13 @@ public class DefaultRepresentationFactory extends AbstractRepresentationFactory private NamespaceManager namespaceManager = new NamespaceManager(); private List links = Lists.newArrayList(); private Set flags = Sets.newHashSet(); + private Map rels = Maps.newHashMap(); + + public DefaultRepresentationFactory() { + withRel(Rel.singleton("self")); + } - public DefaultRepresentationFactory withRenderer(String contentType, Class> rendererClass) { + public DefaultRepresentationFactory withRenderer(String contentType, Class> rendererClass) { contentRenderers.put(new ContentType(contentType), rendererClass); return this; } @@ -46,7 +53,16 @@ public DefaultRepresentationFactory withNamespace(String namespace, String href) return this; } - @Override + @Override + public RepresentationFactory withRel(Rel rel) { + if (rels.containsKey(rel.rel())) { + throw new IllegalStateException(String.format("Rel %s is already declared.", rel.rel())); + } + rels.put(rel.rel(), rel); + return this; + } + + @Override public DefaultRepresentationFactory withLink(String rel, String href) { links.add(new Link(this, rel, href)); return this; @@ -77,6 +93,11 @@ public Representation newRepresentation(String href) { representation.withNamespace(entry.getKey(), entry.getValue()); } + // Add factorry standard rels + for (Rel rel : rels.values()) { + representation.withRel(rel); + } + // Add factory standard links for (Link link : links) { representation.withLink(link.getRel(), link.getHref(), link.getName(), link.getTitle(), link.getHreflang(), link.getProfile()); @@ -121,4 +142,8 @@ public Set getFlags() { return ImmutableSet.copyOf(flags); } + public Map getRels() { + return ImmutableMap.copyOf(rels); + } + } diff --git a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/BaseRepresentation.java b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/BaseRepresentation.java index 7110d86..810e8d9 100644 --- a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/BaseRepresentation.java +++ b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/BaseRepresentation.java @@ -17,6 +17,7 @@ import com.google.common.collect.Ordering; import com.google.common.collect.Table; import com.theoryinpractise.halbuilder.AbstractRepresentationFactory; +import com.theoryinpractise.halbuilder.api.Rel; import com.theoryinpractise.halbuilder.api.Contract; import com.theoryinpractise.halbuilder.api.Link; import com.theoryinpractise.halbuilder.api.ReadableRepresentation; @@ -58,6 +59,7 @@ public int compare(Link l1, Link l2) { protected NamespaceManager namespaceManager = new NamespaceManager(); + protected Map rels = Maps.newHashMap(); protected List links = Lists.newArrayList(); protected Map properties = Maps.newTreeMap(usingToString()); protected Multimap resources = ArrayListMultimap.create(); @@ -298,17 +300,17 @@ public void toString(String contentType, Set flags, Writer writer) { representationWriter.write(this, uriBuilder.build(), writer); } - @Override - public String toString(String contentType, URI... flags) { - return toString(contentType, ImmutableSet.copyOf(flags)); - } + @Override + public String toString(String contentType, URI... flags) { + return toString(contentType, ImmutableSet.copyOf(flags)); + } - @Override - public void toString(String contentType, Writer writer, URI... flags) { + @Override + public void toString(String contentType, Writer writer, URI... flags) { toString(contentType, ImmutableSet.copyOf(flags), writer); } - @Override + @Override public int hashCode() { int h = namespaceManager.hashCode(); h += links.hashCode(); diff --git a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/ImmutableRepresentation.java b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/ImmutableRepresentation.java index 50a3211..635172a 100644 --- a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/ImmutableRepresentation.java +++ b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/ImmutableRepresentation.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableMultimap; import com.theoryinpractise.halbuilder.AbstractRepresentationFactory; +import com.theoryinpractise.halbuilder.api.Rel; import com.theoryinpractise.halbuilder.api.Link; import com.theoryinpractise.halbuilder.api.ReadableRepresentation; @@ -34,4 +35,7 @@ public Link getResourceLink() { } + public Map getRels() { + return rels; + } } diff --git a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/MutableRepresentation.java b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/MutableRepresentation.java index eafe14d..a373f51 100644 --- a/src/main/java/com/theoryinpractise/halbuilder/impl/representations/MutableRepresentation.java +++ b/src/main/java/com/theoryinpractise/halbuilder/impl/representations/MutableRepresentation.java @@ -1,7 +1,13 @@ package com.theoryinpractise.halbuilder.impl.representations; +import com.google.common.collect.ImmutableMap; import com.theoryinpractise.halbuilder.AbstractRepresentationFactory; -import com.theoryinpractise.halbuilder.api.*; +import com.theoryinpractise.halbuilder.api.Rel; +import com.theoryinpractise.halbuilder.api.Link; +import com.theoryinpractise.halbuilder.api.ReadableRepresentation; +import com.theoryinpractise.halbuilder.api.Representable; +import com.theoryinpractise.halbuilder.api.Representation; +import com.theoryinpractise.halbuilder.api.RepresentationException; import com.theoryinpractise.halbuilder.impl.api.Support; import java.beans.BeanInfo; @@ -12,6 +18,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.net.URI; +import java.util.Map; import static java.lang.String.format; @@ -28,7 +35,27 @@ public MutableRepresentation(AbstractRepresentationFactory representationFactory super(representationFactory); } + /** + * Define rel semantics for this representation + * @param rel A defined relationship type + */ + public MutableRepresentation withRel(Rel rel) { + if (rels.containsKey(rel.rel())) { + throw new IllegalStateException(String.format("Rel %s is already declared.", rel.rel())); + } + rels.put(rel.rel(), rel); + return this; + } + /** + * Retrieve the defined rel semantics for this representation + * @return + */ + public Map getRels() { + return ImmutableMap.copyOf(rels); + } + + /** * Add a link to this resource * * @param rel @@ -48,11 +75,23 @@ public MutableRepresentation withLink(String rel, String href) { */ public MutableRepresentation withLink(String rel, String href, String name, String title, String hreflang, String profile) { Support.checkRelType(rel); + + validateSingletonRel(rel); + links.add(new Link(representationFactory, rel, href, name, title, hreflang, profile)); return this; } - /** + private void validateSingletonRel(String rel) { + if (rels.containsKey(rel)) { + // Rel is resisted, check for duplicate singleton + if (rels.get(rel).isSingleton() && (!getLinksByRel(rel).isEmpty() || !getResourcesByRel(rel).isEmpty())) { + throw new IllegalStateException(String.format("%s is registered as a single rel and already exists.", rel)); + } + } + } + + /** * Add a link to this resource * * @param rel @@ -135,6 +174,9 @@ public Representation withNamespace(String namespace, String href) { public MutableRepresentation withRepresentation(String rel, ReadableRepresentation resource) { Support.checkRelType(rel); + + validateSingletonRel(rel); + resources.put(rel, resource); // Propagate null property flag to parent. if (resource.hasNullProperties()) { diff --git a/src/test/java/com/theoryinpractise/halbuilder/SingleLinksTest.java b/src/test/java/com/theoryinpractise/halbuilder/SingleLinksTest.java new file mode 100644 index 0000000..81e8767 --- /dev/null +++ b/src/test/java/com/theoryinpractise/halbuilder/SingleLinksTest.java @@ -0,0 +1,46 @@ +package com.theoryinpractise.halbuilder; + +import com.theoryinpractise.halbuilder.api.Rel; +import org.testng.annotations.Test; + +import static org.fest.assertions.api.Fail.fail; + +public class SingleLinksTest { + + @Test + public void testDuplicateSingleLinksFails() { + + try { + new DefaultRepresentationFactory() + .withRel(Rel.singleton("bar")) + .newRepresentation("/foo") + .withLink("bar", "/bar") + .withLink("bar", "/bar"); + + fail("This should have failed with an InvalidStateException.)"); + + } catch (IllegalStateException exected) { + // + } + } + + @Test + public void testDuplicateSingleEmbedFails() { + + try { + final DefaultRepresentationFactory factory = new DefaultRepresentationFactory(); + + factory + .withRel(Rel.singleton("bar")) + .newRepresentation("/foo") + .withRepresentation("bar", factory.newRepresentation().withProperty("id", 1)) + .withRepresentation("bar", factory.newRepresentation().withProperty("id", 1)); + + fail("This should have failed with an InvalidStateException.)"); + + } catch (IllegalStateException exected) { + // + } + } + +}