001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.oapi; 014 015import static org.apache.juneau.httppart.HttpPartCollectionFormat.*; 016import static org.apache.juneau.httppart.HttpPartDataType.*; 017import static org.apache.juneau.httppart.HttpPartFormat.*; 018import static org.apache.juneau.internal.StringUtils.*; 019 020import java.io.IOException; 021import java.lang.reflect.*; 022import java.time.temporal.*; 023import java.util.*; 024 025import org.apache.juneau.*; 026import org.apache.juneau.collections.*; 027import org.apache.juneau.httppart.*; 028import org.apache.juneau.internal.*; 029import org.apache.juneau.serializer.*; 030import org.apache.juneau.transform.*; 031import org.apache.juneau.transforms.*; 032import org.apache.juneau.uon.*; 033 034/** 035 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}. 036 * 037 * <p> 038 * This class is NOT thread safe. 039 * It is typically discarded after one-time use although it can be reused within the same thread. 040 */ 041public class OpenApiSerializerSession extends UonSerializerSession { 042 043 //------------------------------------------------------------------------------------------------------------------- 044 // Predefined instances 045 //------------------------------------------------------------------------------------------------------------------- 046 047 // Cache these for faster lookup 048 private static final BeanContext BC = BeanContext.DEFAULT; 049 private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class); 050 private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class); 051 private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class); 052 private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class); 053 private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class); 054 private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class); 055 private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class); 056 private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class); 057 058 private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT; 059 060 //------------------------------------------------------------------------------------------------------------------- 061 // Instance 062 //------------------------------------------------------------------------------------------------------------------- 063 064 private final OpenApiSerializer ctx; 065 066 /** 067 * Create a new session using properties specified in the context. 068 * 069 * @param ctx 070 * The context creating this session object. 071 * The context contains all the configuration settings for this object. 072 * @param args 073 * Runtime session arguments. 074 */ 075 protected OpenApiSerializerSession(OpenApiSerializer ctx, SerializerSessionArgs args) { 076 super(ctx, false, args); 077 this.ctx = ctx; 078 } 079 080 //----------------------------------------------------------------------------------------------------------------- 081 // Entry point methods 082 //----------------------------------------------------------------------------------------------------------------- 083 084 @Override /* Serializer */ 085 protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { 086 try { 087 out.getWriter().write(serialize(HttpPartType.BODY, getSchema(), o)); 088 } catch (SchemaValidationException e) { 089 throw new SerializeException(e); 090 } 091 } 092 093 @SuppressWarnings("rawtypes") 094 @Override /* PartSerializer */ 095 public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException { 096 097 ClassMeta<?> type = getClassMetaForObject(value); 098 if (type == null) 099 type = object(); 100 101 // Swap if necessary 102 PojoSwap swap = type.getSwap(this); 103 if (swap != null && ! type.isDateOrCalendarOrTemporal()) { 104 value = swap(swap, value); 105 type = swap.getSwapClassMeta(this); 106 107 // If the getSwapClass() method returns Object, we need to figure out 108 // the actual type now. 109 if (type.isObject()) 110 type = getClassMetaForObject(value); 111 } 112 113 schema = ObjectUtils.firstNonNull(schema, DEFAULT_SCHEMA); 114 115 HttpPartDataType t = schema.getType(type); 116 117 HttpPartFormat f = schema.getFormat(type); 118 if (f == HttpPartFormat.NO_FORMAT) 119 f = ctx.getFormat(); 120 121 HttpPartCollectionFormat cf = schema.getCollectionFormat(); 122 if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT) 123 cf = ctx.getCollectionFormat(); 124 125 String out = null; 126 127 schema.validateOutput(value, ctx); 128 129 if (type.hasMutaterTo(schema.getParsedType()) || schema.getParsedType().hasMutaterFrom(type)) { 130 value = toType(value, schema.getParsedType()); 131 type = schema.getParsedType(); 132 } 133 134 if (type.isUri()) { 135 value = getUriResolver().resolve(value); 136 type = string(); 137 } 138 139 if (value != null) { 140 141 if (t == STRING) { 142 143 if (f == BYTE) { 144 out = base64Encode(toType(value, CM_ByteArray)); 145 } else if (f == BINARY) { 146 out = toHex(toType(value, CM_ByteArray)); 147 } else if (f == BINARY_SPACED) { 148 out = toSpacedHex(toType(value, CM_ByteArray)); 149 } else if (f == DATE) { 150 try { 151 if (value instanceof Calendar) 152 out = TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, (Calendar)value); 153 else if (value instanceof Date) 154 out = TemporalDateSwap.IsoDate.DEFAULT.swap(this, (Date)value); 155 else if (value instanceof Temporal) 156 out = TemporalSwap.IsoDate.DEFAULT.swap(this, (Temporal)value); 157 else 158 out = value.toString(); 159 } catch (Exception e) { 160 throw new SerializeException(e); 161 } 162 } else if (f == DATE_TIME) { 163 try { 164 if (value instanceof Calendar) 165 out = TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, (Calendar)value); 166 else if (value instanceof Date) 167 out = TemporalDateSwap.IsoInstant.DEFAULT.swap(this, (Date)value); 168 else if (value instanceof Temporal) 169 out = TemporalSwap.IsoInstant.DEFAULT.swap(this, (Temporal)value); 170 else 171 out = value.toString(); 172 } catch (Exception e) { 173 throw new SerializeException(e); 174 } 175 } else if (f == HttpPartFormat.UON) { 176 out = super.serialize(partType, schema, value); 177 } else { 178 out = toType(value, string()); 179 } 180 181 } else if (t == BOOLEAN) { 182 183 out = stringify(toType(value, CM_Boolean)); 184 185 } else if (t == INTEGER) { 186 187 if (f == INT64) 188 out = stringify(toType(value, CM_Long)); 189 else 190 out = stringify(toType(value, CM_Integer)); 191 192 } else if (t == NUMBER) { 193 194 if (f == DOUBLE) 195 out = stringify(toType(value, CM_Double)); 196 else 197 out = stringify(toType(value, CM_Float)); 198 199 } else if (t == ARRAY) { 200 201 if (cf == HttpPartCollectionFormat.UONC) 202 out = super.serialize(partType, null, toList(partType, type, value, schema)); 203 else { 204 205 HttpPartSchema items = schema.getItems(); 206 ClassMeta<?> vt = getClassMetaForObject(value); 207 OapiStringBuilder sb = new OapiStringBuilder(cf); 208 209 if (type.isArray()) { 210 for (int i = 0; i < Array.getLength(value); i++) 211 sb.append(serialize(partType, items, Array.get(value, i))); 212 } else if (type.isCollection()) { 213 for (Object o : (Collection<?>)value) 214 sb.append(serialize(partType, items, o)); 215 } else if (vt.hasMutaterTo(String[].class)) { 216 String[] ss = toType(value, CM_StringArray); 217 for (int i = 0; i < ss.length; i++) 218 sb.append(serialize(partType, items, ss[i])); 219 } else { 220 throw new SerializeException("Input is not a valid array type: " + type); 221 } 222 223 out = sb.toString(); 224 } 225 226 } else if (t == OBJECT) { 227 228 if (cf == HttpPartCollectionFormat.UONC) { 229 if (schema.hasProperties() && type.isMapOrBean()) 230 value = toMap(partType, type, value, schema); 231 out = super.serialize(partType, null, value); 232 233 } else if (type.isBean()) { 234 OapiStringBuilder sb = new OapiStringBuilder(cf); 235 for (BeanPropertyValue p : toBeanMap(value).getValues(isKeepNullProperties())) { 236 if (p.getMeta().canRead()) { 237 Throwable x = p.getThrown(); 238 if (x == null) 239 sb.append(p.getName(), serialize(partType, schema.getProperty(p.getName()), p.getValue())); 240 } 241 } 242 out = sb.toString(); 243 244 } else if (type.isMap()) { 245 OapiStringBuilder sb = new OapiStringBuilder(cf); 246 for (Map.Entry e : (Set<Map.Entry>)((Map)value).entrySet()) 247 sb.append(e.getKey(), serialize(partType, schema.getProperty(stringify(e.getKey())), e.getValue())); 248 out = sb.toString(); 249 250 } else { 251 throw new SerializeException("Input is not a valid object type: " + type); 252 } 253 254 } else if (t == FILE) { 255 throw new SerializeException("File part not supported."); 256 257 } else if (t == NO_TYPE) { 258 // This should never be returned by HttpPartSchema.getType(ClassMeta). 259 throw new SerializeException("Invalid type."); 260 } 261 } 262 263 schema.validateInput(out); 264 if (out == null) 265 out = schema.getDefault(); 266 if (out == null) 267 out = "null"; 268 return out; 269 } 270 271 private static class OapiStringBuilder { 272 static final AsciiSet EQ = AsciiSet.create("=\\"); 273 static final AsciiSet PIPE = AsciiSet.create("|\\"); 274 static final AsciiSet PIPE_OR_EQ = AsciiSet.create("|=\\"); 275 static final AsciiSet COMMA = AsciiSet.create(",\\"); 276 static final AsciiSet COMMA_OR_EQ = AsciiSet.create(",=\\"); 277 278 private final StringBuilder sb = new StringBuilder(); 279 private final HttpPartCollectionFormat cf; 280 private boolean first = true; 281 282 OapiStringBuilder(HttpPartCollectionFormat cf) { 283 this.cf = cf; 284 } 285 286 private void delim(HttpPartCollectionFormat cf) { 287 if (cf == PIPES) 288 sb.append('|'); 289 else if (cf == SSV) 290 sb.append(' '); 291 else if (cf == TSV) 292 sb.append('\t'); 293 else 294 sb.append(','); 295 } 296 297 OapiStringBuilder append(Object o) { 298 if (! first) 299 delim(cf); 300 first = false; 301 if (cf == PIPES) 302 sb.append(escapeChars(stringify(o), PIPE)); 303 else if (cf == SSV || cf == TSV) 304 sb.append(stringify(o)); 305 else 306 sb.append(escapeChars(stringify(o), COMMA)); 307 return this; 308 } 309 310 OapiStringBuilder append(Object key, Object val) { 311 if (! first) 312 delim(cf); 313 first = false; 314 if (cf == PIPES) 315 sb.append(escapeChars(stringify(key), PIPE_OR_EQ)).append('=').append(escapeChars(stringify(val), PIPE_OR_EQ)); 316 else if (cf == SSV || cf == TSV) 317 sb.append(escapeChars(stringify(key), EQ)).append('=').append(escapeChars(stringify(val), EQ)); 318 else 319 sb.append(escapeChars(stringify(key), COMMA_OR_EQ)).append('=').append(escapeChars(stringify(val), COMMA_OR_EQ)); 320 return this; 321 } 322 323 @Override 324 public String toString() { 325 return sb.toString(); 326 } 327 } 328 329 @SuppressWarnings({ "rawtypes" }) 330 private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 331 if (s == null) 332 s = DEFAULT_SCHEMA; 333 OMap m = new OMap(); 334 if (type.isBean()) { 335 for (BeanPropertyValue p : toBeanMap(o).getValues(isKeepNullProperties())) { 336 if (p.getMeta().canRead()) { 337 Throwable t = p.getThrown(); 338 if (t == null) 339 m.put(p.getName(), toObject(partType, p.getValue(), s.getProperty(p.getName()))); 340 } 341 } 342 } else { 343 for (Map.Entry e : (Set<Map.Entry>)((Map)o).entrySet()) 344 m.put(stringify(e.getKey()), toObject(partType, e.getValue(), s.getProperty(stringify(e.getKey())))); 345 } 346 if (isSortMaps()) 347 return sort(m); 348 return m; 349 } 350 351 @SuppressWarnings("rawtypes") 352 private Collection toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 353 if (s == null) 354 s = DEFAULT_SCHEMA; 355 OList l = new OList(); 356 HttpPartSchema items = s.getItems(); 357 if (type.isArray()) { 358 for (int i = 0; i < Array.getLength(o); i++) 359 l.add(toObject(partType, Array.get(o, i), items)); 360 } else if (type.isCollection()) { 361 for (Object o2 : (Collection<?>)o) 362 l.add(toObject(partType, o2, items)); 363 } else { 364 l.add(toObject(partType, o, items)); 365 } 366 if (isSortCollections()) 367 return sort(l); 368 return l; 369 } 370 371 @SuppressWarnings("rawtypes") 372 private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 373 if (o == null) 374 return null; 375 if (s == null) 376 s = DEFAULT_SCHEMA; 377 ClassMeta cm = getClassMetaForObject(o); 378 HttpPartDataType t = s.getType(cm); 379 HttpPartFormat f = s.getFormat(cm); 380 HttpPartCollectionFormat cf = s.getCollectionFormat(); 381 382 if (t == STRING) { 383 if (f == BYTE) 384 return base64Encode(toType(o, CM_ByteArray)); 385 if (f == BINARY) 386 return toHex(toType(o, CM_ByteArray)); 387 if (f == BINARY_SPACED) 388 return toSpacedHex(toType(o, CM_ByteArray)); 389 if (f == DATE) 390 return toIsoDate(toType(o, CM_Calendar)); 391 if (f == DATE_TIME) 392 return toIsoDateTime(toType(o, CM_Calendar)); 393 return o; 394 } else if (t == ARRAY) { 395 Collection l = toList(partType, getClassMetaForObject(o), o, s); 396 if (cf == CSV) 397 return joine(l, ','); 398 if (cf == PIPES) 399 return joine(l, '|'); 400 if (cf == SSV) 401 return join(l, ' '); 402 if (cf == TSV) 403 return join(l, '\t'); 404 return l; 405 } else if (t == OBJECT) { 406 return toMap(partType, getClassMetaForObject(o), o, s); 407 } 408 409 return o; 410 } 411 412 private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException { 413 try { 414 return convertToType(in, type); 415 } catch (InvalidDataConversionException e) { 416 throw new SerializeException(e); 417 } 418 } 419 420 //----------------------------------------------------------------------------------------------------------------- 421 // Other methods 422 //----------------------------------------------------------------------------------------------------------------- 423 424 @Override /* Session */ 425 public OMap toMap() { 426 return super.toMap() 427 .a("OpenApiSerializerSession", new DefaultFilteringOMap() 428 ); 429 } 430}