001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2022 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.loader.jackson; 018 019import java.io.IOException; 020import java.io.BufferedInputStream; 021import java.io.FileNotFoundException; 022import java.io.InputStream; 023import java.io.UncheckedIOException; 024 025import java.lang.reflect.Type; 026 027import java.nio.file.Files; 028import java.nio.file.NoSuchFileException; 029import java.nio.file.Paths; 030 031import java.util.Objects; 032 033import java.util.function.BiFunction; 034import java.util.function.Consumer; 035import java.util.function.Supplier; 036 037import com.fasterxml.jackson.core.JsonParser; 038import com.fasterxml.jackson.core.ObjectCodec; 039import com.fasterxml.jackson.core.TreeNode; 040 041import com.fasterxml.jackson.databind.DeserializationConfig; 042import com.fasterxml.jackson.databind.JavaType; 043import com.fasterxml.jackson.databind.ObjectMapper; 044 045import org.microbean.development.annotation.Convenience; 046 047import org.microbean.loader.api.Loader; 048 049import org.microbean.path.Path; 050 051import org.microbean.type.JavaTypes; 052 053/** 054 * A {@link JacksonProvider} built around an {@link 055 * InputStream}-providing {@linkplain BiFunction bifunction} and an 056 * {@link ObjectCodec}-providing {@linkplain BiFunction bifunction}. 057 * 058 * @author <a href="https://about.me/lairdnelson" 059 * target="_parent">Laird Nelson</a> 060 */ 061public class InputStreamJacksonProvider extends JacksonProvider { 062 063 064 /* 065 * Instance fields. 066 */ 067 068 069 private final BiFunction<? super Loader<?>, ? super Path<? extends Type>, ? extends ObjectCodec> objectCodecFunction; 070 071 private final BiFunction<? super Loader<?>, ? super Path<? extends Type>, ? extends InputStream> inputStreamFunction; 072 073 private final Consumer<? super InputStream> inputStreamReadConsumer; 074 075 076 /* 077 * Constructors. 078 */ 079 080 081 private InputStreamJacksonProvider() { 082 super(null); 083 throw new UnsupportedOperationException(); 084 } 085 086 /** 087 * Creates a new {@link InputStreamJacksonProvider}. 088 * 089 * @param lowerBound the {@linkplain #lowerBound() lower type bound} 090 * of this {@link InputStreamJacksonProvider} implementation; may be 091 * {@code null} 092 * 093 * @param mapperSupplier a {@link Supplier}, deterministic or not, 094 * of {@link ObjectMapper} instances; ordinarily callers should 095 * supply a {@link Supplier} that caches; may be {@code null} 096 * 097 * @param resourceName a resource name that is treated first as a 098 * classpath resource and finally as the name of a file relative to 099 * the directory identified by the {@link System#getProperty(String, 100 * String) user.dir} system property 101 * 102 * @see #InputStreamJacksonProvider(Type, BiFunction, BiFunction, 103 * Consumer) 104 * 105 * @see #inputStream(ClassLoader, String) 106 */ 107 public InputStreamJacksonProvider(final Type lowerBound, 108 final Supplier<? extends ObjectMapper> mapperSupplier, 109 final String resourceName) { 110 this(lowerBound, 111 objectCodecFunction(mapperSupplier), 112 (l, p) -> inputStream(classLoader(p), resourceName), 113 InputStreamJacksonProvider::closeInputStream); 114 } 115 116 /** 117 * Creates a new {@link InputStreamJacksonProvider}. 118 * 119 * @param lowerBound the {@linkplain #lowerBound() lower type bound} 120 * of this {@link InputStreamJacksonProvider} implementation; may be 121 * {@code null} 122 * 123 * @param objectCodecFunction a {@link BiFunction} that returns an 124 * {@link ObjectCodec} when supplied with a {@link Loader} and a 125 * {@link Path}; may be {@code null} 126 * 127 * @param inputStreamFunction a {@link BiFunction} that returns an 128 * open {@link InputStream} when supplied with a {@link Loader} and 129 * a {@link Path}; may be {@code null} 130 * 131 * @param inputStreamReadConsumer a {@link Consumer} that is called 132 * with an {@link InputStream} after the {@link InputStream} has 133 * been fully read; may be {@code null}; normally should 134 * {@linkplain InputStream#close() close} the {@link InputStream} 135 */ 136 public InputStreamJacksonProvider(final Type lowerBound, 137 final BiFunction<? super Loader<?>, ? super Path<? extends Type>, ? extends ObjectCodec> objectCodecFunction, 138 final BiFunction<? super Loader<?>, ? super Path<? extends Type>, ? extends InputStream> inputStreamFunction, 139 final Consumer<? super InputStream> inputStreamReadConsumer) { 140 super(lowerBound); 141 this.objectCodecFunction = objectCodecFunction == null ? InputStreamJacksonProvider::returnNull : objectCodecFunction; 142 this.inputStreamFunction = inputStreamFunction == null ? InputStreamJacksonProvider::returnNull : inputStreamFunction; 143 this.inputStreamReadConsumer = inputStreamReadConsumer == null ? InputStreamJacksonProvider::sink : inputStreamReadConsumer; 144 } 145 146 147 /* 148 * Instance methods. 149 */ 150 151 152 /** 153 * Invokes the {@link BiFunction#apply(Object, Object)} method of 154 * the {@code objectCodecFunction} {@linkplain 155 * #InputStreamJacksonProvider(Type, BiFunction, BiFunction, 156 * Consumer) supplied at construction time} and returns the result. 157 * 158 * @param requestingLoader the {@link Loader} requesting a value; 159 * must not be {@code null} 160 * 161 * @param absolutePath the {@linkplain Path#absolute() absolute} 162 * {@link Path} the {@code requestingLoader} is currently 163 * requesting; must not be {@code null} 164 * 165 * @return an {@link ObjectCodec} suitable for the supplied 166 * arguments, or {@code null} if this {@link 167 * InputStreamJacksonProvider} should not handle the current request 168 * 169 * @nullability This method may return {@code null}. 170 * 171 * @threadsafety This method is safe for concurrent use by multiple 172 * threads, but the {@code objectCodecFunction} {@linkplain 173 * #InputStreamJacksonProvider(Type, BiFunction, BiFunction, 174 * Consumer) supplied at construction time} may not be. 175 * 176 * @idempotency This method is idempotent and deterministic if the 177 * {@code objectCodecFunction} {@linkplain 178 * #InputStreamJacksonProvider(Type, BiFunction, BiFunction, 179 * Consumer) supplied at construction time} is. 180 */ 181 @Override // JacksonProvider 182 protected final ObjectCodec objectCodec(final Loader<?> requestingLoader, final Path<? extends Type> absolutePath) { 183 return this.objectCodecFunction.apply(requestingLoader, absolutePath); 184 } 185 186 @Override // JacksonProvider 187 protected TreeNode rootNode(final Loader<?> requestingLoader, 188 final Path<? extends Type> absolutePath) { 189 InputStream is = null; 190 RuntimeException runtimeException = null; 191 ObjectCodec objectCodec = this.objectCodec(requestingLoader, absolutePath); 192 if (objectCodec == null) { 193 return null; 194 } 195 JsonParser parser = null; 196 try { 197 is = this.inputStreamFunction.apply(requestingLoader, absolutePath); 198 if (is == null) { 199 return null; 200 } 201 parser = objectCodec.getFactory().createParser(is); 202 parser.setCodec(objectCodec); 203 return parser.readValueAsTree(); 204 } catch (final IOException ioException) { 205 runtimeException = new UncheckedIOException(ioException.getMessage(), ioException); 206 } catch (final RuntimeException e) { 207 runtimeException = e; 208 } finally { 209 try { 210 if (parser != null) { 211 parser.setCodec(null); 212 parser.close(); 213 } 214 } catch (final IOException ioException) { 215 if (runtimeException == null) { 216 runtimeException = new UncheckedIOException(ioException.getMessage(), ioException); 217 } else { 218 runtimeException.addSuppressed(ioException); 219 } 220 } catch (final RuntimeException e) { 221 if (runtimeException == null) { 222 runtimeException = e; 223 } else { 224 runtimeException.addSuppressed(e); 225 } 226 } finally { 227 try { 228 if (is != null) { 229 this.inputStreamReadConsumer.accept(is); 230 } 231 } catch (final RuntimeException e) { 232 if (runtimeException == null) { 233 runtimeException = e; 234 } else { 235 runtimeException.addSuppressed(e); 236 } 237 } finally { 238 if (runtimeException != null) { 239 throw runtimeException; 240 } 241 } 242 } 243 } 244 return null; 245 } 246 247 248 /* 249 * Static methods. 250 */ 251 252 253 /** 254 * Returns an open {@link InputStream} loaded using the supplied 255 * {@link ClassLoader} and a name of a classpath resource. 256 * 257 * <p>This method never returns {@code null}.</p> 258 * 259 * @param cl the {@link ClassLoader} that will actually cause the 260 * {@link InputStream} to be created and opened; may be {@code null} 261 * in which case the system classloader will be used instead 262 * 263 * @param resourceName the name of the classpath resource for which 264 * an {@link InputStream} will be created and opened; must not be 265 * {@code null} 266 * 267 * @return a non-{@code null}, open {@link InputStream} 268 * 269 * @exception NullPointerException if {@code resourceName} is {@code 270 * null} 271 * 272 * @nullability This method never returns {@code null}. 273 * 274 * @idempotency This method is idempotent and deterministic. 275 * 276 * @threadsafety This method is safe for concurrent use by multiple 277 * threads. 278 */ 279 @Convenience 280 protected static final InputStream inputStream(final ClassLoader cl, final String resourceName) { 281 final InputStream returnValue; 282 InputStream temp = cl == null ? ClassLoader.getSystemResourceAsStream(resourceName) : cl.getResourceAsStream(resourceName); 283 if (temp == null) { 284 try { 285 temp = new BufferedInputStream(Files.newInputStream(Paths.get(System.getProperty("user.dir", "."), resourceName))); 286 } catch (final FileNotFoundException /* this probably isn't thrown */ | NoSuchFileException e) { 287 288 } catch (final IOException ioException) { 289 throw new UncheckedIOException(ioException.getMessage(), ioException); 290 } finally { 291 returnValue = temp; 292 } 293 } else if (temp instanceof BufferedInputStream) { 294 returnValue = (BufferedInputStream)temp; 295 } else { 296 returnValue = new BufferedInputStream(temp); 297 } 298 return returnValue; 299 } 300 301 /** 302 * Calls {@link InputStream#close()} on the supplied {@link 303 * InputStream} if it is non-{@code null}. 304 * 305 * @param is the {@link InputStream}; may be {@code null} in which 306 * case no action will be taken 307 * 308 * @exception UncheckedIOException if an {@link IOException} is 309 * thrown by the {@link InputStream#close()} method 310 */ 311 @Convenience 312 protected static final void closeInputStream(final InputStream is) { 313 if (is != null) { 314 try { 315 is.close(); 316 } catch (final IOException ioException) { 317 throw new UncheckedIOException(ioException.getMessage(), ioException); 318 } 319 } 320 } 321 322 private static final BiFunction<? super Loader<?>, ? super Path<? extends Type>, ? extends ObjectCodec> objectCodecFunction(final Supplier<? extends ObjectMapper> mapperSupplier) { 323 if (mapperSupplier == null) { 324 return InputStreamJacksonProvider::returnNull; 325 } else { 326 return (l, p) -> { 327 // Note that otherwise potential infinite loops are handled in 328 // the DefaultLoader class. 329 final ObjectMapper mapper = l.load(ObjectMapper.class).orElseGet(mapperSupplier); 330 if (mapper == null) { 331 return null; 332 } 333 final JavaType javaType = mapper.constructType(p.qualified()); 334 return mapper.canDeserialize(javaType) ? mapper.readerFor(javaType) : null; 335 }; 336 } 337 } 338 339 private static final ClassLoader classLoader(final Path<? extends Type> path) { 340 final Class<?> c = JavaTypes.erase(path.qualified()); 341 ClassLoader cl = null; 342 if (c == null) { 343 cl = Thread.currentThread().getContextClassLoader(); 344 if (cl == null) { 345 cl = ClassLoader.getSystemClassLoader(); 346 } 347 } else { 348 cl = c.getClassLoader(); 349 } 350 return cl; 351 } 352 353 private static final void sink(final Object ignored) { 354 355 } 356 357 private static final <T> T returnNull(final Object ignored, final Object alsoIgnored) { 358 return null; 359 } 360 361}