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.swap.spi; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.util.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.commons.http.*; 026import org.apache.juneau.commons.reflect.*; 027import org.apache.juneau.marshall.*; 028import org.apache.juneau.marshall.parser.*; 029import org.apache.juneau.marshall.serializer.*; 030 031/** 032 * Used to swap out non-serializable objects with serializable replacements during serialization, and vis-versa during 033 * parsing. 034 * 035 * <h5 class='topic'>Description</h5> 036 * 037 * <p> 038 * <c>ObjectSwaps</c> are used to extend the functionality of the serializers and parsers to be able to handle 039 * objects that aren't automatically handled by the serializers or parsers. 040 * <br>For example, JSON does not have a standard representation for rendering dates. 041 * By defining a special {@code Date} swap and associating it with a serializer and parser, you can convert a 042 * {@code Date} object to a {@code String} during serialization, and convert that {@code String} object back into a 043 * {@code Date} object during parsing. 044 * 045 * <p> 046 * Swaps MUST declare a public no-arg constructor so that the bean context can instantiate them. 047 * 048 * <p> 049 * <c>ObjectSwaps</c> are associated with serializers and parsers through the following: 050 * <ul class='javatree'> 051 * <li class='ja'>{@link Swap @Swap} 052 * <li class='jm'>{@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder#swaps(Class...)} 053 * </ul> 054 * 055 * <p> 056 * <c>ObjectSwaps</c> have two parameters: 057 * <ol> 058 * <li>{@code <T>} - The normal representation of an object. 059 * <li>{@code <S>} - The swapped representation of an object. 060 * </ol> 061 * <br>{@link Serializer Serializers} use swaps to convert objects of type T into objects of type S, and on calls to 062 * {@link BeanMap#get(Object)}. 063 * <br>{@link Parser Parsers} use swaps to convert objects of type S into objects of type T, and on calls to 064 * {@link BeanMap#put(String,Object)}. 065 * 066 * <h5 class='topic'>Swap Class Type {@code <S>}</h5> 067 * 068 * <p> 069 * For normal serialization, the swapped object representation of an object must be an object type that the serializers can natively convert to 070 * JSON (or language-specific equivalent). 071 * <br>The list of valid transformed types are as follows... 072 * <ul> 073 * <li> 074 * {@link String} 075 * <li> 076 * {@link Number} 077 * <li> 078 * {@link Boolean} 079 * <li> 080 * {@link Collection} containing anything on this list. 081 * <li> 082 * {@link Map} containing anything on this list. 083 * <li> 084 * A java bean with properties of anything on this list. 085 * <li> 086 * An array of anything on this list. 087 * </ul> 088 * 089 * <p> 090 * For OpenAPI serialization, the valid swapped types also include <code><jk>byte</jk>[]</code> and <c>Calendar</c>. 091 * 092 * <h5 class='topic'>Normal Class Type {@code <T>}</h5> 093 * 094 * <p> 095 * The normal object representation of an object. 096 * 097 * <h5 class='section'>See Also:</h5><ul> 098 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps">Swap</a> 099 100 * </ul> 101 * 102 * @param <T> The normal form of the class. 103 * @param <S> The swapped form of the class. 104 */ 105@SuppressWarnings({ 106 "java:S112", // throws Exception intentional - callback/lifecycle method for user implementations 107 "java:S1452", // Wildcard required - ObjectSwap<T,?>, ClassMeta<?> for fluent API and swap metadata 108 "rawtypes", // Raw types necessary for generic type handling 109 "unchecked" // Type erasure requires unchecked casts 110}) 111public abstract class ObjectSwap<T,S> { 112 113 /** 114 * Represents a non-existent object swap. 115 */ 116 public static final ObjectSwap NULL = new ObjectSwap((Class)null, (Class)null) {}; 117 118 private final Class<T> normalClass; 119 private final Class<?> swapClass; 120 private final ClassInfo normalClassInfo; 121 private final ClassInfo swapClassInfo; 122 private ClassMeta<?> swapClassMeta; 123 124 // Unfortunately these cannot be made final because we want to allow for ObjectSwaps with no-arg constructors 125 // which simplifies declarations. 126 private MediaType[] forMediaTypes; 127 private String template; 128 129 /** 130 * Constructor. 131 */ 132 protected ObjectSwap() { 133 var ci = info(this.getClass()); 134 normalClass = (Class<T>)ci.getParameterType(0, ObjectSwap.class); 135 swapClass = ci.getParameterType(1, ObjectSwap.class); 136 normalClassInfo = info(normalClass); 137 swapClassInfo = info(swapClass); 138 forMediaTypes = forMediaTypes(); 139 template = withTemplate(); 140 } 141 142 /** 143 * Constructor for when the normal and transformed classes are already known. 144 * 145 * @param normalClass The normal class (cannot be serialized). 146 * @param swapClass The transformed class (serializable). 147 */ 148 protected ObjectSwap(Class<T> normalClass, Class<?> swapClass) { 149 this.normalClass = normalClass; 150 this.swapClass = swapClass; 151 normalClassInfo = o(normalClass).map(x -> info(x)).orElse(null); 152 swapClassInfo = o(swapClass).map(x -> info(x)).orElse(null); 153 this.forMediaTypes = forMediaTypes(); 154 this.template = withTemplate(); 155 } 156 157 /** 158 * Returns the media types that this swap is applicable to. 159 * 160 * <p> 161 * This method can be overridden to programmatically specify what media types it applies to. 162 * 163 * <p> 164 * This method is the programmatic equivalent to the {@link Swap#mediaTypes() @Swap(mediaTypes)} annotation. 165 * 166 * <h5 class='section'>See Also:</h5><ul> 167 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps#per-media-type-swaps">Per-media-type swaps</a> 168 * </ul> 169 * @return The media types that this swap is applicable to, or <jk>null</jk> if it's applicable for all media types. 170 */ 171 @SuppressWarnings({ 172 "java:S1168" // Intentional null = applicable to all media types; callers branch on null (not an empty array). 173 }) 174 public MediaType[] forMediaTypes() { 175 return null; 176 } 177 178 /** 179 * Sets the media types that this swap is associated with. 180 * 181 * <h5 class='section'>See Also:</h5><ul> 182 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps#per-media-type-swaps">Per-media-type swaps</a> 183 * </ul> 184 * 185 * @param mediaTypes The media types that this swap is associated with. Can be <jk>null</jk> to make this swap applicable to all media types. 186 * @return This object. 187 */ 188 public ObjectSwap<T,?> forMediaTypes(MediaType[] mediaTypes) { 189 this.forMediaTypes = mediaTypes; 190 return this; 191 } 192 193 /** 194 * Returns the T class, the normalized form of the class. 195 * 196 * @return The normal form of this class. 197 */ 198 public ClassInfo getNormalClass() { return normalClassInfo; } 199 200 /** 201 * Returns the G class, the generalized form of the class. 202 * 203 * <p> 204 * Subclasses must override this method if the generalized class is {@code Object}, meaning it can produce multiple 205 * generalized forms. 206 * 207 * @return The transformed form of this class. 208 */ 209 public ClassInfo getSwapClass() { return swapClassInfo; } 210 211 /** 212 * Returns the {@link ClassMeta} of the transformed class type. 213 * 214 * <p> 215 * This value is cached for quick lookup. 216 * 217 * @param session 218 * The bean context to use to get the class meta. 219 * This is always going to be the same bean context that created this swap. 220 * Must not be <jk>null</jk> on the first (uncached) call. 221 * @return The {@link ClassMeta} of the transformed class type. 222 */ 223 public ClassMeta<?> getSwapClassMeta(MarshallingSession session) { 224 if (swapClassMeta == null) 225 swapClassMeta = session.getClassMeta(swapClass); 226 return swapClassMeta; 227 } 228 229 /** 230 * Checks if the specified object is an instance of the normal class defined on this swap. 231 * 232 * @param o The object to check. Can be <jk>null</jk> (returns <jk>false</jk>). 233 * @return 234 * <jk>true</jk> if the specified object is a subclass of the normal class defined on this transform. 235 * A <jk>null</jk> value always returns <jk>false</jk>. 236 */ 237 public boolean isNormalObject(Object o) { 238 if (o == null) 239 return false; 240 return normalClassInfo.isAssignableFrom(o.getClass()); 241 } 242 243 /** 244 * Checks if the specified object is an instance of the swap class defined on this swap. 245 * 246 * @param o The object to check. Can be <jk>null</jk> (returns <jk>false</jk>). 247 * @return 248 * <jk>true</jk> if the specified object is a subclass of the transformed class defined on this transform. 249 * A <jk>null</jk> value always returns <jk>false</jk>. 250 */ 251 public boolean isSwappedObject(Object o) { 252 if (o == null) 253 return false; 254 return swapClassInfo.isAssignableFrom(o.getClass()); 255 } 256 257 /** 258 * Returns a number indicating how well this swap matches the specified session. 259 * 260 * <p> 261 * Uses the {@link MediaType#match(MediaType, boolean)} method algorithm to produce a number whereby a 262 * larger value indicates a "better match". 263 * The idea being that if multiple swaps are associated with a given object, we want to pick the best one. 264 * 265 * <p> 266 * For example, if the session media type is <js>"text/json"</js>, then the match values are shown below: 267 * 268 * <ul> 269 * <li><js>"text/json"</js> = <c>100,000</c> 270 * <li><js>"*/json"</js> = <c>5,100</c> 271 * <li><js>"*/*"</js> = <c>5,000</c> 272 * <li>No media types specified on swap = <c>1</c> 273 * <li><js>"text/xml"</js> = <c>0</c> 274 * </ul> 275 * 276 * @param session The bean session. Can be <jk>null</jk> (returns <c>0</c>, or <c>1</c> if this swap has no media types configured). 277 * @return Zero if swap doesn't match the session, or a positive number if it does. 278 */ 279 public int match(MarshallingSession session) { 280 if (forMediaTypes == null) 281 return 1; 282 int i = 0; 283 var mt = session != null ? session.getMediaType() : null; 284 if (mt == null) 285 return 0; 286 if (nn(forMediaTypes)) 287 for (var mt2 : forMediaTypes) 288 i = Math.max(i, mt2.match(mt, false)); 289 return i; 290 } 291 292 /** 293 * If this transform is to be used to serialize non-serializable objects, it must implement this method. 294 * 295 * <p> 296 * The object must be converted into one of the following serializable types: 297 * <ul class='spaced-list'> 298 * <li> 299 * {@link String} 300 * <li> 301 * {@link Number} 302 * <li> 303 * {@link Boolean} 304 * <li> 305 * {@link Collection} containing anything on this list. 306 * <li> 307 * {@link Map} containing anything on this list. 308 * <li> 309 * A java bean with properties of anything on this list. 310 * <li> 311 * An array of anything on this list. 312 * </ul> 313 * 314 * @param session 315 * The bean session to use to get the class meta. 316 * This is always going to be the same bean context that created this swap. 317 * @param o The object to be transformed. 318 * @return The transformed object. 319 * @throws Exception If a problem occurred trying to convert the output. 320 */ 321 public S swap(MarshallingSession session, T o) throws Exception { 322 return swap(session, o, template); 323 } 324 325 /** 326 * Same as {@link #swap(MarshallingSession, Object)}, but can be used if your swap has a template associated with it. 327 * 328 * @param session 329 * The bean session to use to get the class meta. 330 * This is always going to be the same bean context that created this swap. 331 * @param o The object to be transformed. 332 * @param template 333 * The template string associated with this swap. 334 * @return The transformed object. 335 * @throws Exception If a problem occurred trying to convert the output. 336 */ 337 public S swap(MarshallingSession session, T o, String template) throws Exception { 338 throw new SerializeException("Swap method not implemented on ObjectSwap '%s'", cn(this)); 339 } 340 341 @Override /* Overridden from Object */ 342 public String toString() { 343 return cns(this) + '<' + getNormalClass().getNameSimple() + "," + getSwapClass().getNameSimple() + '>'; 344 } 345 346 /** 347 * If this transform is to be used to reconstitute objects that aren't true Java beans, it must implement this method. 348 * 349 * @param session 350 * The bean session to use to get the class meta. 351 * This is always going to be the same bean context that created this swap. 352 * @param f The transformed object. 353 * @param hint 354 * If possible, the parser will try to tell you the object type being created. 355 * For example, on a serialized date, this may tell you that the object being created must be of type 356 * {@code GregorianCalendar}. 357 * <br>This may be <jk>null</jk> if the parser cannot make this determination. 358 * @return The narrowed object. 359 * @throws Exception If this method is not implemented. 360 */ 361 public T unswap(MarshallingSession session, S f, ClassMeta<?> hint) throws Exception { 362 return unswap(session, f, hint, template); 363 } 364 365 /** 366 * Same as {@link #unswap(MarshallingSession, Object, ClassMeta)}, but can be used if your swap has a template associated with it. 367 * 368 * @param session 369 * The bean session to use to get the class meta. 370 * This is always going to be the same bean context that created this swap. 371 * @param f The transformed object. 372 * @param hint 373 * If possible, the parser will try to tell you the object type being created. 374 * For example, on a serialized date, this may tell you that the object being created must be of type 375 * {@code GregorianCalendar}. 376 * <br>This may be <jk>null</jk> if the parser cannot make this determination. 377 * @param template 378 * The template string associated with this swap. 379 * @return The transformed object. 380 * @throws Exception If a problem occurred trying to convert the output. 381 */ 382 public T unswap(MarshallingSession session, S f, ClassMeta<?> hint, String template) throws Exception { 383 throw new ParseException("Unswap method not implemented on ObjectSwap '%s'", cn(this)); 384 } 385 386 /** 387 * Returns additional context information for this swap. 388 * 389 * <p> 390 * Typically this is going to be used to specify a template name, such as a FreeMarker template file name. 391 * 392 * <p> 393 * This method can be overridden to programmatically specify a template value. 394 * 395 * <p> 396 * This method is the programmatic equivalent to the {@link Swap#template() @Swap(template)} annotation. 397 * 398 * <h5 class='section'>See Also:</h5><ul> 399 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/TemplatedSwaps">Templated Swaps</a> 400 * </ul> 401 * 402 * @return Additional context information, or <jk>null</jk> if not specified. 403 */ 404 public String withTemplate() { 405 return null; 406 } 407 408 /** 409 * Sets the template string on this swap. 410 * 411 * <h5 class='section'>See Also:</h5><ul> 412 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/TemplatedSwaps">Templated Swaps</a> 413 * </ul> 414 * 415 * @param template The template string on this swap. Can be <jk>null</jk> to unset the template. 416 * @return This object. 417 */ 418 public ObjectSwap<T,?> withTemplate(String template) { 419 this.template = template; 420 return this; 421 } 422}