diff --git a/pom.xml b/pom.xml
index eaaf7b16..c3fabd3d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,8 @@
1.9.102.1.11.0
+ 0.8.1
+ 1.1.1true
@@ -132,6 +134,13 @@
${jaxrs.version}true
+
+
+ com.jayway.jsonpath
+ json-path
+ ${jsonpath.version}
+ true
+ org.hamcrest
diff --git a/src/main/java/org/springframework/hateoas/LinkDiscoverer.java b/src/main/java/org/springframework/hateoas/LinkDiscoverer.java
new file mode 100644
index 00000000..0a2a38f7
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/LinkDiscoverer.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012 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.io.InputStream;
+import java.util.List;
+
+/**
+ * Interface to allow discovering links by relation type from some source.
+ *
+ * @author Oliver Gierke
+ */
+public interface LinkDiscoverer {
+
+ /**
+ * Finds a single link with the given relation type in the given {@link String} representation.
+ *
+ * @param rel must not be {@literal null} or empty.
+ * @param representation must not be {@literal null} or empty.
+ * @return the first link with the given relation type found, or {@literal null} if none was found.
+ */
+ Link findLinkWithRel(String rel, String representation);
+
+ /**
+ * Finds a single link with the given relation type in the given {@link InputStream} representation.
+ *
+ * @param rel must not be {@literal null} or empty.
+ * @param representation must not be {@literal null} or empty.
+ * @return the first link with the given relation type found, or {@literal null} if none was found.
+ */
+ Link findLinkWithRel(String rel, InputStream representation);
+
+ /**
+ * Returns all links with the given relation type found in the given {@link String} representation.
+ *
+ * @param rel must not be {@literal null} or empty.
+ * @param representation must not be {@literal null} or empty.
+ * @return
+ */
+ List findLinksWithRel(String rel, String representation);
+
+ /**
+ * Returns all links with the given relation type found in the given {@link InputStream} representation.
+ *
+ * @param rel must not be {@literal null} or empty.
+ * @param representation must not be {@literal null} or empty.
+ * @return
+ */
+ List findLinksWithRel(String rel, InputStream representation);
+}
diff --git a/src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java
new file mode 100644
index 00000000..ef68bc4e
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012 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.core;
+
+/**
+ * {@link JsonPathLinkDiscoverer} based on the default link representation we expose.
+ *
+ * @author Oliver Gierke
+ */
+public class DefaultLinkDiscoverer extends JsonPathLinkDiscoverer {
+
+ /**
+ * Creates a new {@link DefaultLinkDiscoverer}.
+ */
+ public DefaultLinkDiscoverer() {
+ super("$links[?(@.rel == '%s')].href");
+ }
+}
diff --git a/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java
new file mode 100644
index 00000000..493af2d0
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2012 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.core;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import net.minidev.json.JSONArray;
+
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.LinkDiscoverer;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.jayway.jsonpath.JsonPath;
+
+/**
+ * {@link LinkDiscoverer} that uses {@link JsonPath} to find links inside a representation.
+ *
+ * @author Oliver Gierke
+ */
+public class JsonPathLinkDiscoverer implements LinkDiscoverer {
+
+ private final String pathTemplate;
+
+ /**
+ * Creates a new {@link JsonPathLinkDiscoverer} using the given path template. The template has to contain a single
+ * {@code %s} placeholder which will be replaced by the relation type.
+ *
+ * @param pathTemplate must not be {@literal null} or empty and contain a single placeholder.
+ */
+ public JsonPathLinkDiscoverer(String pathTemplate) {
+
+ Assert.hasText(pathTemplate, "Path template must not be null!");
+ Assert.isTrue(StringUtils.countOccurrencesOf(pathTemplate, "%s") == 1,
+ "Path template must contain a single placeholder!");
+ this.pathTemplate = pathTemplate;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(java.lang.String, java.lang.String)
+ */
+ @Override
+ public Link findLinkWithRel(String rel, String representation) {
+
+ List links = findLinksWithRel(rel, representation);
+ return links.isEmpty() ? null : links.get(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(java.lang.String, java.io.InputStream)
+ */
+ @Override
+ public Link findLinkWithRel(String rel, InputStream representation) {
+
+ List links = findLinksWithRel(rel, representation);
+ return links.isEmpty() ? null : links.get(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(java.lang.String, java.lang.String)
+ */
+ @Override
+ public List findLinksWithRel(String rel, String representation) {
+
+ Object parseResult = getExpression(rel).read(representation);
+ return createLinksFrom(parseResult, rel);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(java.lang.String, java.io.InputStream)
+ */
+ @Override
+ public List findLinksWithRel(String rel, InputStream representation) {
+
+ try {
+ Object parseResult = getExpression(rel).read(representation);
+ return createLinksFrom(parseResult, rel);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the {@link JsonPath} to find links with the given relation type.
+ *
+ * @param rel
+ * @return
+ */
+ private JsonPath getExpression(String rel) {
+ return JsonPath.compile(String.format(pathTemplate, rel));
+ }
+
+ /**
+ * Creates {@link Link} instances from the given parse result.
+ *
+ * @param parseResult the result originating from parsing the source content using the JSON path expression.
+ * @param rel the relation type that was parsed for.
+ * @return
+ */
+ private List createLinksFrom(Object parseResult, String rel) {
+
+ if (parseResult instanceof JSONArray) {
+
+ List links = new ArrayList();
+ JSONArray array = (JSONArray) parseResult;
+
+ for (Object element : array) {
+ links.add(new Link(element.toString(), rel));
+ }
+
+ return Collections.unmodifiableList(links);
+ }
+
+ Link link = new Link(parseResult.toString(), rel);
+ return Collections.unmodifiableList(Arrays.asList(link));
+ }
+}
diff --git a/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java
new file mode 100644
index 00000000..4f770468
--- /dev/null
+++ b/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2012 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.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import org.junit.Test;
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.LinkDiscoverer;
+
+/**
+ * Unit tests for {@link DefaultLinkDiscoverer}.
+ *
+ * @author Oliver Gierke
+ */
+public class DefaultLinkDiscovererUnitTest {
+
+ static final String SAMPLE = "{ links : [ " + //
+ "{ rel : 'self', href : 'selfHref' }, " + //
+ "{ rel : 'relation', href : 'firstHref' }, " + //
+ "{ rel : 'relation', href : 'secondHref' }]}";
+
+ static final LinkDiscoverer discoverer = new DefaultLinkDiscoverer();
+
+ @Test
+ public void findsSingleLink() {
+
+ assertThat(discoverer.findLinkWithRel("self", SAMPLE), is(new Link("selfHref")));
+
+ List links = discoverer.findLinksWithRel("self", SAMPLE);
+ assertThat(links, hasSize(1));
+ assertThat(links, hasItem(new Link("selfHref")));
+ }
+
+ @Test
+ public void findsFirstLink() {
+
+ assertThat(discoverer.findLinkWithRel("relation", SAMPLE), is(new Link("firstHref", "relation")));
+ }
+
+ @Test
+ public void findsAllLinks() {
+
+ List links = discoverer.findLinksWithRel("relation", SAMPLE);
+ assertThat(links, hasSize(2));
+ assertThat(links, hasItems(new Link("firstHref", "relation"), new Link("secondHref", "relation")));
+ }
+
+ @Test
+ public void returnsForInexistingLink() {
+ assertThat(discoverer.findLinkWithRel("something", SAMPLE), is(nullValue()));
+ }
+
+ @Test
+ public void returnsForInxistingLinkFromInputStream() throws Exception {
+
+ InputStream inputStream = new ByteArrayInputStream(SAMPLE.getBytes("UTF-8"));
+ assertThat(discoverer.findLinkWithRel("something", inputStream), is(nullValue()));
+ }
+}
diff --git a/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTests.java b/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTests.java
new file mode 100644
index 00000000..dfda42e9
--- /dev/null
+++ b/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTests.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012 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.core;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link JsonPathLinkDiscoverer}.
+ *
+ * @author Oliver Gierke
+ */
+public class JsonPathLinkDiscovererUnitTests {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsNullPattern() {
+ new JsonPathLinkDiscoverer(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsPatternWithWithoutPlaceholder() {
+ new JsonPathLinkDiscoverer("$links");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsPatternWithMultiplePlaceholders() {
+ new JsonPathLinkDiscoverer("$links%s%s");
+ }
+}
diff --git a/template.mf b/template.mf
index 3b056f9a..252ef0cb 100644
--- a/template.mf
+++ b/template.mf
@@ -4,7 +4,9 @@ Bundle-Vendor: SpringSource
Bundle-ManifestVersion: 2
Import-Template:
com.fasterxml.jackson.*;version="${jackson2.version:[=.=.=,+1.0.0)}";resolution:=optional,
+ com.jayway.jsonpath.*;version="${jsonpath.version:[=.=.=,+1.0.0)}";resolution:=optional,
javax.ws.rs.*;version="${jaxrs.version:[=.=.=,+1.0.0)}";resolution:=optional,
javax.xml.bind.*;version="0",
+ net.minidev.json.*;version="${minidevjson.version:[=.=.=,+1.0.0)}";resolution:=optional,
org.springframework.*;version="${spring.version:[=.=.=,+1.0.0)}";resolution:=optional,
org.codehaus.jackson.*;version="${jackson1.version:[=.=.=,+1.0.0)}";resolution:=optional