diff --git a/pom.xml b/pom.xml index eaaf7b16..c3fabd3d 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,8 @@ 1.9.10 2.1.1 1.0 + 0.8.1 + 1.1.1 true @@ -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