1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.io;
7   
8   import org.eclipse.jdt.annotation.NotOwning;
9   import org.eclipse.jdt.annotation.Owning;
10  
11  import java.io.IOException;
12  import java.io.InputStream;
13  import java.net.URI;
14  import java.net.URL;
15  import java.util.Map;
16  
17  import dev.metaschema.core.configuration.DefaultConfiguration;
18  import dev.metaschema.core.configuration.IConfiguration;
19  import dev.metaschema.core.configuration.IMutableConfiguration;
20  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
21  import dev.metaschema.core.model.AbstractResourceResolver;
22  import dev.metaschema.core.model.IBoundObject;
23  import dev.metaschema.core.util.ObjectUtils;
24  import dev.metaschema.databind.IBindingContext;
25  import dev.metaschema.databind.io.ModelDetector.Result;
26  import edu.umd.cs.findbugs.annotations.NonNull;
27  
28  /**
29   * A default implementation of an {@link IBoundLoader}.
30   */
31  @SuppressWarnings("PMD.CouplingBetweenObjects")
32  public class DefaultBoundLoader
33      extends AbstractResourceResolver
34      implements IBoundLoader {
35    /**
36     * The number of bytes to read ahead when determining the source format.
37     */
38    public static final int LOOK_AHEAD_BYTES = 32_768;
39    // @NonNull
40    // private static final JsonFactory JSON_FACTORY = new JsonFactory();
41    // @NonNull
42    // private static final XmlFactory XML_FACTORY = new XmlFactory();
43    // @NonNull
44    // private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
45  
46    private FormatDetector formatDetector;
47  
48    private ModelDetector modelDetector;
49  
50    @NonNull
51    private final IBindingContext bindingContext;
52    @NonNull
53    private final IMutableConfiguration<DeserializationFeature<?>> configuration;
54  
55    /**
56     * Construct a new loader instance, using the provided {@link IBindingContext}.
57     *
58     * @param bindingContext
59     *          the Module binding context to use to load Java types
60     */
61    public DefaultBoundLoader(@NonNull IBindingContext bindingContext) {
62      this.bindingContext = bindingContext;
63      this.configuration = new DefaultConfiguration<>();
64    }
65  
66    @NonNull
67    private IMutableConfiguration<DeserializationFeature<?>> getConfiguration() {
68      return configuration;
69    }
70  
71    @Override
72    public boolean isFeatureEnabled(DeserializationFeature<?> feature) {
73      return getConfiguration().isFeatureEnabled(feature);
74    }
75  
76    @Override
77    public Map<DeserializationFeature<?>, Object> getFeatureValues() {
78      return getConfiguration().getFeatureValues();
79    }
80  
81    @Override
82    public IBoundLoader applyConfiguration(@NonNull IConfiguration<DeserializationFeature<?>> other) {
83      getConfiguration().applyConfiguration(other);
84      resetDetector();
85      return this;
86    }
87  
88    @SuppressWarnings("PMD.NullAssignment")
89    private void resetDetector() {
90      // reset the detector
91      formatDetector = null;
92    }
93  
94    @Override
95    public IBoundLoader set(DeserializationFeature<?> feature, Object value) {
96      getConfiguration().set(feature, value);
97      resetDetector();
98      return this;
99    }
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 }