1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.io;
7   
8   import com.fasterxml.jackson.core.JsonFactory;
9   import com.fasterxml.jackson.core.format.DataFormatDetector;
10  import com.fasterxml.jackson.core.format.DataFormatMatcher;
11  import com.fasterxml.jackson.core.format.MatchStrength;
12  import com.fasterxml.jackson.dataformat.xml.XmlFactory;
13  import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
14  
15  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
16  import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
17  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
18  import gov.nist.secauto.metaschema.databind.io.json.JsonFactoryFactory;
19  import gov.nist.secauto.metaschema.databind.io.yaml.impl.YamlFactoryFactory;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.URL;
24  
25  import edu.umd.cs.findbugs.annotations.NonNull;
26  
27  /**
28   * Provides a means to analyze content to determine what {@link Format} the data
29   * is represented as.
30   */
31  public class FormatDetector {
32  
33    private final DataFormatDetector detector;
34  
35    /**
36     * Construct a new format detector using the default configuration.
37     */
38    public FormatDetector() {
39      this(new DefaultConfiguration<>());
40    }
41  
42    /**
43     * Construct a new format detector using the provided {@code configuration}.
44     *
45     * @param configuration
46     *          the deserialization configuration to use for detection
47     */
48    public FormatDetector(
49        @NonNull IConfiguration<DeserializationFeature<?>> configuration) {
50      this(configuration, newDetectorFactory(configuration));
51    }
52  
53    /**
54     * Construct a new format detector using the provided {@code configuration}.
55     *
56     * @param configuration
57     *          the deserialization configuration to use for detection
58     * @param detectors
59     *          the JSON parser instances to use for format detection
60     */
61    protected FormatDetector(
62        @NonNull IConfiguration<DeserializationFeature<?>> configuration,
63        @NonNull JsonFactory... detectors) {
64      int lookaheadBytes = configuration.get(DeserializationFeature.FORMAT_DETECTION_LOOKAHEAD_LIMIT);
65      this.detector = new DataFormatDetector(detectors)
66          .withMinimalMatch(MatchStrength.INCONCLUSIVE)
67          .withOptimalMatch(MatchStrength.SOLID_MATCH)
68          .withMaxInputLookahead(lookaheadBytes - 1);
69  
70    }
71  
72    @NonNull
73    private static JsonFactory[] newDetectorFactory(@NonNull IConfiguration<DeserializationFeature<?>> config) {
74      JsonFactory[] detectorFactory = new JsonFactory[3];
75      detectorFactory[0] = YamlFactoryFactory.newParserFactoryInstance(config);
76      detectorFactory[1] = JsonFactoryFactory.instance();
77      detectorFactory[2] = new XmlFactory();
78      return detectorFactory;
79    }
80  
81    /**
82     * Analyzes the provided {@code resource} to determine it's format.
83     *
84     * @param resource
85     *          the resource to analyze
86     * @return the analysis result
87     * @throws IOException
88     *           if an error occurred while reading the resource
89     */
90    @NonNull
91    public Result detect(@NonNull URL resource) throws IOException {
92      try (InputStream is = ObjectUtils.notNull(resource.openStream())) {
93        return detect(is);
94      }
95    }
96  
97    /**
98     * Analyzes the data from the provided {@code inputStream} to determine it's
99     * format.
100    *
101    * @param inputStream
102    *          the resource stream to analyze
103    * @return the analysis result
104    * @throws IOException
105    *           if an error occurred while reading the resource
106    */
107   @NonNull
108   public Result detect(@NonNull InputStream inputStream) throws IOException {
109     DataFormatMatcher matcher = detector.findFormat(inputStream);
110     switch (matcher.getMatchStrength()) {
111     case FULL_MATCH:
112     case SOLID_MATCH:
113     case WEAK_MATCH:
114     case INCONCLUSIVE:
115       return new Result(matcher);
116     case NO_MATCH:
117     default:
118       throw new IOException("Unable to identify format");
119     }
120   }
121 
122   public static final class Result {
123     @NonNull
124     private final DataFormatMatcher matcher;
125 
126     private Result(@NonNull DataFormatMatcher matcher) {
127       this.matcher = matcher;
128     }
129 
130     /**
131      * Get the detected format.
132      *
133      * @return the format
134      */
135     @NonNull
136     public Format getFormat() {
137       Format retval;
138       String formatName = matcher.getMatchedFormatName();
139       if (YAMLFactory.FORMAT_NAME_YAML.equals(formatName)) {
140         retval = Format.YAML;
141       } else if (JsonFactory.FORMAT_NAME_JSON.equals(formatName)) {
142         retval = Format.JSON;
143       } else if (XmlFactory.FORMAT_NAME_XML.equals(formatName)) {
144         retval = Format.XML;
145       } else {
146         throw new UnsupportedOperationException(String.format("The detected format '%s' is not supported", formatName));
147       }
148       return retval;
149     }
150 
151     /**
152      * Get an {@link InputStream} that can be used to read the analyzed data from
153      * the start.
154      *
155      * @return the stream
156      */
157     @SuppressWarnings("resource")
158     @NonNull
159     public InputStream getDataStream() {
160       return ObjectUtils.notNull(matcher.getDataStream());
161     }
162 
163     // @SuppressWarnings("resource")
164     // @NonNull
165     // public JsonParser getParser() throws IOException {
166     // return ObjectUtils.notNull(matcher.createParserWithMatch());
167     // }
168 
169     /**
170      * Get the strength of the match.
171      *
172      * @return the strength
173      */
174     @NonNull
175     public MatchStrength getMatchStrength() {
176       return ObjectUtils.notNull(matcher.getMatchStrength());
177     }
178   }
179 }