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