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 &#167; 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}