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.jsonapi;
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 the top-level JSON:API document as defined by
030 * <a href="https://jsonapi.org/format/#document-top-level">JSON:API v1.1 &#167; Document Structure</a>.
031 *
032 * <p>
033 * Carries the standard six top-level members: {@code data}, {@code errors}, {@code meta}, {@code jsonapi},
034 * {@code links}, {@code included}.
035 *
036 * <h5 class='topic'>Mutual exclusion</h5>
037 *
038 * <p>
039 * Per spec, a document MUST contain at least one of {@code data}/{@code errors}/{@code meta}, and {@code data}
040 * and {@code errors} MUST NOT coexist. The {@link #validate()} helper checks both rules and throws
041 * {@link IllegalStateException} on violation.
042 *
043 * <h5 class='topic'>The polymorphic {@code data} member</h5>
044 *
045 * <p>
046 * The top-level {@code data} can be {@code null}, a single {@link JsonApiResource}, or an array of them. We
047 * model {@code data} as a plain {@code Object} property; the writer chooses single-vs-array shape based on
048 * which value was set. Typed setters {@link #setData(JsonApiResource)} and {@link #setData(List)} are provided
049 * for convenience.
050 *
051 * <h5 class='section'>See Also:</h5><ul>
052 *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
053 * </ul>
054 */
055@Marshalled
056public class JsonApiDocument {
057
058   private Object data;
059   private List<JsonApiError> errors;
060   private Map<String,Object> meta;
061   private JsonApiVersion jsonapi;
062   private Map<String,Object> links;
063   private List<JsonApiResource> included;
064
065   /**
066    * Default constructor.
067    */
068   public JsonApiDocument() { /* intentionally empty */ }
069
070   /**
071    * Bean property getter:  <property>data</property>.
072    *
073    * <p>
074    * Top-level resource linkage. May be {@code null}, a single {@link JsonApiResource}, or a
075    * {@code List&lt;JsonApiResource&gt;}.
076    *
077    * @return The value of the <property>data</property> property, or <jk>null</jk> if it is not set.
078    */
079   public Object getData() { return data; }
080
081   /**
082    * Bean property setter:  <property>data</property>.
083    *
084    * @param value The new value (single resource, list of resources, or {@code null} to unset).
085    * @return This object.
086    */
087   public JsonApiDocument setData(Object value) {
088      data = value;
089      return this;
090   }
091
092   /**
093    * Typed setter for single-resource primary data.
094    *
095    * @param value The resource.  Can be <jk>null</jk> to unset the property.
096    * @return This object.
097    */
098   public JsonApiDocument setData(JsonApiResource value) {
099      data = value;
100      return this;
101   }
102
103   /**
104    * Typed setter for to-many primary data.
105    *
106    * @param value The list of resources.  Can be <jk>null</jk> to unset the property.
107    * @return This object.
108    */
109   public JsonApiDocument setData(List<JsonApiResource> value) {
110      data = value;
111      return this;
112   }
113
114   /**
115    * Bean property getter:  <property>errors</property>.
116    *
117    * @return The value of the <property>errors</property> property, or <jk>null</jk> if it is not set.
118    */
119   public List<JsonApiError> getErrors() { return u(errors); }
120
121   /**
122    * Bean property setter:  <property>errors</property>.
123    *
124    * @param value The new value for this property.  Can be <jk>null</jk> to unset the property.
125    * @return This object.
126    */
127   public JsonApiDocument setErrors(List<JsonApiError> value) {
128      errors = value;
129      return this;
130   }
131
132   /**
133    * Bean property setter:  <property>errors</property>.
134    *
135    * @param value The new value for this property.
136    * @return This object.
137    */
138   public JsonApiDocument setErrors(JsonApiError...value) {
139      errors = list(value);
140      return this;
141   }
142
143   /**
144    * Bean property appender:  <property>errors</property>.
145    *
146    * @param value The error objects to append.
147    * @return This object.
148    */
149   public JsonApiDocument addErrors(JsonApiError...value) {
150      if (errors == null)
151         errors = list();
152      Collections.addAll(errors, value);
153      return this;
154   }
155
156   /**
157    * Bean property appender:  <property>errors</property>.
158    *
159    * @param value The error objects to append.
160    * @return This object.
161    */
162   public JsonApiDocument addErrors(Collection<JsonApiError> value) {
163      if (errors == null)
164         errors = list();
165      errors.addAll(value);
166      return this;
167   }
168
169   /**
170    * Bean property getter:  <property>meta</property>.
171    *
172    * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
173    */
174   public Map<String,Object> getMeta() { return u(meta); }
175
176   /**
177    * Bean property setter:  <property>meta</property>.
178    *
179    * @param value The new value for this property.  Can be <jk>null</jk> to unset the property.
180    * @return This object.
181    */
182   public JsonApiDocument setMeta(Map<String,Object> value) {
183      meta = value;
184      return this;
185   }
186
187   /**
188    * Convenience method to add a single meta entry.
189    *
190    * @param name The meta key.  Can be <jk>null</jk> ({@link LinkedHashMap} tolerates a <jk>null</jk> key).
191    * @param value The meta value.  Can be <jk>null</jk> (stored as <jk>null</jk>).
192    * @return This object.
193    */
194   public JsonApiDocument putMeta(String name, Object value) {
195      if (meta == null)
196         meta = map();
197      meta.put(name, value);
198      return this;
199   }
200
201   /**
202    * Bean property getter:  <property>jsonapi</property>.
203    *
204    * @return The value of the <property>jsonapi</property> property, or <jk>null</jk> if it is not set.
205    */
206   public JsonApiVersion getJsonapi() { return jsonapi; }
207
208   /**
209    * Bean property setter:  <property>jsonapi</property>.
210    *
211    * @param value The new value for this property.  Can be <jk>null</jk> to unset the property.
212    * @return This object.
213    */
214   public JsonApiDocument setJsonapi(JsonApiVersion value) {
215      jsonapi = value;
216      return this;
217   }
218
219   /**
220    * Bean property getter:  <property>links</property>.
221    *
222    * <p>
223    * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
224    *
225    * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
226    */
227   @BeanProp(value="links")
228   @Swap(JsonApiLinkOrStringSwap.class)
229   public Map<String,Object> getLinks() { return u(links); }
230
231   /**
232    * Bean property setter:  <property>links</property>.
233    *
234    * @param value The new value for this property.  Can be <jk>null</jk> to unset the property.
235    * @return This object.
236    */
237   @BeanProp(value="links")
238   @Swap(JsonApiLinkOrStringSwap.class)
239   public JsonApiDocument setLinks(Map<String,Object> value) {
240      links = value;
241      return this;
242   }
243
244   /**
245    * Convenience method to add a single link.
246    *
247    * @param name The link name.  Can be <jk>null</jk> ({@link LinkedHashMap} tolerates a <jk>null</jk> key).
248    * @param value The link value (a {@link String} URL or a {@link JsonApiLink} object).  Can be <jk>null</jk> (stored as <jk>null</jk>).
249    * @return This object.
250    */
251   public JsonApiDocument putLink(String name, Object value) {
252      if (links == null)
253         links = map();
254      links.put(name, value);
255      return this;
256   }
257
258   /**
259    * Bean property getter:  <property>included</property>.
260    *
261    * @return The value of the <property>included</property> property, or <jk>null</jk> if it is not set.
262    */
263   public List<JsonApiResource> getIncluded() { return u(included); }
264
265   /**
266    * Bean property setter:  <property>included</property>.
267    *
268    * @param value The new value for this property.  Can be <jk>null</jk> to unset the property.
269    * @return This object.
270    */
271   public JsonApiDocument setIncluded(List<JsonApiResource> value) {
272      included = value;
273      return this;
274   }
275
276   /**
277    * Bean property setter:  <property>included</property>.
278    *
279    * @param value The new value for this property.
280    * @return This object.
281    */
282   public JsonApiDocument setIncluded(JsonApiResource...value) {
283      included = list(value);
284      return this;
285   }
286
287   /**
288    * Bean property appender:  <property>included</property>.
289    *
290    * @param value The resource objects to append.
291    * @return This object.
292    */
293   public JsonApiDocument addIncluded(JsonApiResource...value) {
294      if (included == null)
295         included = list();
296      Collections.addAll(included, value);
297      return this;
298   }
299
300   /**
301    * Bean property appender:  <property>included</property>.
302    *
303    * @param value The resource objects to append.
304    * @return This object.
305    */
306   public JsonApiDocument addIncluded(Collection<JsonApiResource> value) {
307      if (included == null)
308         included = list();
309      included.addAll(value);
310      return this;
311   }
312
313   /**
314    * Validates document-level invariants per the JSON:API spec.
315    *
316    * <p>
317    * Throws if {@code data} and {@code errors} are both present, or if none of {@code data}/{@code errors}/{@code meta}
318    * is present.
319    *
320    * @return This object.
321    * @throws IllegalStateException If the document violates a top-level invariant.
322    */
323   public JsonApiDocument validate() {
324      if (data != null && errors != null)
325         throw isex("JSON:API document must not contain both 'data' and 'errors'.");
326      if (data == null && errors == null && meta == null)
327         throw isex("JSON:API document must contain at least one of 'data', 'errors', or 'meta'.");
328      return this;
329   }
330
331   @Override /* Overridden from Object */
332   public String toString() {
333      return Json.of(this);
334   }
335}