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.Owning;
9   import org.xml.sax.InputSource;
10  
11  import java.io.File;
12  import java.io.FileNotFoundException;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.OutputStream;
16  import java.io.Writer;
17  import java.net.URI;
18  import java.net.URISyntaxException;
19  import java.net.URL;
20  import java.nio.file.Path;
21  
22  import dev.metaschema.core.configuration.IConfiguration;
23  import dev.metaschema.core.configuration.IMutableConfiguration;
24  import dev.metaschema.core.metapath.IDocumentLoader;
25  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
26  import dev.metaschema.core.model.IBoundObject;
27  import dev.metaschema.core.util.ObjectUtils;
28  import dev.metaschema.databind.DefaultBindingContext;
29  import dev.metaschema.databind.IBindingContext;
30  import edu.umd.cs.findbugs.annotations.NonNull;
31  
32  /**
33   * A common interface for loading Module based instance resources.
34   */
35  public interface IBoundLoader extends IDocumentLoader, IMutableConfiguration<DeserializationFeature<?>> {
36  
37    @Override
38    default IBoundLoader enableFeature(DeserializationFeature<?> feature) {
39      return set(feature, true);
40    }
41  
42    @Override
43    default IBoundLoader disableFeature(DeserializationFeature<?> feature) {
44      return set(feature, false);
45    }
46  
47    @Override
48    IBoundLoader applyConfiguration(IConfiguration<DeserializationFeature<?>> other);
49  
50    @Override
51    IBoundLoader set(DeserializationFeature<?> feature, Object value);
52  
53    /**
54     * Determine the format of the provided resource.
55     *
56     * @param file
57     *          the resource
58     * @return the format information for the provided resource
59     * @throws IOException
60     *           if an error occurred while reading the resource
61     */
62    @NonNull
63    default Format detectFormat(@NonNull File file) throws IOException {
64      return detectFormat(ObjectUtils.notNull(file.toPath()));
65    }
66  
67    /**
68     * Determine the format of the provided resource.
69     *
70     * @param path
71     *          the resource
72     * @return the format information for the provided resource
73     * @throws IOException
74     *           if an error occurred while reading the resource
75     */
76    @NonNull
77    default Format detectFormat(@NonNull Path path) throws IOException {
78      return detectFormat(ObjectUtils.notNull(path.toUri()));
79    }
80  
81    /**
82     * Determine the format of the provided resource.
83     *
84     * @param url
85     *          the resource
86     * @return the format information for the provided resource
87     * @throws IOException
88     *           if an error occurred while reading the resource
89     */
90    @NonNull
91    default Format detectFormat(@NonNull URL url) throws IOException {
92      try {
93        return detectFormat(ObjectUtils.notNull(url.toURI()));
94      } catch (URISyntaxException ex) {
95        throw new IOException(ex);
96      }
97    }
98  
99    /**
100    * Determine the format of the resource identified by the provided {@code uri}.
101    *
102    * @param uri
103    *          the resource
104    * @return the format information for the provided resource
105    * @throws IOException
106    *           if an error occurred while reading the resource
107    */
108   @NonNull
109   Format detectFormat(@NonNull URI uri) throws IOException;
110 
111   /**
112    * Determine the format of the provided resource.
113    * <p>
114    * This method will consume data from the provided {@link InputStream}. If the
115    * caller of this method intends to read data from the stream after determining
116    * the format, the caller should pass in a stream that can be reset.
117    * <p>
118    * This method will not close the provided {@link InputStream}, since it does
119    * not own the stream.
120    *
121    * @param is
122    *          an input stream for the resource
123    * @param resource
124    *          the URI of the resource
125    * @return the format information for the provided resource
126    * @throws IOException
127    *           if an error occurred while reading the resource
128    */
129   @NonNull
130   FormatDetector.Result detectFormat(@NonNull InputStream is, @NonNull URI resource) throws IOException;
131 
132   /**
133    * Determine the model of the provided resource.
134    * <p>
135    * This method will consume data from any {@link InputStream} provided by the
136    * {@link InputSource}. If the caller of this method intends to read data from
137    * the stream after determining the format, the caller should pass in a stream
138    * that can be reset.
139    * <p>
140    * This method will not close any {@link InputStream} provided by the
141    * {@link InputSource}, since it does not own the stream.
142    * <p>
143    * The caller owns the returned result and is responsible for closing it.
144    *
145    * @param is
146    *          an input stream for the resource
147    * @param resource
148    *          the URI of the resource
149    * @param format
150    *          the format of the provided resource
151    * @return the model of the provided resource
152    * @throws IOException
153    *           if an error occurred while reading the resource
154    */
155   @NonNull
156   @Owning
157   ModelDetector.Result detectModel(@NonNull InputStream is, @NonNull URI resource, @NonNull Format format)
158       throws IOException;
159 
160   /**
161    * Load data from the provided resource into a bound object.
162    * <p>
163    * This method will auto-detect the format of the provided resource.
164    *
165    * @param <CLASS>
166    *          the type of the bound object to return
167    * @param file
168    *          the resource
169    * @return a bound object containing the loaded data
170    * @throws IOException
171    *           if an error occurred while reading the resource
172    * @see #detectFormat(File)
173    */
174   @NonNull
175   default <CLASS extends IBoundObject> CLASS load(@NonNull File file) throws IOException {
176     return load(ObjectUtils.notNull(file.toPath()));
177   }
178 
179   /**
180    * Load data from the provided resource into a bound object.
181    * <p>
182    * This method will auto-detect the format of the provided resource.
183    *
184    * @param <CLASS>
185    *          the type of the bound object to return
186    * @param path
187    *          the resource
188    * @return a bound object containing the loaded data
189    * @throws IOException
190    *           if an error occurred while reading the resource
191    * @see #detectFormat(File)
192    */
193   @NonNull
194   default <CLASS extends IBoundObject> CLASS load(@NonNull Path path) throws IOException {
195     return load(ObjectUtils.notNull(path.toUri()));
196   }
197 
198   /**
199    * Load data from the provided resource into a bound object.
200    * <p>
201    * This method will auto-detect the format of the provided resource.
202    *
203    * @param <CLASS>
204    *          the type of the bound object to return
205    * @param url
206    *          the resource
207    * @return a bound object containing the loaded data
208    * @throws IOException
209    *           if an error occurred while reading the resource
210    * @throws URISyntaxException
211    *           if the provided {@code url} is malformed
212    * @see #detectFormat(URL)
213    */
214   @NonNull
215   default <CLASS extends IBoundObject> CLASS load(@NonNull URL url) throws IOException, URISyntaxException {
216     return load(ObjectUtils.notNull(url.toURI()));
217   }
218 
219   /**
220    * Load data from the resource identified by the provided {@code uri} into a
221    * bound object.
222    * <p>
223    * This method will auto-detect the format of the provided resource.
224    *
225    * @param <CLASS>
226    *          the type of the bound object to return
227    * @param uri
228    *          the resource
229    * @return a bound object containing the loaded data
230    * @throws IOException
231    *           if an error occurred while reading the resource
232    * @see #detectFormat(URL)
233    */
234   @NonNull
235   <CLASS extends IBoundObject> CLASS load(@NonNull URI uri) throws IOException;
236 
237   /**
238    * Load data from the provided resource into a bound object.
239    * <p>
240    * This method should auto-detect the format of the provided resource.
241    * <p>
242    * This method will not close the provided {@link InputStream}, since it does
243    * not own the stream.
244    *
245    * @param <CLASS>
246    *          the type of the bound object to return
247    * @param is
248    *          the resource stream
249    * @param resource
250    *          the URI of the resource
251    * @return a bound object containing the loaded data
252    * @throws IOException
253    *           if an error occurred while reading the resource
254    * @see #detectFormat(InputStream, URI)
255    */
256   @NonNull
257   <CLASS extends IBoundObject> CLASS load(@NonNull InputStream is, @NonNull URI resource) throws IOException;
258 
259   /**
260    * Load data from the specified resource into a bound object with the type of
261    * the specified Java class.
262    *
263    * @param <CLASS>
264    *          the Java type to load data into
265    * @param clazz
266    *          the class for the java type
267    * @param file
268    *          the resource to load
269    * @return the loaded instance data
270    * @throws IOException
271    *           if an error occurred while loading the data in the specified file
272    */
273   @NonNull
274   default <CLASS extends IBoundObject> CLASS load(
275       @NonNull Class<CLASS> clazz,
276       @NonNull File file) throws IOException {
277     return load(clazz, ObjectUtils.notNull(file.toPath()));
278   }
279 
280   /**
281    * Load data from the specified resource into a bound object with the type of
282    * the specified Java class.
283    *
284    * @param <CLASS>
285    *          the Java type to load data into
286    * @param clazz
287    *          the class for the java type
288    * @param path
289    *          the resource to load
290    * @return the loaded instance data
291    * @throws IOException
292    *           if an error occurred while loading the data in the specified file
293    */
294   @NonNull
295   default <CLASS extends IBoundObject> CLASS load(
296       @NonNull Class<CLASS> clazz,
297       @NonNull Path path) throws IOException {
298     return load(clazz, ObjectUtils.notNull(path.toUri()));
299   }
300 
301   /**
302    * Load data from the specified resource into a bound object with the type of
303    * the specified Java class.
304    *
305    * @param <CLASS>
306    *          the Java type to load data into
307    * @param clazz
308    *          the class for the java type
309    * @param url
310    *          the resource to load
311    * @return the loaded instance data
312    * @throws IOException
313    *           if an error occurred while loading the data in the specified file
314    * @throws URISyntaxException
315    *           if the provided {@code url} is malformed
316    */
317   @NonNull
318   default <CLASS extends IBoundObject> CLASS load(
319       @NonNull Class<CLASS> clazz,
320       @NonNull URL url) throws IOException, URISyntaxException {
321     return load(clazz, ObjectUtils.notNull(url.toURI()));
322   }
323 
324   /**
325    * Load data from the specified resource into a bound object with the type of
326    * the specified Java class.
327    *
328    * @param <CLASS>
329    *          the Java type to load data into
330    * @param clazz
331    *          the class for the java type
332    * @param uri
333    *          the resource to load
334    * @return the loaded instance data
335    * @throws IOException
336    *           if an error occurred while loading the data in the specified file
337    */
338   @NonNull
339   <CLASS extends IBoundObject> CLASS load(
340       @NonNull Class<CLASS> clazz,
341       @NonNull URI uri) throws IOException;
342 
343   /**
344    * Load data from the specified resource into a bound object with the type of
345    * the specified Java class.
346    * <p>
347    * This method will not close the provided {@link InputStream}, since it does
348    * not own the stream.
349    * <p>
350    * Implementations of this method will do format detection. This process might
351    * leave the provided {@link InputStream} at a position beyond the last parsed
352    * location. If you want to avoid this possibility, use and implementation of
353    * {@link IDeserializer#deserialize(InputStream, URI)} instead, such as what is
354    * provided by {@link DefaultBindingContext#newDeserializer(Format, Class)}.
355    *
356    * @param <CLASS>
357    *          the Java type to load data into
358    * @param clazz
359    *          the class for the java type
360    * @param is
361    *          the resource stream
362    * @param resource
363    *          the URI of the resource
364    * @return the loaded data
365    * @throws IOException
366    *           if an error occurred while loading the data from the specified
367    *           resource
368    */
369   @NonNull
370   <CLASS extends IBoundObject> CLASS load(
371       @NonNull Class<CLASS> clazz,
372       @NonNull InputStream is,
373       @NonNull URI resource) throws IOException;
374 
375   /**
376    * Load data from the specified resource into a bound object with the type of
377    * the specified Java class.
378    * <p>
379    * This method will not close the provided {@link InputStream}, since it does
380    * not own the stream.
381    *
382    * @param <CLASS>
383    *          the Java type to load data into
384    * @param format
385    *          the format to parse
386    * @param clazz
387    *          the class for the java type
388    * @param is
389    *          the resource stream
390    * @param resource
391    *          the URI of the resource
392    * @return the loaded data
393    * @throws IOException
394    *           if an error occurred while loading the data from the specified
395    *           resource
396    */
397   @NonNull
398   <CLASS extends IBoundObject> CLASS load(
399       @NonNull Class<CLASS> clazz,
400       @NonNull Format format,
401       @NonNull InputStream is,
402       @NonNull URI resource) throws IOException;
403 
404   /**
405    * Load data expressed using the provided {@code format} and return that data as
406    * a Metapath node item.
407    * <p>
408    * The specific Module model is auto-detected by analyzing the source. The class
409    * reported is implementation specific.
410    *
411    * @param format
412    *          the expected format of the data to parse
413    * @param path
414    *          the resource
415    * @return the Metapath node item for the parsed data
416    * @throws IOException
417    *           if an error occurred while loading the data from the specified
418    *           resource
419    */
420   @NonNull
421   default IDocumentNodeItem loadAsNodeItem(
422       @NonNull Format format,
423       @NonNull Path path) throws IOException {
424     return loadAsNodeItem(format, ObjectUtils.notNull(path.toUri()));
425   }
426 
427   /**
428    * Load data expressed using the provided {@code format} and return that data as
429    * a Metapath node item.
430    * <p>
431    * The specific Module model is auto-detected by analyzing the source. The class
432    * reported is implementation specific.
433    *
434    * @param format
435    *          the expected format of the data to parse
436    * @param uri
437    *          the resource
438    * @return the Metapath node item for the parsed data
439    * @throws IOException
440    *           if an error occurred while loading the data from the specified
441    *           resource
442    */
443   @NonNull
444   IDocumentNodeItem loadAsNodeItem(
445       @NonNull Format format,
446       @NonNull URI uri) throws IOException;
447 
448   /**
449    * Load data expressed using the provided {@code format} and return that data as
450    * a Metapath node item.
451    * <p>
452    * The specific Module model is auto-detected by analyzing the source. The class
453    * reported is implementation specific.
454    *
455    * @param format
456    *          the expected format of the data to parse
457    * @param is
458    *          the resource stream
459    * @param resource
460    *          the URI of the resource
461    * @return the Metapath node item for the parsed data
462    * @throws IOException
463    *           if an error occurred while loading the data from the specified
464    *           resource
465    */
466   @NonNull
467   IDocumentNodeItem loadAsNodeItem(
468       @NonNull Format format,
469       @NonNull InputStream is,
470       @NonNull URI resource) throws IOException;
471 
472   /**
473    * Get the configured Module binding context to use to load Java types.
474    *
475    * @return the binding context
476    */
477   @NonNull
478   IBindingContext getBindingContext();
479 
480   /**
481    * Auto convert the provided {@code source} to the provided {@code toFormat}.
482    * Write the converted content to the provided {@code destination}.
483    * <p>
484    * The format of the source is expected to be auto detected using
485    * {@link #detectFormat(Path)}.
486    *
487    * @param <CLASS>
488    *          the Java type to load data into
489    * @param source
490    *          the resource to convert
491    * @param destination
492    *          the resource to write converted content to
493    * @param toFormat
494    *          the format to convert to
495    * @param rootClass
496    *          the class for the Java type to load data into
497    * @throws FileNotFoundException
498    *           the the provided source file was not found
499    * @throws IOException
500    *           if an error occurred while loading the data from the specified
501    *           resource or writing the converted data to the specified destination
502    */
503   default <CLASS extends IBoundObject> void convert(
504       @NonNull Path source,
505       @NonNull Path destination,
506       @NonNull Format toFormat,
507       @NonNull Class<CLASS> rootClass) throws FileNotFoundException, IOException {
508     CLASS object = load(rootClass, source);
509 
510     ISerializer<CLASS> serializer = getBindingContext().newSerializer(toFormat, rootClass);
511     serializer.serialize(object, destination);
512   }
513 
514   /**
515    * Auto convert the provided {@code source} to the provided {@code toFormat}.
516    * Write the converted content to the provided {@code destination}.
517    * <p>
518    * The format of the source is expected to be auto detected using
519    * {@link #detectFormat(Path)}.
520    *
521    * @param <CLASS>
522    *          the Java type to load data into
523    * @param source
524    *          the resource to convert
525    * @param os
526    *          the output stream to write converted content to
527    * @param toFormat
528    *          the format to convert to
529    * @param rootClass
530    *          the class for the Java type to load data into
531    * @throws FileNotFoundException
532    *           the the provided source file was not found
533    * @throws IOException
534    *           if an error occurred while loading the data from the specified
535    *           resource or writing the converted data to the specified destination
536    */
537   default <CLASS extends IBoundObject> void convert(
538       @NonNull Path source,
539       @NonNull OutputStream os,
540       @NonNull Format toFormat,
541       @NonNull Class<CLASS> rootClass) throws FileNotFoundException, IOException {
542     CLASS object = load(rootClass, source);
543 
544     ISerializer<CLASS> serializer = getBindingContext().newSerializer(toFormat, rootClass);
545     serializer.serialize(object, os);
546   }
547 
548   /**
549    * Auto convert the provided {@code source} to the provided {@code toFormat}.
550    * Write the converted content to the provided {@code destination}.
551    * <p>
552    * The format of the source is expected to be auto detected using
553    * {@link #detectFormat(Path)}.
554    *
555    * @param <CLASS>
556    *          the Java type to load data into
557    * @param source
558    *          the resource to convert
559    * @param destination
560    *          the resource to write converted content to
561    * @param toFormat
562    *          the format to convert to
563    * @param rootClass
564    *          the class for the Java type to load data into
565    * @throws FileNotFoundException
566    *           the the provided source file was not found
567    * @throws IOException
568    *           if an error occurred while loading the data from the specified
569    *           resource or writing the converted data to the specified destination
570    */
571   default <CLASS extends IBoundObject> void convert(
572       @NonNull URI source,
573       @NonNull Path destination,
574       @NonNull Format toFormat,
575       @NonNull Class<CLASS> rootClass) throws FileNotFoundException, IOException {
576     CLASS object = load(rootClass, source);
577 
578     ISerializer<CLASS> serializer = getBindingContext().newSerializer(toFormat, rootClass);
579     serializer.serialize(object, destination);
580   }
581 
582   /**
583    * Auto convert the provided {@code source} to the provided {@code toFormat}.
584    * Write the converted content to the provided {@code destination}.
585    * <p>
586    * The format of the source is expected to be auto detected using
587    * {@link #detectFormat(Path)}.
588    *
589    * @param <CLASS>
590    *          the Java type to load data into
591    * @param source
592    *          the resource to convert
593    * @param os
594    *          the output stream to write converted content to
595    * @param toFormat
596    *          the format to convert to
597    * @param rootClass
598    *          the class for the Java type to load data into
599    * @throws FileNotFoundException
600    *           the the provided source file was not found
601    * @throws IOException
602    *           if an error occurred while loading the data from the specified
603    *           resource or writing the converted data to the specified destination
604    */
605   default <CLASS extends IBoundObject> void convert(
606       @NonNull URI source,
607       @NonNull OutputStream os,
608       @NonNull Format toFormat,
609       @NonNull Class<CLASS> rootClass) throws FileNotFoundException, IOException {
610     CLASS object = load(rootClass, source);
611 
612     ISerializer<CLASS> serializer = getBindingContext().newSerializer(toFormat, rootClass);
613     serializer.serialize(object, os);
614   }
615 
616   /**
617    * Auto convert the provided {@code source} to the provided {@code toFormat}.
618    * Write the converted content to the provided {@code destination}.
619    * <p>
620    * The format of the source is expected to be auto detected using
621    * {@link #detectFormat(Path)}.
622    *
623    * @param <CLASS>
624    *          the Java type to load data into
625    * @param source
626    *          the resource to convert
627    * @param writer
628    *          the writer to write converted content to
629    * @param toFormat
630    *          the format to convert to
631    * @param rootClass
632    *          the class for the Java type to load data into
633    * @throws FileNotFoundException
634    *           the the provided source file was not found
635    * @throws IOException
636    *           if an error occurred while loading the data from the specified
637    *           resource or writing the converted data to the specified destination
638    */
639   default <CLASS extends IBoundObject> void convert(
640       @NonNull URI source,
641       @NonNull Writer writer,
642       @NonNull Format toFormat,
643       @NonNull Class<CLASS> rootClass) throws FileNotFoundException, IOException {
644     CLASS object = load(rootClass, source);
645 
646     ISerializer<CLASS> serializer = getBindingContext().newSerializer(toFormat, rootClass);
647     serializer.serialize(object, writer);
648   }
649 }