001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io;
007
008import org.eclipse.jdt.annotation.NotOwning;
009import org.eclipse.jdt.annotation.Owning;
010
011import java.io.IOException;
012import java.io.InputStream;
013import java.net.URI;
014import java.net.URL;
015import java.util.Map;
016
017import dev.metaschema.core.configuration.DefaultConfiguration;
018import dev.metaschema.core.configuration.IConfiguration;
019import dev.metaschema.core.configuration.IMutableConfiguration;
020import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
021import dev.metaschema.core.model.AbstractResourceResolver;
022import dev.metaschema.core.model.IBoundObject;
023import dev.metaschema.core.util.ObjectUtils;
024import dev.metaschema.databind.IBindingContext;
025import dev.metaschema.databind.io.ModelDetector.Result;
026import edu.umd.cs.findbugs.annotations.NonNull;
027
028/**
029 * A default implementation of an {@link IBoundLoader}.
030 */
031@SuppressWarnings("PMD.CouplingBetweenObjects")
032public class DefaultBoundLoader
033    extends AbstractResourceResolver
034    implements IBoundLoader {
035  /**
036   * The number of bytes to read ahead when determining the source format.
037   */
038  public static final int LOOK_AHEAD_BYTES = 32_768;
039  // @NonNull
040  // private static final JsonFactory JSON_FACTORY = new JsonFactory();
041  // @NonNull
042  // private static final XmlFactory XML_FACTORY = new XmlFactory();
043  // @NonNull
044  // private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
045
046  private FormatDetector formatDetector;
047
048  private ModelDetector modelDetector;
049
050  @NonNull
051  private final IBindingContext bindingContext;
052  @NonNull
053  private final IMutableConfiguration<DeserializationFeature<?>> configuration;
054
055  /**
056   * Construct a new loader instance, using the provided {@link IBindingContext}.
057   *
058   * @param bindingContext
059   *          the Module binding context to use to load Java types
060   */
061  public DefaultBoundLoader(@NonNull IBindingContext bindingContext) {
062    this.bindingContext = bindingContext;
063    this.configuration = new DefaultConfiguration<>();
064  }
065
066  @NonNull
067  private IMutableConfiguration<DeserializationFeature<?>> getConfiguration() {
068    return configuration;
069  }
070
071  @Override
072  public boolean isFeatureEnabled(DeserializationFeature<?> feature) {
073    return getConfiguration().isFeatureEnabled(feature);
074  }
075
076  @Override
077  public Map<DeserializationFeature<?>, Object> getFeatureValues() {
078    return getConfiguration().getFeatureValues();
079  }
080
081  @Override
082  public IBoundLoader applyConfiguration(@NonNull IConfiguration<DeserializationFeature<?>> other) {
083    getConfiguration().applyConfiguration(other);
084    resetDetector();
085    return this;
086  }
087
088  @SuppressWarnings("PMD.NullAssignment")
089  private void resetDetector() {
090    // reset the detector
091    formatDetector = null;
092  }
093
094  @Override
095  public IBoundLoader set(DeserializationFeature<?> feature, Object value) {
096    getConfiguration().set(feature, value);
097    resetDetector();
098    return this;
099  }
100
101  @Override
102  public IBindingContext getBindingContext() {
103    return bindingContext;
104  }
105
106  @Override
107  public Format detectFormat(@NonNull URI uri) throws IOException {
108    URI resourceUri = resolve(uri);
109    URL resource = resourceUri.toURL();
110
111    try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
112      return detectFormat(is, uri).getFormat();
113    }
114  }
115
116  @Override
117  public FormatDetector.Result detectFormat(InputStream is, URI resource) throws IOException {
118    return getFormatDetector().detect(is);
119  }
120
121  @NonNull
122  private FormatDetector getFormatDetector() {
123    if (formatDetector == null) {
124      formatDetector = new FormatDetector(getConfiguration());
125    }
126    assert formatDetector != null;
127    return formatDetector;
128  }
129
130  @NonNull
131  private ModelDetector getModelDetector() {
132    if (modelDetector == null) {
133      modelDetector = new ModelDetector(
134          getBindingContext(),
135          getConfiguration());
136    }
137    assert modelDetector != null;
138    return modelDetector;
139  }
140
141  @Override
142  @Owning
143  public Result detectModel(@NotOwning InputStream is, URI resource, Format format) throws IOException {
144    return getModelDetector().detect(is, resource, format);
145  }
146
147  @Override
148  public <CLASS extends IBoundObject> CLASS load(@NonNull URI uri) throws IOException {
149    URI resourceUri = resolve(uri);
150    URL resource = resourceUri.toURL();
151
152    try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
153      return load(is, uri);
154    }
155  }
156
157  @SuppressWarnings("unchecked")
158  @Override
159  @NonNull
160  public <CLASS extends IBoundObject> CLASS load(
161      @NotOwning @NonNull InputStream is,
162      @NonNull URI resource)
163      throws IOException {
164    FormatDetector.Result formatMatch = getFormatDetector().detect(is);
165    Format format = formatMatch.getFormat();
166
167    try (InputStream formatStream = formatMatch.getDataStream()) {
168      try (ModelDetector.Result modelMatch = detectModel(formatStream, resource, format)) {
169
170        IDeserializer<?> deserializer = getDeserializer(
171            modelMatch.getBoundClass(),
172            format,
173            getConfiguration());
174        try (InputStream modelStream = modelMatch.getDataStream()) {
175          return (CLASS) deserializer.deserialize(modelStream, resource);
176        }
177      }
178    }
179  }
180
181  @Override
182  public <CLASS extends IBoundObject> CLASS load(Class<CLASS> clazz, URI uri) throws IOException {
183    URI resourceUri = resolve(uri);
184    URL resource = resourceUri.toURL();
185
186    try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
187      return load(clazz, is, resourceUri);
188    }
189  }
190
191  @Override
192  public <CLASS extends IBoundObject> CLASS load(Class<CLASS> clazz, InputStream is, URI documentUri)
193      throws IOException {
194    // we cannot close this stream, since it will cause the underlying stream to be
195    // closed
196    FormatDetector.Result match = getFormatDetector().detect(is);
197    Format format = match.getFormat();
198
199    try (InputStream remainingStream = match.getDataStream()) {
200      // is autoclosing ok?
201      return load(clazz, format, remainingStream, documentUri);
202    }
203  }
204
205  @Override
206  @NonNull
207  public <CLASS extends IBoundObject> CLASS load(
208      @NonNull Class<CLASS> clazz,
209      @NonNull Format format,
210      @NonNull InputStream is,
211      @NonNull URI documentUri) throws IOException {
212
213    IDeserializer<CLASS> deserializer = getDeserializer(clazz, format, getConfiguration());
214    return deserializer.deserialize(is, documentUri);
215  }
216
217  @Override
218  public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
219    URI resourceUri = resolve(uri);
220    URL resource = resourceUri.toURL();
221
222    try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
223      return loadAsNodeItem(is, resourceUri);
224    }
225  }
226
227  @NonNull
228  private IDocumentNodeItem loadAsNodeItem(@NonNull InputStream is, @NonNull URI documentUri) throws IOException {
229    FormatDetector.Result formatMatch = getFormatDetector().detect(is);
230    Format format = formatMatch.getFormat();
231
232    try (InputStream formatStream = formatMatch.getDataStream()) {
233      return loadAsNodeItem(format, formatStream, documentUri);
234    }
235  }
236
237  @Override
238  public IDocumentNodeItem loadAsNodeItem(Format format, URI uri) throws IOException {
239    URI resourceUri = resolve(uri);
240    URL resource = resourceUri.toURL();
241
242    try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
243      return loadAsNodeItem(format, is, resourceUri);
244    }
245  }
246
247  @Override
248  public IDocumentNodeItem loadAsNodeItem(Format format, InputStream is, URI resource)
249      throws IOException {
250    try (ModelDetector.Result modelMatch = detectModel(is, resource, format)) {
251
252      IDeserializer<?> deserializer = getDeserializer(
253          modelMatch.getBoundClass(),
254          format,
255          getConfiguration());
256      try (InputStream modelStream = modelMatch.getDataStream()) {
257        return (IDocumentNodeItem) deserializer.deserializeToNodeItem(modelStream, resource);
258      }
259    }
260  }
261
262  @NonNull
263  private <CLASS extends IBoundObject> IDeserializer<CLASS> getDeserializer(
264      @NonNull Class<CLASS> clazz,
265      @NonNull Format format,
266      @NonNull IConfiguration<DeserializationFeature<?>> config) {
267    IDeserializer<CLASS> retval = getBindingContext().newDeserializer(format, clazz);
268    retval.applyConfiguration(config);
269    return retval;
270  }
271}