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.marshall.oapi; 018 019import static org.apache.juneau.commons.httppart.HttpPartCollectionFormat.*; 020import static org.apache.juneau.commons.httppart.HttpPartDataType.*; 021import static org.apache.juneau.commons.httppart.HttpPartFormat.*; 022import static org.apache.juneau.commons.utils.ObjectUtils.*; 023import static org.apache.juneau.commons.utils.Shorts.*; 024import static org.apache.juneau.commons.utils.StringUtils.*; 025 026import java.io.*; 027import java.lang.reflect.*; 028import java.util.*; 029import java.util.function.*; 030 031import org.apache.juneau.commons.httppart.*; 032import org.apache.juneau.commons.lang.*; 033import org.apache.juneau.commons.utils.*; 034import org.apache.juneau.marshall.*; 035import org.apache.juneau.marshall.collections.*; 036import org.apache.juneau.marshall.httppart.*; 037import org.apache.juneau.marshall.serializer.*; 038import org.apache.juneau.marshall.uon.*; 039import org.apache.juneau.marshall.utils.*; 040 041/** 042 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}. 043 * 044 * <h5 class='section'>Notes:</h5><ul> 045 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 046 * </ul> 047 * 048 * <h5 class='section'>See Also:</h5><ul> 049 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/OpenApiSupport">OpenApi</a> 050 051 * </ul> 052 */ 053@SuppressWarnings({ 054 "java:S110", // Inheritance depth acceptable for this class hierarchy 055 "java:S3776", // Cognitive complexity acceptable for this specific logic 056 "rawtypes", // toList returns a raw List and getClassMetaForObject results are held as raw ClassMeta 057 "resource" // Resource management handled externally 058}) 059public class OpenApiSerializerSession extends UonSerializerSession { 060 061 /** 062 * Builder class. 063 */ 064 public static class Builder extends UonSerializerSession.Builder<Builder> { 065 066 private final OpenApiSerializer ctx; 067 068 /** 069 * Constructor 070 * 071 * @param ctx The context creating this session. 072 */ 073 protected Builder(OpenApiSerializer ctx) { 074 super(ctx); 075 this.ctx = ctx; 076 } 077 078 @Override 079 public OpenApiSerializerSession build() { 080 return new OpenApiSerializerSession(this); 081 } 082 083 } 084 085 private static class OapiStringBuilder { 086 static final AsciiSet EQ = AsciiSet.of("=\\"); 087 static final AsciiSet PIPE = AsciiSet.of("|\\"); 088 static final AsciiSet PIPE_OR_EQ = AsciiSet.of("|=\\"); 089 static final AsciiSet COMMA = AsciiSet.of(",\\"); 090 static final AsciiSet COMMA_OR_EQ = AsciiSet.of(",=\\"); 091 092 private final StringBuilder sb = new StringBuilder(); 093 private final HttpPartCollectionFormat cf; 094 private boolean first = true; 095 096 OapiStringBuilder(HttpPartCollectionFormat cf) { 097 this.cf = cf; 098 } 099 100 @Override 101 public String toString() { 102 return sb.toString(); 103 } 104 105 private void delim(HttpPartCollectionFormat cf) { 106 if (cf == PIPES) 107 sb.append('|'); 108 else if (cf == SSV) 109 sb.append(' '); 110 else if (cf == TSV) 111 sb.append('\t'); 112 else 113 sb.append(','); 114 } 115 116 OapiStringBuilder append(Object o) { 117 if (! first) 118 delim(cf); 119 first = false; 120 if (cf == PIPES) 121 sb.append(escapeChars(s(o), PIPE)); 122 else if (cf == SSV || cf == TSV) 123 sb.append(s(o)); 124 else 125 sb.append(escapeChars(s(o), COMMA)); 126 return this; 127 } 128 129 OapiStringBuilder append(Object key, Object val) { 130 if (! first) 131 delim(cf); 132 first = false; 133 if (cf == PIPES) 134 sb.append(escapeChars(s(key), PIPE_OR_EQ)).append('=').append(escapeChars(s(val), PIPE_OR_EQ)); 135 else if (cf == SSV || cf == TSV) 136 sb.append(escapeChars(s(key), EQ)).append('=').append(escapeChars(s(val), EQ)); 137 else 138 sb.append(escapeChars(s(key), COMMA_OR_EQ)).append('=').append(escapeChars(s(val), COMMA_OR_EQ)); 139 return this; 140 } 141 } 142 143 // Cache these for faster lookup 144 private static final MarshallingContext BC = MarshallingContext.DEFAULT; 145 private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class); 146 private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class); 147 private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class); 148 private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class); 149 private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class); 150 private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class); 151 152 private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class); 153 154 private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class); 155 private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT; 156 157 /** 158 * Creates a new builder for this object. 159 * 160 * @param ctx The context creating this session. 161 * @return A new builder. 162 */ 163 public static Builder create(OpenApiSerializer ctx) { 164 return new Builder(ctx); 165 } 166 167 private final OpenApiSerializer ctx; 168 169 /** 170 * Constructor. 171 * 172 * @param builder The builder for this object. 173 */ 174 protected OpenApiSerializerSession(Builder builder) { 175 super(builder); 176 ctx = builder.ctx; 177 } 178 179 @Override /* Overridden from PartSerializer */ 180 @SuppressWarnings({ 181 "java:S6541" // Single-threaded session contexts do not require synchronization 182 }) 183 public String write(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException { 184 185 ClassMeta<?> type = getClassMetaForObject(value); 186 if (type == null) 187 type = object(); 188 189 // Swap if necessary 190 var swap = type.getSwap(this); 191 if (nn(swap) && ! type.isDateOrCalendarOrTemporal()) { 192 value = swap(swap, value); 193 type = swap.getSwapClassMeta(this); 194 195 // If the getSwapClass() method returns Object, we need to figure out 196 // the actual type now. 197 if (type.isObject()) 198 type = getClassMetaForObject(value); 199 } 200 201 schema = coalesce(schema, DEFAULT_SCHEMA); 202 203 HttpPartDataType t = schema.getType(type); 204 205 HttpPartFormat f = schema.getFormat(type); 206 if (f == HttpPartFormat.NO_FORMAT) 207 f = ctx.getFormat(); 208 209 HttpPartCollectionFormat cf = schema.getCollectionFormat(); 210 if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT) 211 cf = ctx.getCollectionFormat(); 212 213 String out = null; 214 215 schema.validateOutput(value); 216 217 var spt = getClassMeta(schema.getParsedType()); 218 if (type.hasMutaterTo(spt) || spt.hasMutaterFrom(type)) { 219 value = toType(value, spt); 220 type = spt; 221 } 222 223 if (type.isUri()) { 224 value = getUriResolver().resolve(value); 225 type = string(); 226 } 227 228 if (nn(value)) { 229 230 if (t == STRING) { 231 232 if (f == BYTE) { 233 out = base64Encode(toType(value, CM_ByteArray)); 234 } else if (f == BINARY) { 235 out = toHex(toType(value, CM_ByteArray)); 236 } else if (f == BINARY_SPACED) { 237 out = toSpacedHex(toType(value, CM_ByteArray)); 238 } else if (f == DATE) { 239 out = Iso8601Utils.formatAsDate(value, type, getTimeZone()); 240 } else if (f == DATE_TIME) { 241 out = Iso8601Utils.formatAsDateTime(value, type, getTimeZone()); 242 } else if (f == HttpPartFormat.UON) { 243 out = super.write(partType, schema, value); 244 } else { 245 out = toType(value, string()); 246 } 247 248 } else if (t == BOOLEAN) { 249 250 out = s(toType(value, CM_Boolean)); 251 252 } else if (t == INTEGER) { 253 254 if (f == INT64) 255 out = s(toType(value, CM_Long)); 256 else 257 out = s(toType(value, CM_Integer)); 258 259 } else if (t == NUMBER) { 260 261 if (f == DOUBLE) 262 out = s(toType(value, CM_Double)); 263 else 264 out = s(toType(value, CM_Float)); 265 266 } else if (t == ARRAY) { 267 268 if (cf == HttpPartCollectionFormat.UONC) 269 out = super.write(partType, null, toList(partType, type, value, schema)); 270 else { 271 272 HttpPartSchema items = schema.getItems(); 273 ClassMeta<?> vt = getClassMetaForObject(value); 274 var sb = new OapiStringBuilder(cf); 275 276 if (type.isArray()) { 277 for (var i = 0; i < Array.getLength(value); i++) 278 sb.append(write(partType, items, Array.get(value, i))); 279 } else if (type.isCollection()) { 280 ((Collection<?>)value).forEach(x -> sb.append(write(partType, items, x))); 281 } else if (type.isStreamable()) { 282 forEachStreamableEntry(value, type, x -> sb.append(write(partType, items, x))); 283 } else if (vt.hasMutaterTo(String[].class)) { 284 String[] ss = toType(value, CM_StringArray); 285 for (var element : ss) 286 sb.append(write(partType, items, element)); 287 } else { 288 throw new SerializeException("Input is not a valid array type: " + type); 289 } 290 291 out = sb.toString(); 292 } 293 294 } else if (t == OBJECT) { 295 296 if (cf == HttpPartCollectionFormat.UONC) { 297 if (schema.hasProperties() && type.isMapOrBean()) 298 value = toMap(partType, type, value, schema); 299 out = super.write(partType, null, value); 300 301 } else if (type.isBean()) { 302 var sb = new OapiStringBuilder(cf); 303 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 304 HttpPartSchema schema2 = schema; 305 306 toBeanMap(value).forEachValue(checkNull, (pMeta, key, val, thrown) -> { 307 if (thrown == null) 308 sb.append(key, write(partType, schema2.getProperty(key), val)); 309 }); 310 out = sb.toString(); 311 312 } else if (type.isMap()) { 313 var sb = new OapiStringBuilder(cf); 314 HttpPartSchema schema2 = schema; 315 ((Map<?,?>)value).forEach((k, v) -> sb.append(k, write(partType, schema2.getProperty(s(k)), v))); 316 out = sb.toString(); 317 318 } else { 319 throw new SerializeException("Input is not a valid object type: " + type); 320 } 321 322 } else if (t == FILE) { 323 throw new SerializeException("File part not supported."); 324 325 } else if (t == NO_TYPE) { 326 // This should never be returned by HttpPartSchema.getType(ClassMeta). 327 throw new SerializeException("Invalid type."); 328 } 329 } 330 331 schema.validateInput(out); 332 if (out == null) 333 out = schema.getDefault(); 334 if (out == null) 335 out = "null"; 336 return out; 337 } 338 339 private List toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 340 if (s == null) 341 s = DEFAULT_SCHEMA; 342 var l = new JsonList(); 343 HttpPartSchema items = s.getItems(); 344 if (type.isArray()) { 345 for (var i = 0; i < Array.getLength(o); i++) 346 l.add(toObject(partType, Array.get(o, i), items)); 347 } else if (type.isCollection()) { 348 ((Collection<?>)o).forEach(x -> l.add(toObject(partType, x, items))); 349 } else if (type.isStreamable()) { 350 forEachStreamableEntry(o, type, x -> l.add(toObject(partType, x, items))); 351 } else { 352 l.add(toObject(partType, o, items)); 353 } 354 if (isSortCollections()) 355 return sort(l); 356 return l; 357 } 358 359 private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 360 if (s == null) 361 s = DEFAULT_SCHEMA; 362 var m = new JsonMap(); 363 if (type.isBean()) { 364 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 365 HttpPartSchema s2 = s; 366 toBeanMap(o).forEachValue(checkNull, (pMeta, key, val, thrown) -> { 367 if (thrown == null) 368 m.put(key, toObject(partType, val, s2.getProperty(key))); 369 }); 370 } else { 371 HttpPartSchema s2 = s; 372 ((Map<?,?>)o).forEach((k, v) -> m.put(s(k), toObject(partType, v, s2.getProperty(s(k))))); 373 } 374 if (isSortMaps()) 375 return sort(m); 376 return m; 377 } 378 379 private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 380 if (o == null) 381 return null; 382 if (s == null) 383 s = DEFAULT_SCHEMA; 384 ClassMeta cm = getClassMetaForObject(o); 385 HttpPartDataType t = s.getType(cm); 386 HttpPartFormat f = s.getFormat(cm); 387 HttpPartCollectionFormat cf = s.getCollectionFormat(); 388 389 if (t == STRING) { 390 if (f == BYTE) 391 return base64Encode(toType(o, CM_ByteArray)); 392 if (f == BINARY) 393 return toHex(toType(o, CM_ByteArray)); 394 if (f == BINARY_SPACED) 395 return toSpacedHex(toType(o, CM_ByteArray)); 396 if (f == DATE) 397 return toIsoDate(toType(o, CM_Calendar)); 398 if (f == DATE_TIME) 399 return toIsoDateTime(toType(o, CM_Calendar)); 400 return o; 401 } else if (t == ARRAY) { 402 List l = toList(partType, getClassMetaForObject(o), o, s); 403 if (cf == CSV) 404 return StringUtils.joine(l, ','); 405 if (cf == PIPES) 406 return StringUtils.joine(l, '|'); 407 if (cf == SSV) 408 return StringUtils.join(l, ' '); 409 if (cf == TSV) 410 return StringUtils.join(l, '\t'); 411 return l; 412 } else if (t == OBJECT) { 413 return toMap(partType, getClassMetaForObject(o), o, s); 414 } 415 416 return o; 417 } 418 419 private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException { 420 try { 421 return convertToType(in, type); 422 } catch (InvalidDataConversionException e) { 423 throw new SerializeException(e); 424 } 425 } 426 427 @Override /* Overridden from Serializer */ 428 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 429 try { 430 out.getWriter().write(write(HttpPartType.BODY, getSchema(), o)); 431 } catch (SchemaValidationException e) { 432 throw new SerializeException(e); 433 } 434 } 435}