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}