001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.bean.hal; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.util.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.marshaller.*; 027 028/** 029 * Represents a HAL Resource Object as defined by 030 * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-4">draft-kelly-json-hal-08 § 4</a>. 031 * 032 * <p> 033 * A HAL Resource carries three pieces of state: 034 * <ul class='spaced-list'> 035 * <li>The reserved {@code _links} member - a map of relation name to either a single {@link HalLink} 036 * or a {@link HalLinkArray}. 037 * <li>The reserved {@code _embedded} member - a map of relation name to either a single {@link HalResource} 038 * or a {@link HalResourceArray} (sub-resources). HAL embeddings are conceptually a <i>tree</i>; cyclic 039 * {@code _embedded} graphs are rejected by Juneau's {@code BeanTraverseContext} by default. 040 * <li>Arbitrary payload fields, set via the dynamic {@link #set(String,Object)} method (uses the 041 * {@code @BeanProp("*")} extra-properties triplet, the same pattern used by {@code OpenApiElement} and 042 * {@code SwaggerElement}). 043 * </ul> 044 * 045 * <h5 class='section'>Example:</h5> 046 * <p class='bjava'> 047 * HalResource <jv>order</jv> = <jk>new</jk> HalResource() 048 * .addLink(<js>"self"</js>, <jk>new</jk> HalLink().setHref(<js>"/orders/123"</js>)) 049 * .addLinks(<js>"curies"</js>, 050 * <jk>new</jk> HalLink().setName(<js>"acme"</js>).setHref(<js>"https://acme.example/{rel}"</js>).setTemplated(<jk>true</jk>)) 051 * .set(<js>"total"</js>, 99.50); 052 * 053 * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>order</jv>); 054 * </p> 055 * 056 * <h5 class='section'>See Also:</h5><ul> 057 * <li class='link'><a href="https://stateless.group/hal_specification.html">HAL Specification</a> 058 * <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a> 059 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a> 060 * </ul> 061 */ 062@Marshalled 063@SuppressWarnings({ 064 "java:S116", // Field names mirror HAL spec. 065 "java:S1192" // Duplicated literals (property names) read more clearly inline than as constants 066}) 067public class HalResource { 068 069 private Map<String,Object> _links; 070 private Map<String,Object> _embedded; 071 private Map<String,Object> properties; 072 073 /** 074 * Default constructor. 075 */ 076 public HalResource() { 077 /* intentionally empty — required public no-arg constructor for bean creation/decoding */ 078 } 079 080 /** 081 * Bean property getter: <property>_links</property>. 082 * 083 * <p> 084 * The reserved {@code _links} member. Each map value is either a single {@link HalLink} (rendered as a JSON 085 * object on the wire) or a {@link HalLinkArray} (rendered as a JSON array). 086 * 087 * @return The value of the <property>_links</property> property, or <jk>null</jk> if it is not set. 088 */ 089 @BeanProp("_links") 090 @Swap(HalLinkOrArraySwap.class) 091 public Map<String,Object> getLinks() { 092 return u(_links); 093 } 094 095 /** 096 * Bean property setter: <property>_links</property>. 097 * 098 * @param value The new value for this property. Can be <jk>null</jk> to unset. 099 * @return This object. 100 */ 101 @BeanProp("_links") 102 @Swap(HalLinkOrArraySwap.class) 103 public HalResource setLinks(Map<String,Object> value) { 104 _links = value; 105 return this; 106 } 107 108 /** 109 * Convenience method that adds a single link under the given relation. 110 * 111 * <p> 112 * If a value already exists for {@code relation}, it is replaced with the supplied single link. To accumulate 113 * multiple links under the same relation use {@link #addLinks(String, HalLink...)} instead. 114 * 115 * @param relation The link relation (e.g. {@code "self"}, {@code "next"}, {@code "curies"}). Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 116 * @param value The link to set. Can be <jk>null</jk> (stored as a <jk>null</jk> entry under the relation). 117 * @return This object. 118 */ 119 public HalResource addLink(String relation, HalLink value) { 120 reqnn("relation", relation); 121 if (_links == null) 122 _links = map(); 123 _links.put(relation, value); 124 return this; 125 } 126 127 /** 128 * Convenience method that appends one or more links to a {@link HalLinkArray} stored under {@code relation}. 129 * 130 * <p> 131 * If the current value at {@code relation} is a single {@link HalLink}, it is promoted to a {@link HalLinkArray} 132 * containing the original link and the appended ones. 133 * 134 * @param relation The link relation. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 135 * @param values The links to append. 136 * @return This object. 137 */ 138 public HalResource addLinks(String relation, HalLink...values) { 139 reqnn("relation", relation); 140 if (_links == null) 141 _links = map(); 142 var existing = _links.get(relation); 143 HalLinkArray arr; 144 if (existing instanceof HalLinkArray existing2) { 145 arr = existing2; 146 } else if (existing instanceof HalLink existing2) { 147 arr = new HalLinkArray(existing2); 148 _links.put(relation, arr); 149 } else { 150 arr = new HalLinkArray(); 151 _links.put(relation, arr); 152 } 153 arr.addAll(values); 154 return this; 155 } 156 157 /** 158 * Bean property getter: <property>_embedded</property>. 159 * 160 * <p> 161 * The reserved {@code _embedded} member. Each map value is either a single {@link HalResource} or a 162 * {@link HalResourceArray}. 163 * 164 * @return The value of the <property>_embedded</property> property, or <jk>null</jk> if it is not set. 165 */ 166 @BeanProp("_embedded") 167 @Swap(HalResourceOrArraySwap.class) 168 public Map<String,Object> getEmbedded() { 169 return u(_embedded); 170 } 171 172 /** 173 * Bean property setter: <property>_embedded</property>. 174 * 175 * <p> 176 * Note: HAL embeddings are conceptually a tree, not a graph. Juneau's {@code BeanTraverseContext} throws on 177 * cyclic references by default, which matches the spec. 178 * 179 * @param value The new value for this property. Can be <jk>null</jk> to unset. 180 * @return This object. 181 */ 182 @BeanProp("_embedded") 183 @Swap(HalResourceOrArraySwap.class) 184 public HalResource setEmbedded(Map<String,Object> value) { 185 _embedded = value; 186 return this; 187 } 188 189 /** 190 * Convenience method that embeds a single sub-resource under the given relation. 191 * 192 * @param relation The relation name. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 193 * @param value The sub-resource. Can be <jk>null</jk> (stored as a <jk>null</jk> entry under the relation). 194 * @return This object. 195 */ 196 public HalResource addEmbedded(String relation, HalResource value) { 197 reqnn("relation", relation); 198 if (_embedded == null) 199 _embedded = map(); 200 _embedded.put(relation, value); 201 return this; 202 } 203 204 /** 205 * Convenience method that appends one or more sub-resources to a {@link HalResourceArray} stored under 206 * {@code relation}. 207 * 208 * @param relation The relation name. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 209 * @param values The sub-resources to append. 210 * @return This object. 211 */ 212 public HalResource addEmbedded(String relation, HalResource...values) { 213 reqnn("relation", relation); 214 if (_embedded == null) 215 _embedded = map(); 216 var existing = _embedded.get(relation); 217 HalResourceArray arr; 218 if (existing instanceof HalResourceArray existing2) { 219 arr = existing2; 220 } else if (existing instanceof HalResource existing2) { 221 arr = new HalResourceArray(existing2); 222 _embedded.put(relation, arr); 223 } else { 224 arr = new HalResourceArray(); 225 _embedded.put(relation, arr); 226 } 227 arr.addAll(values); 228 return this; 229 } 230 231 /** 232 * Generic property keyset. 233 * 234 * @return All the payload (non-{@code _links}, non-{@code _embedded}) property names on this resource. Never <jk>null</jk>. 235 */ 236 @BeanProp("*") 237 public Set<String> extraKeys() { 238 return properties == null ? Collections.emptySet() : u(properties.keySet()); 239 } 240 241 /** 242 * Generic property getter. 243 * 244 * @param property The property name. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 245 * @return The property value, or <jk>null</jk> if the property is not set. 246 */ 247 @BeanProp("*") 248 public Object get(String property) { 249 reqnn("property", property); 250 return o(properties).map(x -> x.get(property)).orElse(null); 251 } 252 253 /** 254 * Generic property setter. 255 * 256 * <p> 257 * Use this to set arbitrary payload fields alongside {@code _links} and {@code _embedded}. 258 * 259 * @param property The property name. Must not be <jk>null</jk>, or an {@link IllegalArgumentException} is thrown. 260 * @param value The new value for the property. Can be <jk>null</jk> (stored as <jk>null</jk>). 261 * @return This object. 262 */ 263 @BeanProp("*") 264 public HalResource set(String property, Object value) { 265 reqnn("property", property); 266 if (properties == null) 267 properties = map(); 268 properties.put(property, value); 269 return this; 270 } 271 272 @Override /* Overridden from Object */ 273 public String toString() { 274 return Json.of(this); 275 } 276}