001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim2.server;
019
020import com.fasterxml.jackson.core.JsonGenerator;
021import com.fasterxml.jackson.databind.JsonNode;
022import com.fasterxml.jackson.databind.node.ObjectNode;
023import com.unboundid.scim2.common.ScimResource;
024import com.unboundid.scim2.common.annotations.NotNull;
025import com.unboundid.scim2.common.annotations.Nullable;
026import com.unboundid.scim2.common.utils.JsonUtils;
027
028import java.io.IOException;
029import java.io.OutputStream;
030import java.util.Iterator;
031import java.util.Map;
032import java.util.concurrent.atomic.AtomicBoolean;
033import java.util.concurrent.atomic.AtomicInteger;
034
035/**
036 * An interface for writing list/query results using the SCIM ListResponse
037 * container to an OutputStream.
038 */
039public class ListResponseWriter<T extends ScimResource>
040{
041  @NotNull
042  private final JsonGenerator jsonGenerator;
043
044  @NotNull
045  private final AtomicBoolean startedResourcesArray = new AtomicBoolean();
046
047  @NotNull
048  private final AtomicBoolean sentTotalResults = new AtomicBoolean();
049
050  @NotNull
051  private final AtomicInteger resultsSent = new AtomicInteger();
052
053  @NotNull
054  private ObjectNode deferredFields;
055
056  /**
057   * Create a new ListResponseOutputStream that will write to the provided
058   * output stream.
059   *
060   * @param outputStream The output stream to write to.
061   * @throws IOException If an exception occurs while writing to the output
062   * stream.
063   */
064  public ListResponseWriter(@NotNull final OutputStream outputStream)
065      throws IOException
066  {
067    jsonGenerator =
068        JsonUtils.getObjectReader().getFactory().createGenerator(outputStream);
069    deferredFields = JsonUtils.getJsonNodeFactory().objectNode();
070  }
071
072  /**
073   * Start the response.
074   *
075   * @throws IOException If an exception occurs while writing to the output
076   * stream.
077   */
078  void startResponse() throws IOException
079  {
080    jsonGenerator.writeStartObject();
081    jsonGenerator.writeArrayFieldStart("schemas");
082    jsonGenerator.writeString(
083        "urn:ietf:params:scim:api:messages:2.0:ListResponse");
084    jsonGenerator.writeEndArray();
085  }
086
087  /**
088   * End the response.
089   *
090   * @throws IOException If an exception occurs while writing to the output
091   * stream.
092   */
093  void endResponse() throws IOException
094  {
095    if(!sentTotalResults.get() && !deferredFields.has("totalResults"))
096    {
097      // The total results was never set. Set it to the calculated one.
098      totalResults(resultsSent.get());
099    }
100    if(startedResourcesArray.get())
101    {
102      // Close the resources array if currently writing it.
103      jsonGenerator.writeEndArray();
104    }
105
106    Iterator<Map.Entry<String, JsonNode>> i = deferredFields.fields();
107    while(i.hasNext())
108    {
109      Map.Entry<String, JsonNode> field = i.next();
110      jsonGenerator.writeObjectField(field.getKey(), field.getValue());
111    }
112    jsonGenerator.writeEndObject();
113    jsonGenerator.flush();
114    jsonGenerator.close();
115  }
116
117  /**
118   * Write the startIndex to the output stream immediately if no resources have
119   * been streamed, otherwise it will be written after the resources array.
120   *
121   * @param startIndex The startIndex to write.
122   * @throws IOException If an exception occurs while writing to the output
123   * stream.
124   */
125  public void startIndex(final int startIndex) throws IOException
126  {
127    if(startedResourcesArray.get())
128    {
129      deferredFields.put("startIndex", startIndex);
130    }
131    else
132    {
133      jsonGenerator.writeNumberField("startIndex", startIndex);
134    }
135  }
136
137  /**
138   * Write the itemsPerPage to the output stream immediately if no resources
139   * have been streamed, otherwise it will be written after the resources array.
140   *
141   * @param itemsPerPage The itemsPerPage to write.
142   * @throws IOException If an exception occurs while writing to the output
143   * stream.
144   */
145  public void itemsPerPage(final int itemsPerPage) throws IOException
146  {
147    if(startedResourcesArray.get())
148    {
149      deferredFields.put("itemsPerPage", itemsPerPage);
150    }
151    else
152    {
153      jsonGenerator.writeNumberField("itemsPerPage", itemsPerPage);
154    }
155  }
156
157  /**
158   * Write the totalResults to the output stream immediately if no resources
159   * have been streamed, otherwise it will be written after the resources array.
160   *
161   * @param totalResults The totalResults to write.
162   * @throws IOException If an exception occurs while writing to the output
163   * stream.
164   */
165  public void totalResults(final int totalResults) throws IOException
166  {
167    if(startedResourcesArray.get())
168    {
169      deferredFields.put("totalResults", totalResults);
170    }
171    else
172    {
173      jsonGenerator.writeNumberField("totalResults", totalResults);
174      sentTotalResults.set(true);
175    }
176  }
177
178  /**
179   * Write the result resource to the output stream immediately.
180   *
181   * @param scimResource The resource to write.
182   * @throws IOException If an exception occurs while writing to the output
183   * stream.
184   */
185  public void resource(@Nullable final T scimResource) throws IOException
186  {
187    if(startedResourcesArray.compareAndSet(false, true))
188    {
189      jsonGenerator.writeArrayFieldStart("Resources");
190    }
191    jsonGenerator.writeObject(scimResource);
192    resultsSent.incrementAndGet();
193  }
194}