├── .gitignore ├── .travis.yml ├── LICENSE-2.0.txt ├── README.md ├── checkstyle-supressions.xml ├── pom.xml └── src ├── it └── settings.xml ├── main └── java │ └── com │ └── theoryinpractise │ └── halbuilder5 │ ├── ContentType.java │ ├── Link.java │ ├── NamespaceManager.java │ ├── Rel.java │ ├── RepresentationException.java │ ├── ResourceRepresentation.java │ ├── Support.java │ └── json │ ├── JsonRepresentationReader.java │ └── JsonRepresentationWriter.java └── test └── java └── com └── theoryinpractise └── halbuilder5 ├── AbstractAccount.java ├── AbstractCurriedNamespaceData.java ├── ContentTypeTest.java ├── JsonSerializedValue.java ├── LinkListSubject.java ├── LinkTest.java ├── NamespaceManagerTest.java ├── RepresentationLinksTest.java ├── ResourceBasicMethodsTest.java ├── ResourceRepresentationTest.java └── SingleLinksTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | *.iml 4 | *.ipr 5 | *.iws 6 | bazel* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | 4 | script: "mvn clean install" 5 | 6 | cache: 7 | directories: 8 | - "$HOME/.m2/repository" 9 | - "$HOME/apache-maven-3.5.0" 10 | 11 | before_install: 12 | - export M2_HOME=$HOME/apache-maven-3.5.0 13 | - if [ ! -d $M2_HOME/bin ]; then curl https://archive.apache.org/dist/maven/maven-3/3.5.0/binaries/apache-maven-3.5.0-bin.tar.gz | tar zxf - -C $HOME; fi 14 | - export PATH=$M2_HOME/bin:$PATH 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Halbuilder is a simple Java API for generating and consuming HAL documents conforming to the 2 | [HAL Specification](http://stateless.co/hal_specification.html). 3 | 4 | [![travis](https://travis-ci.org/HalBuilder/halbuilder-core.svg?branch=develop)](https://travis-ci.org/HalBuilder/halbuilder-core) 5 | [![codecov](https://codecov.io/gh/HalBuilder/halbuilder-core/branch/develop/graph/badge.svg)](https://codecov.io/gh/HalBuilder/halbuilder-core) 6 | 7 | ### Generating Local Resources 8 | 9 | ```java 10 | Map friend = HashMap.of("name", "Mike", "age", 36); 11 | ResourceRepresentation> owner = 12 | ResourceRepresentation.create("http://example.com/mike", friend) 13 | .withLink("td:friend", "http://example.com/mamund") 14 | 15 | Map todoMeta = HashMap.of( 16 | "created_at", "2010-01-16", "updated_at", "2017-06-13", 17 | "summary", "An example list"); 18 | 19 | ResourceRepresentation> halResource = 20 | ResourceRepresentation.create("http://example.com/todo-list", todoMeta) 21 | .withLink("td:search", "/todo-list/search;{searchterm}") 22 | .withLink("td:description", "/todo-list/description") 23 | .withRepresentation("td:owner", owner); 24 | 25 | JsonRepresentationWriter jsonRepresentationWriter = 26 | JsonRepresentationWriter.create(); 27 | 28 | ByteString representation = jsonRepresentationWriter.print(accountRepWithLinks); 29 | System.out.println(representation.utf8()); 30 | ``` 31 | 32 | ### Reading Local Resources 33 | 34 | ```java 35 | JsonRepresentationReader jsonRepresentationReader = 36 | JsonRepresentationReader.create(); 37 | 38 | ResourceRepresentation representation = 39 | jsonRepresentationReader.read( 40 | new InputStreamReader(Some.class.getResourceAsStream("/test.json"))); 41 | 42 | // or as a type 43 | 44 | ResourceRepresentation personRepresentation = 45 | jsonRepresentationReader.read( 46 | new InputStreamReader(Some.class.getResourceAsStream("/test.json")), 47 | Person.class); 48 | 49 | ``` 50 | 51 | ### Apache Maven 52 | 53 | HalBuilder is deployed to Apache Maven Central under the following coordinates: 54 | 55 | ```xml 56 | 57 | com.theoryinpractise 58 | halbuilder5 59 | 5.0.1 60 | 61 | ``` 62 | 63 | ### Website 64 | 65 | More documentation is available from the main website at [gotohal.net](http://www.gotohal.net/). 66 | 67 | ### Development Forum 68 | 69 | Email support and discussion is available on the [development forum](https://groups.google.com/forum/#!forum/halbuilder-dev). 70 | -------------------------------------------------------------------------------- /checkstyle-supressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.theoryinpractise 7 | halbuilder-parent 8 | 1.0.1 9 | 10 | 11 | com.theoryinpractise 12 | halbuilder5 13 | 5.1.4-SNAPSHOT 14 | jar 15 | 16 | halbuilder5 17 | Java based builder for the Hal specification http://stateless.co/hal_specification.html 18 | http://maven.apache.org 19 | 20 | 21 | 3.3.9 22 | 23 | 24 | 25 | UTF-8 26 | checkstyle-supressions.xml 27 | false 28 | 2.9.6 29 | false 30 | 0.50 31 | 9 32 | 33 | 34 | 35 | 36 | 37 | false 38 | 39 | jcenter 40 | bintray 41 | http://jcenter.bintray.com 42 | 43 | 44 | 45 | 46 | 47 | false 48 | 49 | jcenter 50 | bintray-plugins 51 | http://jcenter.bintray.com 52 | 53 | 54 | 55 | 56 | 57 | org.derive4j 58 | derive4j 59 | 0.12.4 60 | provided 61 | 62 | 63 | org.derive4j 64 | derive4j-annotation 65 | 0.12.4 66 | provided 67 | 68 | 69 | org.immutables 70 | value 71 | 2.6.3 72 | provided 73 | 74 | 75 | com.squareup.okio 76 | okio 77 | 1.15.0 78 | 79 | 80 | io.vavr 81 | vavr 82 | 0.9.2 83 | 84 | 85 | com.fasterxml.jackson.core 86 | jackson-core 87 | ${jackson.version} 88 | 89 | 90 | com.fasterxml.jackson.core 91 | jackson-annotations 92 | ${jackson.version} 93 | 94 | 95 | com.fasterxml.jackson.core 96 | jackson-databind 97 | ${jackson.version} 98 | 99 | 100 | com.damnhandy 101 | handy-uri-templates 102 | 2.1.7 103 | test 104 | 105 | 106 | com.google.code.findbugs 107 | jsr305 108 | 3.0.2 109 | 110 | 111 | org.testng 112 | testng 113 | 6.14.3 114 | test 115 | 116 | 117 | com.google.truth 118 | truth 119 | 0.42 120 | test 121 | 122 | 123 | com.jayway.jsonpath 124 | json-path 125 | 2.4.0 126 | test 127 | 128 | 129 | 130 | 131 | 132 | 133 | maven-clean-plugin 134 | 3.0.0 135 | 136 | 137 | maven-install-plugin 138 | 2.5.2 139 | 140 | 141 | maven-deploy-plugin 142 | 2.8.2 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-surefire-plugin 147 | 2.20 148 | 149 | false 150 | 151 | 152 | 153 | io.repaint.maven 154 | tiles-maven-plugin 155 | 2.12 156 | true 157 | 158 | 159 | com.theoryinpractise:halbuilder-styleguide-tile:1.0.13 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/it/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | it-repo 6 | 7 | true 8 | 9 | 10 | 11 | local.central 12 | @localRepositoryUrl@ 13 | 14 | true 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | local.central 24 | @localRepositoryUrl@ 25 | 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/ContentType.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class ContentType { 7 | private String type; 8 | private String subType; 9 | 10 | public ContentType(String contentType) { 11 | Pattern contentTypePattern = Pattern.compile("([\\w|\\*]*)/([^;,\\s]*)"); 12 | Matcher matcher = contentTypePattern.matcher(contentType); 13 | if (matcher.find()) { 14 | type = matcher.group(1); 15 | subType = matcher.group(2); 16 | } 17 | } 18 | 19 | @Override 20 | public int hashCode() { 21 | int result = type.hashCode(); 22 | return 31 * result + subType.hashCode(); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) { 28 | return true; 29 | } 30 | if (o == null || getClass() != o.getClass()) { 31 | return false; 32 | } 33 | 34 | ContentType that = (ContentType) o; 35 | 36 | if (!subType.equals(that.subType)) { 37 | return false; 38 | } 39 | return type.equals(that.type); 40 | } 41 | 42 | public boolean matches(String contentType) { 43 | return matches(new ContentType(contentType)); 44 | } 45 | 46 | public boolean matches(ContentType contentType) { 47 | 48 | if (typeMatches(getType(), contentType.getType())) { 49 | if (typeMatches(getSubType(), contentType.getSubType())) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | private boolean typeMatches(String left, String right) { 58 | return left.equals(right) || "*".equals(right); 59 | } 60 | 61 | public String getType() { 62 | return type; 63 | } 64 | 65 | public String getSubType() { 66 | return subType; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/Link.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.Tuple; 4 | import io.vavr.Tuple2; 5 | import io.vavr.collection.HashMap; 6 | import io.vavr.collection.List; 7 | import io.vavr.collection.Map; 8 | import org.derive4j.ArgOption; 9 | import org.derive4j.Data; 10 | import org.derive4j.Derive; 11 | import org.derive4j.ExportAsPublic; 12 | import org.derive4j.Flavour; 13 | import org.derive4j.Visibility; 14 | 15 | import java.util.regex.Pattern; 16 | 17 | /** A Link to an external resource. */ 18 | @Data( 19 | flavour = Flavour.Vavr, 20 | arguments = ArgOption.checkedNotNull, 21 | value = @Derive(withVisibility = Visibility.Smart)) 22 | public abstract class Link { 23 | 24 | public static final String HREF = "href"; 25 | public static final String HREFLANG = "hreflang"; 26 | public static final String LINK = "link"; 27 | public static final String METHOD = "method"; 28 | public static final String NAME = "name"; 29 | public static final String PROFILE = "profile"; 30 | public static final String REL = "rel"; 31 | public static final String SELF = "self"; 32 | public static final String TEMPLATED = "templated"; 33 | public static final String TITLE = "title"; 34 | 35 | /** Pattern that will hit an RFC 6570 URI template. */ 36 | private static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("\\{.+\\}"); 37 | 38 | interface Cases { 39 | R simple(String rel, String href, Boolean templated); 40 | 41 | R full(String rel, String href, Boolean templated, Map properties); 42 | } 43 | 44 | public abstract R match(Cases cases); 45 | 46 | private List> templateFragement() { 47 | return Links.getTemplated(this) ? List.of(Tuple.of(TEMPLATED, "true")) : List.empty(); 48 | } 49 | 50 | private List> generateLinkFragments() { 51 | List> linkFragments = 52 | Links.cases() 53 | .simple((rel, href, templated) -> List.of(Tuple.of(REL, rel), Tuple.of(HREF, href))) 54 | .full( 55 | (rel, href, templated, properties) -> 56 | List.of(Tuple.of(REL, rel), Tuple.of(HREF, href)).appendAll(properties)) 57 | .apply(this); 58 | 59 | return linkFragments.appendAll(templateFragement()); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return " String.format("%s=\"%s\"", it._1, it._2)).mkString(" ") 66 | + "/>"; 67 | } 68 | 69 | @Override 70 | public abstract boolean equals(Object o); 71 | 72 | @Override 73 | public abstract int hashCode(); 74 | 75 | private static boolean isTemplated(String href) { 76 | return (href != null) && URI_TEMPLATE_PATTERN.matcher(href).find(); 77 | } 78 | 79 | @ExportAsPublic 80 | static Link create(String rel, String href, String... properties) { 81 | if (properties.length % 2 != 0) { 82 | throw new IllegalArgumentException("Parameter count must be even"); 83 | } 84 | 85 | Map propertyMap = HashMap.empty(); 86 | for (int i = 0; i < properties.length; i = i + 2) { 87 | String key = properties[i]; 88 | String value = properties[i + 1]; 89 | propertyMap = propertyMap.put(key, value); 90 | } 91 | return create(rel, href, propertyMap); 92 | } 93 | 94 | @ExportAsPublic 95 | static Link create(String rel, String href, Map properties) { 96 | return Links.full0(rel, href, isTemplated(href), properties); 97 | } 98 | 99 | @ExportAsPublic 100 | static Link create(String rel, String href, java.util.Map properties) { 101 | return Links.full0(rel, href, isTemplated(href), HashMap.ofAll(properties)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/NamespaceManager.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.Tuple2; 4 | import io.vavr.collection.Map; 5 | import io.vavr.collection.TreeMap; 6 | import io.vavr.control.Either; 7 | 8 | import static java.lang.String.format; 9 | 10 | /** 11 | * The NamespaceManager contains a mapping between CURIE prefixes and their associated HREF's, this 12 | * class is now backed by a persistent TreeMap and also operates as a persistent data structure. 13 | * Adding new namespaces DOES NOT mutate the existing instance. 14 | */ 15 | public class NamespaceManager { 16 | 17 | public static final NamespaceManager EMPTY = new NamespaceManager(TreeMap.empty()); 18 | private final Map namespaces; 19 | 20 | private NamespaceManager(final Map namespaces) { 21 | this.namespaces = namespaces; 22 | } 23 | 24 | public Map getNamespaces() { 25 | return namespaces; 26 | } 27 | 28 | /** 29 | * Update the list of declared namespaces with a new namespace. 30 | * 31 | * @param namespace Namespace curie identifier 32 | * @param href Namesapce URL 33 | * @return A new instance of the namespace manager with the additional namespace. 34 | */ 35 | public NamespaceManager withNamespace(String namespace, String href) { 36 | if (namespaces.containsKey(namespace)) { 37 | throw new RepresentationException( 38 | format("Duplicate namespace '%s' found for representation factory", namespace)); 39 | } 40 | if (!href.contains("{rel}")) { 41 | throw new RepresentationException( 42 | format("Namespace '%s' does not include {rel} URI template argument.", namespace)); 43 | } 44 | return new NamespaceManager(namespaces.put(namespace, href)); 45 | } 46 | 47 | public void validateNamespaces(String rel) { 48 | if (!rel.contains("://") && rel.contains(":")) { 49 | String[] relPart = rel.split(":"); 50 | if (!namespaces.containsKey(relPart[0])) { 51 | throw new RepresentationException( 52 | format("Undeclared namespace in rel %s for resource", rel)); 53 | } 54 | } 55 | } 56 | 57 | public String currieHref(String href) { 58 | for (Tuple2 entry : namespaces.toStream()) { 59 | 60 | String nsRef = entry._2; 61 | int startIndex = nsRef.indexOf("{rel}"); 62 | int endIndex = startIndex + 5; 63 | 64 | String left = nsRef.substring(0, startIndex); 65 | String right = nsRef.substring(endIndex); 66 | 67 | if (href.startsWith(left) && href.endsWith(right)) { 68 | return entry._1 + ":" + href.substring(startIndex, endIndex - 2); 69 | } 70 | } 71 | return href; 72 | } 73 | 74 | public Either resolve(String ns) { 75 | if (!ns.contains(":")) { 76 | return Either.left( 77 | new RepresentationException("Namespaced value does not include : - not namespaced?")); 78 | } 79 | 80 | String[] parts = ns.split(":"); 81 | String prefix = parts[0]; 82 | String suffix = parts[1]; 83 | 84 | return Either.right( 85 | namespaces 86 | .get(prefix) 87 | .map(curry -> curry.replace("{rel}", suffix)) 88 | .getOrElse( 89 | () -> { 90 | throw new RepresentationException("Unknown namespace key: " + prefix); 91 | })); 92 | } 93 | 94 | @Override 95 | public int hashCode() { 96 | return namespaces != null ? namespaces.hashCode() : 0; 97 | } 98 | 99 | @Override 100 | public boolean equals(Object o) { 101 | if (this == o) { 102 | return true; 103 | } 104 | if (o == null || getClass() != o.getClass()) { 105 | return false; 106 | } 107 | 108 | NamespaceManager that = (NamespaceManager) o; 109 | 110 | if (namespaces != null ? !namespaces.equals(that.namespaces) : that.namespaces != null) { 111 | return false; 112 | } 113 | 114 | return true; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/Rel.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.derive4j.ArgOption; 4 | import org.derive4j.Data; 5 | import org.derive4j.Derive; 6 | import org.derive4j.Flavour; 7 | import org.derive4j.Make; 8 | 9 | import java.util.Comparator; 10 | 11 | /** Rel defines the base class of a Algebraic Data Type for relationship semantics. */ 12 | @Data( 13 | flavour = Flavour.Vavr, 14 | arguments = ArgOption.checkedNotNull, 15 | value = @Derive(make = {Make.constructors, Make.getters, Make.casesMatching})) 16 | public abstract class Rel { 17 | 18 | @Override 19 | public abstract String toString(); 20 | 21 | @Override 22 | public abstract boolean equals(Object o); 23 | 24 | @Override 25 | public abstract int hashCode(); 26 | 27 | public abstract R match(Cases cases); 28 | 29 | public String rel() { 30 | return Rels.getRel(this); 31 | } 32 | 33 | /** 34 | * The data type covers three separate cases: singleton, natural, and sorted. 35 | * 36 | * @param The return type used in the various derive4j generated mapping functions. 37 | */ 38 | interface Cases { 39 | 40 | /** 41 | * `singleton` relationships are checked for uniqueness, and render directly as an object ( 42 | * rather than array of objects ) when rendered as JSON. 43 | * 44 | * @param rel The relationship type 45 | */ 46 | R singleton(String rel); 47 | 48 | /** 49 | * `natural` relationships are rendered in natural order, and are rendered as a list of objects, 50 | * or a coalesced into a single object. 51 | * 52 | * @param rel The relationship type 53 | */ 54 | R natural(String rel); 55 | 56 | /** 57 | * `collection` relationships are rendered in natural order, and are ALWAYS rendered as a list 58 | * of objects. 59 | * 60 | * @param rel The relationship type 61 | */ 62 | R collection(String rel); 63 | 64 | /** 65 | * `sorted` relationships are rendered in the order mandated by the associated `Comparator` and 66 | * are rendered as a list of objects. 67 | * 68 | * @param rel The relationship type 69 | * @param id An identifier to associate with the sorting technique used. 70 | * @param comparator The comparator to use when sorting representations. 71 | */ 72 | R sorted(String rel, String id, Comparator> comparator); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/RepresentationException.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | public class RepresentationException extends RuntimeException { 4 | public RepresentationException(String message) { 5 | super(message); 6 | } 7 | 8 | public RepresentationException(Throwable throwable) { 9 | super(throwable); 10 | } 11 | 12 | public RepresentationException(String message, Throwable throwable) { 13 | super(message, throwable); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/ResourceRepresentation.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.Value; 4 | import io.vavr.collection.HashMap; 5 | import io.vavr.collection.Iterator; 6 | import io.vavr.collection.List; 7 | import io.vavr.collection.Map; 8 | import io.vavr.collection.Multimap; 9 | import io.vavr.collection.Traversable; 10 | import io.vavr.collection.TreeMap; 11 | import io.vavr.collection.TreeMultimap; 12 | import io.vavr.control.Option; 13 | import okio.ByteString; 14 | 15 | import java.net.URI; 16 | import java.util.Comparator; 17 | import java.util.Objects; 18 | import java.util.function.Consumer; 19 | import java.util.function.Function; 20 | 21 | import static com.theoryinpractise.halbuilder5.Rels.getRel; 22 | 23 | public final class ResourceRepresentation implements Value { 24 | 25 | public static Rel SELF = Rels.singleton("self"); 26 | 27 | public static final Comparator RELATABLE_ORDERING = 28 | Comparator.comparing( 29 | Links::getRel, 30 | (r1, r2) -> { 31 | if (r1.contains("self")) { 32 | return -1; 33 | } 34 | if (r2.contains("self")) { 35 | return 1; 36 | } 37 | return r1.compareTo(r2); 38 | }); 39 | 40 | @Override 41 | public V get() { 42 | return value; 43 | } 44 | 45 | @Override 46 | public boolean isEmpty() { 47 | return value == null; 48 | } 49 | 50 | @Override 51 | public boolean isAsync() { 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean isLazy() { 57 | return false; 58 | } 59 | 60 | @Override 61 | public boolean isSingleValued() { 62 | return true; 63 | } 64 | 65 | @Override 66 | public ResourceRepresentation map(Function function) { 67 | return new ResourceRepresentation( 68 | content, links, rels, namespaceManager, function.apply(value), resources); 69 | } 70 | 71 | @Override 72 | public ResourceRepresentation peek(Consumer consumer) { 73 | consumer.accept(value); 74 | return this; 75 | } 76 | 77 | @Override 78 | public String stringPrefix() { 79 | return "Representation"; 80 | } 81 | 82 | @Override 83 | public Iterator iterator() { 84 | return Option.of(value).iterator(); 85 | } 86 | 87 | public R transform(Function transformer) { 88 | return transformer.apply(value); 89 | } 90 | 91 | protected NamespaceManager namespaceManager = NamespaceManager.EMPTY; 92 | 93 | protected Option content = Option.none(); 94 | 95 | protected TreeMap rels = TreeMap.empty(); 96 | 97 | protected List links = List.empty(); 98 | 99 | protected V value; 100 | 101 | protected Multimap> resources = TreeMultimap.withSeq().empty(); 102 | 103 | protected boolean hasNullProperties = false; 104 | 105 | private ResourceRepresentation( 106 | Option content, 107 | List links, 108 | TreeMap rels, 109 | NamespaceManager namespaceManager, 110 | V value, 111 | Multimap> resources) { 112 | this.content = content; 113 | this.links = links; 114 | this.rels = rels; 115 | this.namespaceManager = namespaceManager; 116 | this.value = value; 117 | this.resources = resources; 118 | } 119 | 120 | public static ResourceRepresentation empty(String href) { 121 | return empty().withLink("self", href); 122 | } 123 | 124 | public static ResourceRepresentation create(V value) { 125 | return empty().withValue(value); 126 | } 127 | 128 | public static ResourceRepresentation create(String href, V value) { 129 | return empty().withLink("self", href).withValue(value); 130 | } 131 | 132 | private static final ResourceRepresentation EMPTY = 133 | new ResourceRepresentation<>( 134 | Option.none(), 135 | List.empty(), 136 | TreeMap.of("self", SELF), 137 | NamespaceManager.EMPTY, 138 | null, 139 | TreeMultimap.withSeq().empty()); 140 | 141 | public static ResourceRepresentation empty() { 142 | return EMPTY; 143 | } 144 | 145 | /** 146 | * Retrieve the defined rel semantics for this representation. 147 | * 148 | * @return 149 | */ 150 | public Map getRels() { 151 | return rels; 152 | } 153 | 154 | /** 155 | * Adds or replaces the content of the representation. 156 | * 157 | * @param content The source content of the representation. 158 | * @return A new instance of a PersistentRepresentation with the namespace included. 159 | */ 160 | public ResourceRepresentation withContent(ByteString content) { 161 | return new ResourceRepresentation<>( 162 | Option.of(content), links, rels, namespaceManager, value, resources); 163 | } 164 | 165 | /** 166 | * Define rel semantics for this representation. 167 | * 168 | * @param rel A defined relationship type 169 | */ 170 | public ResourceRepresentation withRel(Rel rel) { 171 | if (rels.containsKey(rel.rel())) { 172 | throw new IllegalStateException(String.format("Rel %s is already declared.", rel.rel())); 173 | } 174 | final TreeMap updatedRels = rels.put(rel.rel(), rel); 175 | return new ResourceRepresentation<>( 176 | content, links, updatedRels, namespaceManager, value, resources); 177 | } 178 | 179 | /** 180 | * Add a link to this resource. 181 | * 182 | * @param rel 183 | * @param href The target href for the link, relative to the href of this resource. 184 | * @return 185 | */ 186 | public ResourceRepresentation withLink(String rel, String href) { 187 | return withLink(Links.create(rel, href)); 188 | } 189 | 190 | /** 191 | * Add a link to this resource. 192 | * 193 | * @param rel 194 | * @param uri The target URI for the link, possibly relative to the href of this resource. 195 | * @return 196 | */ 197 | public ResourceRepresentation withLink(String rel, URI uri) { 198 | return withLink(rel, uri.toASCIIString()); 199 | } 200 | 201 | /** 202 | * Add a link to this resource. 203 | * 204 | * @param rel 205 | * @param href The target href for the link, relative to the href of this resource. 206 | * @param properties The properties to add to this link object 207 | */ 208 | public ResourceRepresentation withLink(String rel, String href, String... properties) { 209 | return withLink(Links.create(rel, href, properties)); 210 | } 211 | 212 | /** 213 | * Add a link to this resource. 214 | * 215 | * @param rel 216 | * @param href The target href for the link, relative to the href of this resource. 217 | * @param properties The properties to add to this link object 218 | */ 219 | public ResourceRepresentation withLink( 220 | String rel, String href, Map properties) { 221 | return withLink(Links.create(rel, href, properties)); 222 | } 223 | 224 | /** 225 | * Add a link to this resource. 226 | * 227 | * @param rel 228 | * @param href The target href for the link, relative to the href of this resource. 229 | * @param properties The properties to add to this link object 230 | */ 231 | public ResourceRepresentation withLink( 232 | String rel, String href, java.util.Map properties) { 233 | return withLink(Links.create(rel, href, HashMap.ofAll(properties))); 234 | } 235 | 236 | /** 237 | * Add a link to this resource. 238 | * 239 | * @param link The target link 240 | */ 241 | public ResourceRepresentation withLink(Link link) { 242 | String rel = Links.getRel(link); 243 | Support.checkRelType(rel); 244 | validateSingletonRel(rel); 245 | final TreeMap updatedRels = 246 | !rels.containsKey(rel) ? rels.put(rel, Rels.natural(rel)) : rels; 247 | final List updatedLinks = links.append(link); 248 | return new ResourceRepresentation<>( 249 | content, updatedLinks, updatedRels, namespaceManager, value, resources); 250 | } 251 | 252 | /** 253 | * Add a link to this resource. 254 | * 255 | * @param links The target link 256 | */ 257 | public ResourceRepresentation withLinks(List links) { 258 | links.forEach( 259 | link -> { 260 | String rel = Links.getRel(link); 261 | Support.checkRelType(rel); 262 | validateSingletonRel(rel); 263 | }); 264 | 265 | final TreeMap updatedRels = 266 | links 267 | .map(Links::getRel) 268 | .foldLeft( 269 | rels, 270 | (accum, rel) -> 271 | !accum.containsKey(rel) ? accum.put(rel, Rels.natural(rel)) : accum); 272 | 273 | final List updatedLinks = this.links.appendAll(links); 274 | return new ResourceRepresentation<>( 275 | content, updatedLinks, updatedRels, namespaceManager, value, resources); 276 | } 277 | 278 | /** 279 | * Replace the value of this resource with a new value, optionally of a new type. 280 | * 281 | * @param newValue The new value for this resource 282 | * @param The type of the new value 283 | * @return The new resource 284 | */ 285 | public ResourceRepresentation withValue(R newValue) { 286 | return new ResourceRepresentation<>( 287 | Option.none(), links, rels, namespaceManager, newValue, resources); 288 | } 289 | 290 | /** 291 | * Adds a new namespace. 292 | * 293 | * @param namespace The CURIE prefix for the namespace being added. 294 | * @param href The target href of the namespace being added. This may be relative to the 295 | * resourceFactories baseref 296 | * @return A new instance of a PersistentRepresentation with the namespace included. 297 | */ 298 | public ResourceRepresentation withNamespace(String namespace, String href) { 299 | if (!rels.containsKey("curies")) { 300 | rels = rels.put("curies", Rels.collection("curies")); 301 | } 302 | 303 | final NamespaceManager updatedNamespaceManager = 304 | namespaceManager.withNamespace(namespace, href); 305 | return new ResourceRepresentation<>( 306 | content, links, rels, updatedNamespaceManager, value, resources); 307 | } 308 | 309 | public ResourceRepresentation withRepresentation( 310 | String rel, ResourceRepresentation resource) { 311 | 312 | if (resources.containsValue(resource)) { 313 | throw new IllegalStateException("Resource is already embedded."); 314 | } 315 | 316 | Support.checkRelType(rel); 317 | validateSingletonRel(rel); 318 | 319 | Multimap> updatedResources = resources.put(rel, resource); 320 | 321 | ResourceRepresentation updatedRepresentation = 322 | new ResourceRepresentation<>( 323 | content, links, rels, namespaceManager, value, updatedResources); 324 | // Propagate null property flag to parent. 325 | if (resource.hasNullProperties()) { 326 | updatedRepresentation.hasNullProperties = true; 327 | } 328 | 329 | if (!rels.containsKey(rel)) { 330 | updatedRepresentation = updatedRepresentation.withRel(Rels.natural(rel)); 331 | } 332 | 333 | return updatedRepresentation; 334 | } 335 | 336 | private void validateSingletonRel(String unvalidatedRel) { 337 | rels.get(unvalidatedRel) 338 | .forEach( 339 | rel -> { 340 | // Rel is register, check for duplicate singleton 341 | if (isSingleton(rel) 342 | && (!getLinksByRel(rel).isEmpty() || !getResourcesByRel(rel).isEmpty())) { 343 | throw new IllegalStateException( 344 | String.format("%s is registered as a single rel and already exists.", rel)); 345 | } 346 | }); 347 | } 348 | 349 | private static final Function isSingletonF = 350 | Rels.cases().singleton_(true).otherwise_(false); 351 | 352 | private static Boolean isSingleton(Rel rel) { 353 | return isSingletonF.apply(rel); 354 | } 355 | 356 | public void validateNamespaces() { 357 | getLinks().forEach(link -> namespaceManager.validateNamespaces(Links.getRel(link))); 358 | 359 | resources.forEach( 360 | (key, rel) -> { 361 | namespaceManager.validateNamespaces(key); 362 | rel.validateNamespaces(); 363 | }); 364 | } 365 | 366 | public Option getContent() { 367 | return content; 368 | } 369 | 370 | public Option getResourceLink() { 371 | return getLinkByRel(SELF); 372 | } 373 | 374 | public Map getNamespaces() { 375 | return namespaceManager.getNamespaces(); 376 | } 377 | 378 | public Option getLinkByRel(String rel) { 379 | return getLinksByRel(rel).headOption(); 380 | } 381 | 382 | public Option getLinkByRel(Rel rel) { 383 | return getLinkByRel(getRel(rel)); 384 | } 385 | 386 | public List getLinksByRel(String rel) { 387 | Support.checkRelType(rel); 388 | return getLinksByRel(this, rel); 389 | } 390 | 391 | public List getLinksByRel(Rel rel) { 392 | return getLinksByRel(getRel(rel)); 393 | } 394 | 395 | public Traversable> getResourcesByRel(String rel) { 396 | Support.checkRelType(rel); 397 | return resources.get(rel).getOrElse(List.empty()); 398 | } 399 | 400 | public Traversable> getResourcesByRel(Rel rel) { 401 | return getResourcesByRel(getRel(rel)); 402 | } 403 | 404 | public boolean hasNullProperties() { 405 | return hasNullProperties; 406 | } 407 | 408 | public Multimap> getResources() { 409 | return resources; 410 | } 411 | 412 | public List getLinks() { 413 | return links.sorted(RELATABLE_ORDERING); 414 | } 415 | 416 | private List getLinksByRel(ResourceRepresentation representation, String rel) { 417 | Support.checkRelType(rel); 418 | return representation 419 | .getLinks() 420 | .filter( 421 | link -> { 422 | return rel.equals(Links.getRel(link)); 423 | }); 424 | } 425 | 426 | @Override 427 | public int hashCode() { 428 | int h = namespaceManager.hashCode(); 429 | h += links.hashCode(); 430 | if (value != null) { 431 | h += value.hashCode(); 432 | } 433 | h += resources.hashCode(); 434 | return h; 435 | } 436 | 437 | @Override 438 | public boolean equals(Object obj) { 439 | if (obj == null) { 440 | return false; 441 | } 442 | if (obj == this) { 443 | return true; 444 | } 445 | if (!(obj instanceof ResourceRepresentation)) { 446 | return false; 447 | } 448 | ResourceRepresentation that = (ResourceRepresentation) obj; 449 | boolean e = Objects.equals(this.namespaceManager, that.namespaceManager); 450 | e &= Objects.equals(this.links, that.links); 451 | 452 | e &= Objects.equals(this.value, that.value); 453 | e &= Objects.equals(this.resources, that.resources); 454 | return e; 455 | } 456 | 457 | @Override 458 | public String toString() { 459 | return getLinkByRel("self") 460 | .map(link -> String.format("", Links.getHref(link))) 461 | .getOrElse(String.format("", Integer.toHexString(hashCode()))); 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/Support.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.core.JsonFactory; 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.databind.Module; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.fasterxml.jackson.databind.SerializationFeature; 9 | 10 | import java.util.Objects; 11 | import java.nio.charset.StandardCharsets; 12 | import java.nio.charset.Charset; 13 | 14 | public interface Support { 15 | 16 | Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; 17 | 18 | String LINKS = "_links"; 19 | String EMBEDDED = "_embedded"; 20 | String CURIES = "curies"; 21 | String TEMPLATED = "templated"; 22 | String HAL_JSON = "application/hal+json"; 23 | String HAL_XML = "application/hal+xml"; 24 | 25 | static void checkRelType(String rel) { 26 | Objects.requireNonNull(rel, "Provided rel should not be null."); 27 | if ("".equals(rel) || rel.contains(" ")) { 28 | throw new IllegalArgumentException( 29 | "Provided rel value should be a single rel type, as " 30 | + "defined by http://tools.ietf.org/html/rfc5988"); 31 | } 32 | } 33 | 34 | static ObjectMapper defaultObjectMapper(Module... modules) { 35 | JsonFactory f = new JsonFactory(); 36 | f.enable(JsonGenerator.Feature.QUOTE_FIELD_NAMES); 37 | 38 | ObjectMapper objectMapper = new ObjectMapper(f); 39 | objectMapper.registerModules(modules); 40 | objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 41 | objectMapper.configure(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, false); 42 | 43 | return objectMapper; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/json/JsonRepresentationReader.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5.json; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.Module; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import com.theoryinpractise.halbuilder5.Link; 9 | import com.theoryinpractise.halbuilder5.Links; 10 | import com.theoryinpractise.halbuilder5.RepresentationException; 11 | import com.theoryinpractise.halbuilder5.ResourceRepresentation; 12 | import io.vavr.collection.HashMap; 13 | import io.vavr.collection.List; 14 | import io.vavr.collection.Map; 15 | import okio.ByteString; 16 | 17 | import java.io.IOException; 18 | import java.io.Reader; 19 | import java.io.StringReader; 20 | import java.io.UncheckedIOException; 21 | import java.util.Iterator; 22 | import java.util.Scanner; 23 | import java.util.Map.Entry; 24 | import java.util.function.Function; 25 | import java.util.function.Supplier; 26 | 27 | import static com.theoryinpractise.halbuilder5.Link.HREF; 28 | import static com.theoryinpractise.halbuilder5.Link.NAME; 29 | import static com.theoryinpractise.halbuilder5.Support.CURIES; 30 | import static com.theoryinpractise.halbuilder5.Support.EMBEDDED; 31 | import static com.theoryinpractise.halbuilder5.Support.LINKS; 32 | import static com.theoryinpractise.halbuilder5.Support.defaultObjectMapper; 33 | import static okio.ByteString.encodeUtf8; 34 | 35 | public class JsonRepresentationReader { 36 | 37 | private final ObjectMapper mapper; 38 | 39 | public static final Function readByteStringAs( 40 | ObjectMapper mapper, Class classType, Supplier defaultValue) { 41 | return bs -> { 42 | try { 43 | return mapper.readValue(bs.utf8(), classType); 44 | } catch (IOException e) { 45 | return defaultValue.get(); 46 | } 47 | }; 48 | } 49 | 50 | public static final Function readByteStringAs( 51 | ObjectMapper mapper, Class classType) { 52 | return bs -> { 53 | try { 54 | return mapper.readValue(bs.utf8(), classType); 55 | } catch (IOException e) { 56 | throw new UncheckedIOException(e); 57 | } 58 | }; 59 | } 60 | 61 | private JsonRepresentationReader(ObjectMapper objectMapper) { 62 | this.mapper = objectMapper; 63 | } 64 | 65 | public static JsonRepresentationReader create() { 66 | return create(defaultObjectMapper()); 67 | } 68 | 69 | public static JsonRepresentationReader create(Module... modules) { 70 | return create(defaultObjectMapper(modules)); 71 | } 72 | 73 | public static JsonRepresentationReader create(ObjectMapper objectMapper) { 74 | return new JsonRepresentationReader(objectMapper); 75 | } 76 | 77 | private static String readContent(Reader reader) { 78 | return new Scanner(reader).useDelimiter("\\Z").next(); 79 | } 80 | 81 | public ResourceRepresentation read(Reader reader, Class classType) { 82 | return read(encodeUtf8(readContent(reader)), classType); 83 | } 84 | 85 | public ResourceRepresentation read( 86 | Reader reader, Class classType, Supplier defaultValue) { 87 | return read(encodeUtf8(readContent(reader)), classType, defaultValue); 88 | } 89 | 90 | public ResourceRepresentation read(Reader reader) { 91 | return read(encodeUtf8(readContent(reader))); 92 | } 93 | 94 | public ResourceRepresentation read( 95 | ByteString byteString, Class classType, Supplier defaultValue) { 96 | return read(byteString).map(readByteStringAs(mapper, classType, defaultValue)); 97 | } 98 | 99 | public ResourceRepresentation read(ByteString byteString, Class classType) { 100 | return read(byteString).map(readByteStringAs(mapper, classType)); 101 | } 102 | 103 | public ResourceRepresentation read(ByteString byteString) { 104 | try { 105 | JsonNode rootNode = mapper.readValue(new StringReader(byteString.utf8()), JsonNode.class); 106 | return readResource(rootNode).withContent(byteString); 107 | } catch (Exception e) { 108 | throw new RepresentationException(e.getMessage(), e); 109 | } 110 | } 111 | 112 | private ResourceRepresentation readResource(JsonNode rootNode) { 113 | 114 | ResourceRepresentation representation = 115 | readProperties(rootNode, ResourceRepresentation.empty()); 116 | representation = readNamespaces(rootNode, representation); 117 | representation = readLinks(rootNode, representation); 118 | representation = readResources(rootNode, representation); 119 | 120 | return representation; 121 | } 122 | 123 | private ResourceRepresentation readNamespaces( 124 | JsonNode rootNode, ResourceRepresentation resource) { 125 | ResourceRepresentation newRep = resource; 126 | if (rootNode.has(LINKS)) { 127 | JsonNode linksNode = rootNode.get(LINKS); 128 | if (linksNode.has(CURIES)) { 129 | JsonNode curieNode = linksNode.get(CURIES); 130 | 131 | if (curieNode.isArray()) { 132 | Iterator values = curieNode.elements(); 133 | while (values.hasNext()) { 134 | JsonNode valueNode = values.next(); 135 | newRep = 136 | newRep.withNamespace(valueNode.get(NAME).asText(), valueNode.get(HREF).asText()); 137 | } 138 | } else { 139 | newRep = newRep.withNamespace(curieNode.get(NAME).asText(), curieNode.get(HREF).asText()); 140 | } 141 | } 142 | } 143 | return newRep; 144 | } 145 | 146 | private ResourceRepresentation readLinks( 147 | JsonNode rootNode, ResourceRepresentation resource) { 148 | 149 | List links = List.empty(); 150 | 151 | if (rootNode.has(LINKS)) { 152 | Iterator> fields = rootNode.get(LINKS).fields(); 153 | while (fields.hasNext()) { 154 | Entry keyNode = fields.next(); 155 | if (!CURIES.equals(keyNode.getKey())) { 156 | if (keyNode.getValue().isArray()) { 157 | Iterator values = keyNode.getValue().elements(); 158 | while (values.hasNext()) { 159 | links = links.append(jsonLink(keyNode.getKey(), values.next())); 160 | } 161 | } else { 162 | links = links.append(jsonLink(keyNode.getKey(), keyNode.getValue())); 163 | } 164 | } 165 | } 166 | } 167 | 168 | return links.isEmpty() ? resource : resource.withLinks(links); 169 | } 170 | 171 | private Link jsonLink(String rel, JsonNode node) { 172 | String href = node.get(HREF).asText(); 173 | 174 | Map properties = HashMap.empty(); 175 | Iterator> fields = node.fields(); 176 | while (fields.hasNext()) { 177 | Entry keyNode = fields.next(); 178 | properties = properties.put(keyNode.getKey(), keyNode.getValue().asText()); 179 | } 180 | 181 | return Links.create(rel, href, properties); 182 | } 183 | 184 | private ResourceRepresentation readProperties( 185 | JsonNode rootNode, ResourceRepresentation resource) { 186 | ObjectNode propertyNode = rootNode.deepCopy(); 187 | propertyNode.remove("_links"); 188 | propertyNode.remove("_embedded"); 189 | 190 | try { 191 | return resource.withValue(encodeUtf8(mapper.writeValueAsString(propertyNode))); 192 | } catch (JsonProcessingException e) { 193 | throw new UncheckedIOException(e); 194 | } 195 | } 196 | 197 | private ResourceRepresentation readResources( 198 | JsonNode rootNode, ResourceRepresentation resource) { 199 | if (rootNode.has(EMBEDDED)) { 200 | ResourceRepresentation newResource = resource; 201 | Iterator> fields = rootNode.get(EMBEDDED).fields(); 202 | while (fields.hasNext()) { 203 | Entry keyNode = fields.next(); 204 | if (keyNode.getValue().isArray()) { 205 | Iterator values = keyNode.getValue().elements(); 206 | while (values.hasNext()) { 207 | JsonNode valueNode = values.next(); 208 | newResource = newResource.withRepresentation(keyNode.getKey(), readResource(valueNode)); 209 | } 210 | } else { 211 | newResource = 212 | newResource.withRepresentation(keyNode.getKey(), readResource(keyNode.getValue())); 213 | } 214 | } 215 | return newResource; 216 | } else { 217 | return resource; 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/com/theoryinpractise/halbuilder5/json/JsonRepresentationWriter.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5.json; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.Module; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.node.ArrayNode; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import com.theoryinpractise.halbuilder5.Link; 9 | import com.theoryinpractise.halbuilder5.Links; 10 | import com.theoryinpractise.halbuilder5.Rel; 11 | import com.theoryinpractise.halbuilder5.Rels; 12 | import com.theoryinpractise.halbuilder5.RepresentationException; 13 | import com.theoryinpractise.halbuilder5.ResourceRepresentation; 14 | import io.vavr.Tuple2; 15 | import io.vavr.collection.HashMap; 16 | import io.vavr.collection.List; 17 | import okio.Buffer; 18 | import okio.ByteString; 19 | 20 | import java.io.IOException; 21 | import java.io.OutputStreamWriter; 22 | import java.io.Writer; 23 | import java.nio.charset.StandardCharsets; 24 | import java.util.Collection; 25 | import java.util.Iterator; 26 | import java.util.Map; 27 | import java.util.function.Function; 28 | 29 | import static com.theoryinpractise.halbuilder5.Link.HREF; 30 | import static com.theoryinpractise.halbuilder5.Support.CURIES; 31 | import static com.theoryinpractise.halbuilder5.Support.EMBEDDED; 32 | import static com.theoryinpractise.halbuilder5.Support.LINKS; 33 | import static com.theoryinpractise.halbuilder5.Support.TEMPLATED; 34 | import static com.theoryinpractise.halbuilder5.Support.defaultObjectMapper; 35 | 36 | public final class JsonRepresentationWriter { 37 | 38 | private ObjectMapper codec; 39 | 40 | private JsonRepresentationWriter(ObjectMapper codec) { 41 | this.codec = codec; 42 | } 43 | 44 | public static JsonRepresentationWriter create() { 45 | return create(defaultObjectMapper()); 46 | } 47 | 48 | public static JsonRepresentationWriter create(Module... modules) { 49 | return create(defaultObjectMapper(modules)); 50 | } 51 | 52 | public static JsonRepresentationWriter create(ObjectMapper objectMapper) { 53 | return new JsonRepresentationWriter(objectMapper); 54 | } 55 | 56 | public ByteString print(ResourceRepresentation representation) { 57 | Buffer buffer = new Buffer(); 58 | write(representation, new OutputStreamWriter(buffer.outputStream(), StandardCharsets.UTF_8)); 59 | return buffer.readByteString(); 60 | } 61 | 62 | public void write(ResourceRepresentation representation, Writer writer) { 63 | try { 64 | ObjectNode resourceNode = renderJson(representation, false); 65 | codec.writerWithDefaultPrettyPrinter().writeValue(writer, resourceNode); 66 | } catch (IOException e) { 67 | throw new RepresentationException(e); 68 | } 69 | } 70 | 71 | private static final Function isSingletonF = 72 | Rels.cases().singleton_(true).otherwise_(false); 73 | 74 | private boolean isSingleton(Rel rel) { 75 | return isSingletonF.apply(rel); 76 | } 77 | 78 | private static final Function isCollectionF = 79 | Rels.cases().collection_(true).sorted_(true).otherwise_(false); 80 | 81 | private boolean isCollection(Rel rel) { 82 | return isCollectionF.apply(rel); 83 | } 84 | 85 | private ObjectNode renderJson(ResourceRepresentation representation, boolean embedded) { 86 | 87 | ObjectNode objectNode = codec.getNodeFactory().objectNode(); 88 | 89 | renderJsonProperties(objectNode, representation); 90 | renderJsonLinks(objectNode, representation, embedded); 91 | renderJsonEmbeds(objectNode, representation); 92 | 93 | return objectNode; 94 | } 95 | 96 | private void renderJsonEmbeds(ObjectNode objectNode, ResourceRepresentation representation) { 97 | if (!representation.getResources().isEmpty()) { 98 | 99 | // TODO toJavaMap is kinda nasty 100 | Map>> resourceMap = 101 | representation.getResources().toJavaMap(); 102 | 103 | ObjectNode embedsNode = codec.createObjectNode(); 104 | objectNode.set(EMBEDDED, embedsNode); 105 | 106 | for (Map.Entry>> resourceEntry : 107 | resourceMap.entrySet()) { 108 | 109 | Rel rel = representation.getRels().get(resourceEntry.getKey()).get(); 110 | 111 | boolean coalesce = 112 | !isCollection(rel) && (isSingleton(rel) || resourceEntry.getValue().size() == 1); 113 | 114 | if (coalesce) { 115 | ResourceRepresentation subRepresentation = resourceEntry.getValue().iterator().next(); 116 | ObjectNode embeddedNode = renderJson(subRepresentation, true); 117 | embedsNode.set(resourceEntry.getKey(), embeddedNode); 118 | } else { 119 | 120 | List> values = 121 | Rels.getComparator(rel) 122 | .transform( 123 | comp -> 124 | comp.isDefined() 125 | ? List.ofAll(resourceEntry.getValue()).sorted(comp.get()) 126 | : List.ofAll(resourceEntry.getValue())); 127 | 128 | ArrayNode embedArrayNode = codec.createArrayNode(); 129 | embedsNode.set(rel.rel(), embedArrayNode); 130 | 131 | for (ResourceRepresentation subRepresentation : values) { 132 | ObjectNode embeddedNode = renderJson(subRepresentation, true); 133 | embedArrayNode.add(embeddedNode); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | private void renderJsonProperties( 141 | ObjectNode objectNode, ResourceRepresentation representation) { 142 | JsonNode tree = codec.valueToTree(representation.get()); 143 | if (tree != null) { 144 | if (tree.isObject()) { 145 | Iterator> fields = tree.fields(); 146 | while (fields.hasNext()) { 147 | Map.Entry next = fields.next(); 148 | objectNode.set(next.getKey(), next.getValue()); 149 | } 150 | } else { 151 | throw new IllegalStateException("Unable to serialise a non Object Node"); 152 | } 153 | } 154 | } 155 | 156 | private void renderJsonLinks( 157 | ObjectNode objectNode, ResourceRepresentation representation, boolean embedded) { 158 | if (!representation.getLinks().isEmpty() 159 | || (!embedded && !representation.getNamespaces().isEmpty())) { 160 | 161 | List links = List.empty(); 162 | 163 | // Include namespaces as links when not embedded 164 | if (!embedded) { 165 | links = 166 | links.appendAll( 167 | representation 168 | .getNamespaces() 169 | .map(ns -> Links.create(CURIES, ns._2, "name", ns._1))); 170 | } 171 | 172 | // Add representation links 173 | links = links.appendAll(representation.getLinks()); 174 | 175 | // Partition representation links by rel 176 | 177 | ObjectNode linksNode = codec.createObjectNode(); 178 | objectNode.set(LINKS, linksNode); 179 | 180 | for (Tuple2> linkEntry : links.groupBy(Links::getRel).toList()) { 181 | 182 | Rel rel = representation.getRels().get(linkEntry._1).get(); 183 | boolean coalesce = !isCollection(rel) && (isSingleton(rel) || linkEntry._2.size() == 1); 184 | 185 | if (coalesce) { 186 | Link link = linkEntry._2.iterator().next(); 187 | 188 | ObjectNode linkNode = writeJsonLinkContent(link); 189 | linksNode.set(linkEntry._1, linkNode); 190 | } else { 191 | ArrayNode linkArrayNode = codec.createArrayNode(); 192 | for (Link link : linkEntry._2) { 193 | linkArrayNode.add(writeJsonLinkContent(link)); 194 | } 195 | linksNode.set(linkEntry._1, linkArrayNode); 196 | } 197 | } 198 | } 199 | } 200 | 201 | private ObjectNode writeJsonLinkContent(Link link) { 202 | ObjectNode linkNode = codec.createObjectNode(); 203 | linkNode.set(HREF, codec.getNodeFactory().textNode(Links.getHref(link))); 204 | 205 | io.vavr.collection.Map properties = 206 | Links.getProperties(link).getOrElse(HashMap.empty()); 207 | for (Tuple2 prop : properties) { 208 | linkNode.set(prop._1, codec.getNodeFactory().textNode(prop._2)); 209 | } 210 | if (Links.getTemplated(link)) { 211 | linkNode.set(TEMPLATED, codec.getNodeFactory().booleanNode(true)); 212 | } 213 | return linkNode; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/AbstractAccount.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.immutables.value.Value; 4 | 5 | @JsonSerializedValue 6 | @Value.Immutable 7 | public abstract class AbstractAccount { 8 | @Value.Parameter 9 | public abstract String accountNumber(); 10 | 11 | @Value.Parameter 12 | public abstract String name(); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/AbstractCurriedNamespaceData.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.immutables.value.Value; 4 | 5 | @JsonSerializedValue 6 | @Value.Immutable 7 | public abstract class AbstractCurriedNamespaceData { 8 | @Value.Parameter 9 | public abstract String ns(); 10 | 11 | @Value.Parameter 12 | public abstract String href(); 13 | 14 | @Value.Parameter 15 | public abstract String original(); 16 | 17 | @Value.Parameter 18 | public abstract String curried(); 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/ContentTypeTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | import static com.google.common.truth.Truth.assertThat; 6 | 7 | public class ContentTypeTest { 8 | 9 | @Test 10 | public void testContentTypeEquality() { 11 | final ContentType contentType = new ContentType("application/xml"); 12 | assertThat(contentType.equals(new ContentType("application/xml"))).isTrue(); 13 | assertThat(contentType.equals(new ContentType("application/json"))).isFalse(); 14 | assertThat(contentType.equals(contentType)).isTrue(); 15 | assertThat(contentType.equals(null)).isFalse(); 16 | } 17 | 18 | @Test 19 | public void testContentTypeCreation() { 20 | assertThat(new ContentType("application/xml").getType()).isEqualTo("application"); 21 | assertThat(new ContentType("application/xml").getSubType()).isEqualTo("xml"); 22 | assertThat(new ContentType("{application/hal+json, q=1000}").getType()) 23 | .isEqualTo("application"); 24 | assertThat(new ContentType("{application/hal+json, q=1000}").getSubType()) 25 | .isEqualTo("hal+json"); 26 | assertThat(new ContentType("*/json").getType()).isEqualTo("*"); 27 | assertThat(new ContentType("*/json").getSubType()).isEqualTo("json"); 28 | } 29 | 30 | @Test 31 | public void testContentTypeMatching() { 32 | assertThat(new ContentType("application/xml").matches("application/xml")).isTrue(); 33 | assertThat(new ContentType("application/xml").matches(new ContentType("application/xml"))) 34 | .isTrue(); 35 | assertThat(new ContentType("application/xml").matches(new ContentType("application/*"))) 36 | .isTrue(); 37 | assertThat(new ContentType("application/xml").matches(new ContentType("*/*"))).isTrue(); 38 | assertThat(new ContentType("application/xml").matches(new ContentType("*/xml"))).isTrue(); 39 | assertThat(new ContentType("application/xml").matches(new ContentType("*/json"))).isFalse(); 40 | assertThat(new ContentType("*/*").matches(new ContentType("application/xml"))).isFalse(); 41 | assertThat( 42 | new ContentType("application/json") 43 | .matches(new ContentType("{application/json; q=1000}"))) 44 | .isTrue(); 45 | assertThat( 46 | new ContentType("application/json") 47 | .matches(new ContentType("{application/json;q=1000}"))) 48 | .isTrue(); 49 | assertThat( 50 | new ContentType("application/json") 51 | .matches(new ContentType("{application/json, q=1000}"))) 52 | .isTrue(); 53 | assertThat( 54 | new ContentType("application/json") 55 | .matches(new ContentType("{application/json,q=1000}"))) 56 | .isTrue(); 57 | assertThat( 58 | new ContentType("application/hal+json") 59 | .matches(new ContentType("{application/hal+json; q=1000}"))) 60 | .isTrue(); 61 | assertThat( 62 | new ContentType("application/hal+json") 63 | .matches(new ContentType("{application/hal+json;q=1000}"))) 64 | .isTrue(); 65 | assertThat( 66 | new ContentType("application/hal+json") 67 | .matches(new ContentType("{application/hal+json, q=1000}"))) 68 | .isTrue(); 69 | assertThat( 70 | new ContentType("application/hal+json") 71 | .matches(new ContentType("{application/hal+json,q=1000}"))) 72 | .isTrue(); 73 | assertThat( 74 | new ContentType("application/hal+json") 75 | .matches(new ContentType("{*/hal+json ,q=1000}"))) 76 | .isTrue(); 77 | assertThat(new ContentType("*/*").matches(new ContentType("{application/hal+json ,q=1000}"))) 78 | .isFalse(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/JsonSerializedValue.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 4 | import org.immutables.value.Value; 5 | 6 | @JsonSerialize 7 | @Value.Style( 8 | typeAbstract = {"Abstract*"}, 9 | typeImmutable = "*", 10 | visibility = Value.Style.ImplementationVisibility.PUBLIC, 11 | jdkOnly = true, 12 | defaults = @Value.Immutable(intern = true, builder = false, copy = false)) 13 | @interface JsonSerializedValue {} 14 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/LinkListSubject.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import com.google.common.truth.FailureMetadata; 4 | import com.google.common.truth.Subject; 5 | import io.vavr.collection.List; 6 | 7 | import static com.google.common.truth.Truth.assertAbout; 8 | import static com.theoryinpractise.halbuilder5.Links.getRel; 9 | 10 | public class LinkListSubject extends Subject> { 11 | 12 | private LinkListSubject(FailureMetadata failureMetadata, List subject) { 13 | super(failureMetadata, subject); 14 | } 15 | 16 | public static Subject.Factory> linkLists() { 17 | return LinkListSubject::new; 18 | } 19 | 20 | public static LinkListSubject assertThatLinkLists(List links) { 21 | return assertAbout(linkLists()).that(links); 22 | } 23 | 24 | public void containsRelCondition(String rel) { 25 | boolean hasMatch = false; 26 | for (Link link : getSubject()) { 27 | if (rel.equals(getRel(link))) { 28 | hasMatch = true; 29 | } 30 | } 31 | if (!hasMatch) { 32 | fail("List does not contain rel: " + rel); 33 | } 34 | } 35 | 36 | public void doesNotContainRelCondition(String rel) { 37 | boolean hasMatch = false; 38 | for (Link link : getSubject()) { 39 | if (rel.equals(getRel(link))) { 40 | hasMatch = true; 41 | } 42 | } 43 | if (hasMatch) { 44 | fail("List contains rel: " + rel); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/LinkTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.collection.HashMap; 4 | import org.testng.annotations.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class LinkTest { 9 | 10 | private Link link = 11 | Links.create("rel", "http://example.com/", HashMap.of("name", "name").put("title", "title")); 12 | 13 | @Test 14 | public void equalLinksHaveEqualHashCodes() { 15 | Link otherLink = 16 | Links.create( 17 | "rel", "http://example.com/", HashMap.of("name", "name").put("title", "title")); 18 | assertThat(link.hashCode()).isEqualTo(otherLink.hashCode()); 19 | } 20 | 21 | @Test 22 | public void testHashCodeIsDependentOnHref() { 23 | Link otherLink = 24 | Links.create( 25 | "rel", "http://example.com/other", HashMap.of("name", "name").put("title", "title")); 26 | 27 | assertThat(otherLink.hashCode()).isNotEqualTo(link.hashCode()); 28 | } 29 | 30 | @Test 31 | public void testHashCodeIsDependentOnRel() { 32 | Link otherLink = 33 | Links.create( 34 | "otherrel", "http://example.com/", HashMap.of("name", "name").put("title", "title")); 35 | 36 | assertThat(otherLink.hashCode()).isNotEqualTo(link.hashCode()); 37 | } 38 | 39 | @Test 40 | public void testHashCodeIsDependentOnProperty() { 41 | Link otherLink = 42 | Links.create( 43 | "rel", "http://example.com/", HashMap.of("name", "othername").put("title", "title")); 44 | 45 | assertThat(otherLink.hashCode()).isNotEqualTo(link.hashCode()); 46 | } 47 | 48 | @Test 49 | public void testEqualsIsDependentOnHref() { 50 | Link otherLink = 51 | Links.create( 52 | "rel", "http://example.com/other", HashMap.of("name", "name").put("title", "title")); 53 | 54 | assertThat(otherLink).isNotEqualTo(link); 55 | } 56 | 57 | @Test 58 | public void testEqualsIsDependentOnRel() { 59 | Link otherLink = 60 | Links.create( 61 | "otherrel", "http://example.com/", HashMap.of("name", "name").put("title", "title")); 62 | 63 | assertThat(otherLink).isNotEqualTo(link); 64 | } 65 | 66 | @Test 67 | public void testHasTemplate() { 68 | Link templateLink = Links.create("customerSearch", "http://example.com/search{?customerId}"); 69 | assertThat(Links.getTemplated(templateLink)).isTrue(); 70 | System.out.println(templateLink.toString()); 71 | assertThat(templateLink.toString()).contains("templated"); 72 | } 73 | 74 | @Test 75 | public void testEqualsIsDependentOnProperty() { 76 | Link otherLink = 77 | Links.create( 78 | "rel", "http://example.com/", HashMap.of("name", "othername").put("title", "title")); 79 | 80 | assertThat(otherLink).isNotEqualTo(link); 81 | } 82 | 83 | @Test 84 | public void testToStringRenders() { 85 | String toString = link.toString(); 86 | assertThat(toString) 87 | .isEqualTo( 88 | ""); 89 | } 90 | 91 | @Test 92 | public void testDoesNotHaveTemplate() { 93 | Link nonTemplateLink = Links.create("rel", "http://example.com/other"); 94 | assertThat(Links.getTemplated(nonTemplateLink)).isFalse(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/NamespaceManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.testng.annotations.DataProvider; 4 | import org.testng.annotations.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class NamespaceManagerTest { 9 | 10 | @Test 11 | public void testNamespaceNeedsRel() throws Exception { 12 | 13 | NamespaceManager ns = NamespaceManager.EMPTY; 14 | try { 15 | ns.withNamespace("tst", "http://localhost/test/rel"); 16 | throw new AssertionError("Should not allow us to add a namespace href without {rel}"); 17 | } catch (RepresentationException e) { 18 | // 19 | } 20 | try { 21 | ns.withNamespace("tst", "http://localhost/test/{rel}"); 22 | } catch (RepresentationException e) { 23 | throw new AssertionError("Should not fail when adding a valid namespace href"); 24 | } 25 | } 26 | 27 | @DataProvider 28 | public static Object[][] provideNamespaces() { 29 | return new Object[][] { 30 | { 31 | CurriedNamespaceData.of( 32 | "tst", "http://localhost/test/{rel}", "http://localhost/test/foo", "tst:foo") 33 | }, 34 | { 35 | CurriedNamespaceData.of( 36 | "tst", "http://localhost/test/{rel}/spec", "http://localhost/test/foo/spec", "tst:foo") 37 | } 38 | }; 39 | } 40 | 41 | @Test(dataProvider = "provideNamespaces") 42 | public void testCurrieHref(CurriedNamespaceData data) throws Exception { 43 | NamespaceManager namespaceManager = NamespaceManager.EMPTY; 44 | NamespaceManager updatedNamespaceManager = 45 | namespaceManager.withNamespace(data.ns(), data.href()); 46 | 47 | assertThat(namespaceManager.currieHref(data.original())).isEqualTo(data.original()); 48 | assertThat(updatedNamespaceManager.currieHref(data.original())).isEqualTo(data.curried()); 49 | } 50 | 51 | @Test(dataProvider = "provideNamespaces") 52 | public void testUnCurrieHref(CurriedNamespaceData data) throws Exception { 53 | NamespaceManager namespaceManager = 54 | NamespaceManager.EMPTY.withNamespace(data.ns(), data.href()); 55 | 56 | assertThat(namespaceManager.resolve(data.curried()).isRight()).isTrue(); 57 | assertThat(namespaceManager.resolve(data.curried()).get()).isEqualTo(data.original()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/RepresentationLinksTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.collection.List; 4 | import org.testng.annotations.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | import static com.theoryinpractise.halbuilder5.LinkListSubject.assertThatLinkLists; 8 | 9 | public class RepresentationLinksTest { 10 | 11 | @Test 12 | public void testBasicLinks() { 13 | 14 | ResourceRepresentation resource = 15 | ResourceRepresentation.empty("/foo").withLink("bar", "/bar").withLink("foo", "/bar"); 16 | 17 | List links = resource.getLinks(); 18 | assertThat(links).isNotEmpty(); 19 | 20 | assertThatLinkLists(links).containsRelCondition("bar"); 21 | assertThatLinkLists(links).containsRelCondition("foo"); 22 | 23 | assertThat(resource.getLinksByRel("bar")).isNotNull(); 24 | assertThatLinkLists(resource.getLinksByRel("bar")).containsRelCondition("bar"); 25 | assertThatLinkLists(resource.getLinksByRel("bar")).doesNotContainRelCondition("foo"); 26 | 27 | assertThat(resource.getLinksByRel("foo")).isNotNull(); 28 | assertThatLinkLists(resource.getLinksByRel("foo")).containsRelCondition("foo"); 29 | assertThatLinkLists(resource.getLinksByRel("foo")).doesNotContainRelCondition("bar"); 30 | } 31 | 32 | @Test 33 | public void testSpacedRelsSeparateLinks() { 34 | 35 | ResourceRepresentation resource = ResourceRepresentation.empty("/foo"); 36 | 37 | try { 38 | resource.withLink("bar foo", "/bar"); 39 | throw new AssertionError("We should fail to add a space separated link rel."); 40 | } catch (IllegalArgumentException e) { 41 | // expected 42 | } 43 | } 44 | 45 | @Test 46 | public void testMultiSpacedRelsSeparateLinks() { 47 | 48 | ResourceRepresentation resource = ResourceRepresentation.empty("/foo"); 49 | 50 | try { 51 | resource.withLink("bar foo", "/bar"); 52 | throw new AssertionError("We should fail to add a space separated link rel."); 53 | } catch (IllegalArgumentException e) { 54 | // expected 55 | } 56 | } 57 | 58 | @Test 59 | public void testRelLookupsWithNullFail() { 60 | try { 61 | ResourceRepresentation resource = 62 | ResourceRepresentation.empty("/foo").withLink("bar foo", "/bar"); 63 | 64 | resource.getLinkByRel((String) null); 65 | throw new AssertionError("Should fail"); 66 | } catch (IllegalArgumentException e) { 67 | // ignore 68 | } 69 | } 70 | 71 | @Test 72 | public void testRelLookupsWithEmptyRelFail() { 73 | try { 74 | ResourceRepresentation resource = 75 | ResourceRepresentation.empty("/foo").withLink("bar", "/bar"); 76 | 77 | resource.getLinkByRel(""); 78 | 79 | throw new AssertionError("Should fail"); 80 | } catch (IllegalArgumentException e) { 81 | // ignore 82 | } 83 | } 84 | 85 | @Test 86 | public void testRelLookupsWithSpacesFail() { 87 | try { 88 | ResourceRepresentation resource = 89 | ResourceRepresentation.empty("/foo").withLink("bar", "/bar"); 90 | 91 | resource.getLinkByRel("test fail"); 92 | throw new AssertionError("Should fail"); 93 | } catch (IllegalArgumentException e) { 94 | // ignore 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/ResourceBasicMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import io.vavr.collection.TreeMap; 4 | import io.vavr.control.Option; 5 | import org.testng.annotations.BeforeMethod; 6 | import org.testng.annotations.Test; 7 | 8 | import static com.google.common.truth.Truth.assertThat; 9 | import static io.vavr.control.Option.none; 10 | import static io.vavr.control.Option.some; 11 | 12 | public class ResourceBasicMethodsTest { 13 | 14 | private ResourceRepresentation>> resource; 15 | private ResourceRepresentation>> otherResource; 16 | private int resourceHashCode; 17 | 18 | @BeforeMethod 19 | public void setUpResources() { 20 | resource = createDefaultResource(); 21 | otherResource = createDefaultResource(); 22 | resourceHashCode = resource.hashCode(); 23 | } 24 | 25 | private ResourceRepresentation>> createDefaultResource() { 26 | TreeMap> properties = 27 | TreeMap.of("testprop", some("value"), "nullprop", none()); 28 | return ResourceRepresentation.create("http://localhost/test") 29 | .withNamespace("testns", "http://example.com/test/{rel}") 30 | .withLink("testlink", "http://example.com/link") 31 | .withValue(properties) 32 | .withRepresentation("testsub", ResourceRepresentation.empty("/subtest")); 33 | } 34 | 35 | @Test 36 | public void equalResourcesHaveEqualHashCodes() { 37 | assertThat(resource.hashCode()).isEqualTo(otherResource.hashCode()); 38 | } 39 | 40 | @Test 41 | public void testHashCodeIsDependentOnNamespaces() { 42 | ResourceRepresentation>> updatedResource = 43 | resource.withNamespace("testns2", "http://example.com/test2/{rel}"); 44 | assertThat(resource).isEqualTo(otherResource); 45 | assertThat(updatedResource.hashCode()).isNotEqualTo(resourceHashCode); 46 | } 47 | 48 | @Test 49 | public void testHashCodeIsDependentOnLinks() { 50 | ResourceRepresentation>> updatedResource = 51 | resource.withLink("testlink2", "http://example.com/link2"); 52 | assertThat(resource).isEqualTo(otherResource); 53 | assertThat(updatedResource.hashCode()).isNotEqualTo(resourceHashCode); 54 | } 55 | 56 | @Test 57 | public void testHashCodeIsDependentOnProperties() { 58 | ResourceRepresentation>> updatedResource = 59 | resource.map(values -> values.put("proptest2", some("value2"))); 60 | assertThat(resource).isEqualTo(otherResource); 61 | assertThat(updatedResource).isNotEqualTo(resource); 62 | assertThat(updatedResource).isNotEqualTo(otherResource); 63 | assertThat(updatedResource.hashCode()).isNotEqualTo(resourceHashCode); 64 | } 65 | 66 | @Test 67 | public void testHashCodeIsDependentOnNullProperties() { 68 | ResourceRepresentation>> updatedResource = 69 | resource.map(values -> values.put("othernullprop", none())); 70 | assertThat(resource).isEqualTo(otherResource); 71 | assertThat(updatedResource.hashCode()).isNotEqualTo(resourceHashCode); 72 | } 73 | 74 | @Test 75 | public void testHashCodeIsDependentOnResources() { 76 | ResourceRepresentation>> updatedResource = 77 | resource.withRepresentation("testsub2", ResourceRepresentation.empty("/subtest2")); 78 | assertThat(resource).isEqualTo(otherResource); 79 | assertThat(updatedResource.hashCode()).isNotEqualTo(resourceHashCode); 80 | } 81 | 82 | @Test 83 | public void testEqualsIsDependentOnNamespaces() { 84 | ResourceRepresentation>> updatedResource = 85 | resource.withNamespace("testns2", "http://example.com/test2/{rel}"); 86 | assertThat(resource).isEqualTo(otherResource); 87 | assertThat(updatedResource).isNotEqualTo(otherResource); 88 | } 89 | 90 | @Test 91 | public void testEqualsIsDependentOnLinks() { 92 | ResourceRepresentation>> updatedResource = 93 | resource.withLink("testlink2", "http://example.com/link2"); 94 | assertThat(resource).isEqualTo(otherResource); 95 | assertThat(updatedResource).isNotEqualTo(otherResource); 96 | } 97 | 98 | @Test 99 | public void testEqualsIsDependentOnProperties() { 100 | ResourceRepresentation>> updatedResource = 101 | resource.map(values -> values.put("proptest2", some("value2"))); 102 | assertThat(resource).isEqualTo(otherResource); 103 | assertThat(updatedResource).isNotEqualTo(otherResource); 104 | } 105 | 106 | @Test 107 | public void testEqualsIsDependentOnNullProperties() { 108 | ResourceRepresentation>> updatedResource = 109 | resource.map(values -> values.put("othernullprop", none())); 110 | assertThat(resource).isEqualTo(otherResource); 111 | assertThat(updatedResource).isNotEqualTo(otherResource); 112 | } 113 | 114 | @Test 115 | public void testEqualsIsDependentOnResources() { 116 | ResourceRepresentation>> updatedResource = 117 | resource.withRepresentation("testsub2", ResourceRepresentation.empty("/subtest2")); 118 | assertThat(resource).isEqualTo(otherResource); 119 | assertThat(updatedResource).isNotEqualTo(otherResource); 120 | } 121 | 122 | @Test 123 | public void testToStringRendersSelfHref() { 124 | String toString = 125 | ResourceRepresentation.empty().withLink("self", "http://localhost/test").toString(); 126 | assertThat(toString).isEqualTo(""); 127 | } 128 | 129 | @Test 130 | public void testToStringRendersHashCode() { 131 | String toString = ResourceRepresentation.empty().toString(); 132 | assertThat(toString).matches(""); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/ResourceRepresentationTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import com.damnhandy.uri.template.UriTemplate; 4 | import com.jayway.jsonpath.JsonPath; 5 | import com.theoryinpractise.halbuilder5.json.JsonRepresentationReader; 6 | import com.theoryinpractise.halbuilder5.json.JsonRepresentationWriter; 7 | import io.vavr.collection.List; 8 | import io.vavr.Function1; 9 | import io.vavr.Function2; 10 | import io.vavr.collection.HashMap; 11 | import io.vavr.control.Option; 12 | import okio.ByteString; 13 | import org.testng.annotations.Test; 14 | 15 | import java.io.StringReader; 16 | import java.util.Collections; 17 | import java.util.Map; 18 | 19 | import static com.google.common.truth.Truth.assertThat; 20 | import static com.google.common.truth.Truth.assertWithMessage; 21 | import static com.theoryinpractise.halbuilder5.Support.defaultObjectMapper; 22 | import static com.theoryinpractise.halbuilder5.json.JsonRepresentationReader.readByteStringAs; 23 | 24 | public class ResourceRepresentationTest { 25 | 26 | private String noOp(String event) { 27 | return ""; 28 | } 29 | 30 | @Test 31 | public void testEmptyRepresentationIsEmpty() { 32 | assertThat(ResourceRepresentation.empty().isEmpty()).isTrue(); 33 | } 34 | 35 | @Test 36 | public void testEmptyRepresentationRendersJsonCorrectly() { 37 | ResourceRepresentation resource = ResourceRepresentation.empty().withLink("foo", "/foo"); 38 | JsonRepresentationWriter.create().print(resource); 39 | } 40 | 41 | @Test 42 | public void testNonEmptyRepresentationIsNotEmpty() { 43 | assertThat(ResourceRepresentation.create("value").isEmpty()).isFalse(); 44 | } 45 | 46 | @Test 47 | public void testRepresentationLinks() { 48 | assertThat( 49 | ResourceRepresentation.create("value") 50 | .withLink("link", "/link") 51 | .getLinkByRel("link") 52 | .map(Links::getHref) 53 | .get()) 54 | .isEqualTo("/link"); 55 | 56 | assertThat( 57 | ResourceRepresentation.create("/self", "value") 58 | .withLinks( 59 | List.of(Links.create("link1", "/link1"), Links.create("link2", "/link2"))) 60 | .getLinkByRel(ResourceRepresentation.SELF) 61 | .map(Links::getHref) 62 | .get()) 63 | .isEqualTo("/self"); 64 | } 65 | 66 | @Test 67 | public void testMultipleEmbeddedRepresentations() { 68 | 69 | Account account = Account.of("0101232", "Test Account"); 70 | 71 | ResourceRepresentation accountRep = 72 | ResourceRepresentation.create("/somewhere", account) 73 | .withLink( 74 | "bible-verse", 75 | "https://www.bible.com/bible/1/mat.11.28", 76 | HashMap.of("content-type", "text/html")); 77 | 78 | Account subAccountA = Account.of("87912312-a", "Sub Account A"); 79 | ResourceRepresentation subAccountRepA = 80 | ResourceRepresentation.create("/subaccount/a", subAccountA); 81 | 82 | Account subAccountB = Account.of("87912312-b", "Sub Account B"); 83 | ResourceRepresentation subAccountRepB = 84 | ResourceRepresentation.create("/subaccount/a", subAccountB); 85 | 86 | accountRep = accountRep.withRepresentation("bank:associated-account", subAccountRepA); 87 | accountRep = accountRep.withRepresentation("bank:associated-account", subAccountRepB); 88 | 89 | JsonRepresentationWriter jsonRepresentationWriter = JsonRepresentationWriter.create(); 90 | String representation = jsonRepresentationWriter.print(accountRep).utf8(); 91 | 92 | String accountNumber = 93 | jsonPath(representation, "$['_embedded']['bank:associated-account'][1]['accountNumber']"); 94 | 95 | assertThat(accountNumber).isEqualTo("87912312-b"); 96 | } 97 | 98 | private String jsonPath(String json, String path) { 99 | return JsonPath.parse(json).read(path); 100 | } 101 | 102 | @Test 103 | public void testEmbeddedRepresentationNaturalOrdering() { 104 | ResourceRepresentation resource = 105 | ResourceRepresentation.empty() 106 | .withLink("foo", "/foo/1") 107 | .withRepresentation("foo", ResourceRepresentation.empty().withLink("self", "/foo/1")) 108 | .withLink("foo", "/foo/2") 109 | .withRepresentation("foo", ResourceRepresentation.empty().withLink("self", "/foo/2")) 110 | .withLink("foo", "/foo/3") 111 | .withRepresentation("foo", ResourceRepresentation.empty().withLink("self", "/foo/3")); 112 | 113 | JsonRepresentationWriter jsonRepresentationWriter = JsonRepresentationWriter.create(); 114 | String representation = jsonRepresentationWriter.print(resource).utf8(); 115 | 116 | System.out.println("java map"); 117 | System.out.println(resource.getResources().toJavaMap()); 118 | 119 | System.out.println("vavr list"); 120 | System.out.println(resource.getResources().toList()); 121 | 122 | System.out.println("vavr multimap"); 123 | System.out.println(resource.getResources()); 124 | 125 | System.out.println("vavr multimap->foo"); 126 | System.out.println(resource.getResources().get("foo")); 127 | 128 | System.out.println(representation); 129 | 130 | assertThat(jsonPath(representation, "$['_links']['foo'][0]['href']")).isEqualTo("/foo/1"); 131 | assertThat(jsonPath(representation, "$['_links']['foo'][1]['href']")).isEqualTo("/foo/2"); 132 | assertThat(jsonPath(representation, "$['_links']['foo'][2]['href']")).isEqualTo("/foo/3"); 133 | 134 | assertThat(jsonPath(representation, "$['_embedded']['foo'][0]['_links']['self']['href']")) 135 | .isEqualTo("/foo/1"); 136 | assertThat(jsonPath(representation, "$['_embedded']['foo'][1]['_links']['self']['href']")) 137 | .isEqualTo("/foo/2"); 138 | assertThat(jsonPath(representation, "$['_embedded']['foo'][2]['_links']['self']['href']")) 139 | .isEqualTo("/foo/3"); 140 | } 141 | 142 | @Test 143 | public void testBasicRepresentationUsage() { 144 | 145 | Account account = Account.of("0101232", "Test Account"); 146 | 147 | ResourceRepresentation accountRep = 148 | ResourceRepresentation.create("/somewhere", account) 149 | .withLink( 150 | "bible-verse", 151 | "https://www.bible.com/bible/1/mat.11.28", 152 | HashMap.of("content-type", "text/html")); 153 | 154 | accountRep.getLinks().forEach(link -> System.out.println(" *** " + link.toString())); 155 | 156 | assertThat(accountRep.get().name()).isEqualTo("Test Account"); 157 | 158 | ResourceRepresentation nameRep = accountRep.map(Account::name); 159 | assertThat(nameRep.get()).isEqualTo("Test Account"); 160 | 161 | int lengthOfName = accountRep.transform(a -> a.name().length()); 162 | 163 | assertThat(lengthOfName).isEqualTo(12); 164 | 165 | Account subAccount = Account.of("87912312", "Sub Account"); 166 | ResourceRepresentation subAccountRep = 167 | ResourceRepresentation.create("/subaccount", subAccount); 168 | 169 | ResourceRepresentation accountRepWithLinks = 170 | accountRep.withRepresentation("bank:associated-account", subAccountRep); 171 | 172 | JsonRepresentationWriter jsonRepresentationWriter = JsonRepresentationWriter.create(); 173 | 174 | ByteString representation = jsonRepresentationWriter.print(accountRepWithLinks); 175 | 176 | System.out.println(representation.utf8()); 177 | 178 | ResourceRepresentation byteStringResourceRepresentation = 179 | JsonRepresentationReader.create(defaultObjectMapper()) 180 | .read(new StringReader(representation.utf8())); 181 | 182 | ResourceRepresentation readRepresentation = 183 | byteStringResourceRepresentation.map( 184 | readByteStringAs(defaultObjectMapper(), Map.class, () -> Collections.emptyMap())); 185 | 186 | assertWithMessage("read representation should not be null") 187 | .that(readRepresentation) 188 | .isNotNull(); 189 | Map readValue = readRepresentation.get(); 190 | assertWithMessage("account name should be Test Account") 191 | .that(readValue.get("name")) 192 | .isEqualTo("Test Account"); 193 | 194 | ResourceRepresentation readAccountRepresentation = 195 | byteStringResourceRepresentation.map( 196 | readByteStringAs(defaultObjectMapper(), Account.class, () -> Account.of("", ""))); 197 | 198 | assertWithMessage("read representation should not be null") 199 | .that(readRepresentation) 200 | .isNotNull(); 201 | Account readAccount = readAccountRepresentation.get(); 202 | assertWithMessage("account name should be Test Account") 203 | .that(readAccount.name()) 204 | .isEqualTo("Test Account"); 205 | 206 | Option subLink = 207 | accountRepWithLinks 208 | .getResourcesByRel("bank:associated-account") 209 | .headOption() 210 | .flatMap(ResourceRepresentation::getResourceLink) 211 | .map(Links::getHref); 212 | 213 | System.out.println(subLink); 214 | 215 | Function1 deleteFunction = 216 | linkFunction(accountRepWithLinks, "self", this::deleteResource); 217 | 218 | Function2, String, String> deleteRepFunction = 219 | repFunction("self", this::deleteResource); 220 | 221 | System.out.println(deleteFunction.apply("click-event")); 222 | System.out.println(deleteRepFunction.apply(accountRepWithLinks, "click-event-on-rep")); 223 | 224 | UriTemplate template = 225 | UriTemplate.buildFromTemplate("http://api.smxemail.com/api/sp") 226 | .literal("/mailbox") 227 | .path("mailbox") 228 | .literal("/updateAlias") 229 | .build(); 230 | 231 | System.out.println(template.getTemplate()); 232 | 233 | System.out.println( 234 | template.expand(HashMap.of("mailbox", "greg@amer.com").toJavaMap())); 235 | } 236 | 237 | @Test 238 | public void testMultipleLinks() { 239 | ResourceRepresentation> withLinks = 240 | ResourceRepresentation.create("/self", HashMap.empty()) 241 | .withLinks(List.of(Links.create("link1", "/link1"), Links.create("link2", "/link2"))); 242 | 243 | JsonRepresentationWriter jsonRepresentationWriter = JsonRepresentationWriter.create(); 244 | ByteString representation = jsonRepresentationWriter.print(withLinks); 245 | 246 | assertThat(withLinks.getLinks()).hasSize(3); 247 | } 248 | 249 | private String deleteResource(Link link, String event) { 250 | System.out.printf("Deleting %s due to %s\n", Links.getHref(link), event); 251 | return String.format("deleted %s", Links.getHref(link)); 252 | } 253 | 254 | Function2, String, String> repFunction( 255 | String rel, Function2 fn) { 256 | return (rep, event) -> 257 | rep.getLinkByRel(rel).map(link -> fn.apply(link, event)).getOrElse(() -> noOp(event)); 258 | } 259 | 260 | Function1 linkFunction( 261 | ResourceRepresentation rep, String rel, Function2 fn) { 262 | return rep.getLinkByRel(rel).map(link -> fn.curried().apply(link)).getOrElse(this::noOp); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/test/java/com/theoryinpractise/halbuilder5/SingleLinksTest.java: -------------------------------------------------------------------------------- 1 | package com.theoryinpractise.halbuilder5; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | public class SingleLinksTest { 6 | 7 | @Test 8 | public void testDuplicateSingleLinksFails() { 9 | 10 | try { 11 | ResourceRepresentation.empty("/foo") 12 | .withRel(Rels.singleton("bar")) 13 | .withLink("bar", "/bar") 14 | .withLink("bar", "/bar"); 15 | 16 | throw new AssertionError("This should have failed with an IllegalStateException.)"); 17 | 18 | } catch (IllegalStateException exected) { 19 | // 20 | } 21 | } 22 | 23 | @Test(expectedExceptions = IllegalStateException.class) 24 | public void testDuplicateSingleEmbedFails() { 25 | ResourceRepresentation.empty("/foo") 26 | .withRel(Rels.singleton("bar")) 27 | .withRepresentation("bar", ResourceRepresentation.empty()) 28 | .withRepresentation("bar", ResourceRepresentation.empty()); 29 | } 30 | } 31 | --------------------------------------------------------------------------------