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}